<commit_before>package shareit

import (
	"encoding/json"
	"fmt"
	"github.com/gorilla/mux"
	"io"
	"io/ioutil"
	"net/http"
	"path/filepath"
	//	"strconv"
	"archive/zip"
	"errors"
	"github.com/scritch007/ShareMinatorApiGenerator/api"
	"github.com/scritch007/go-tools"
	"github.com/scritch007/shareit/browse"
	"github.com/scritch007/shareit/share_link"
	"github.com/scritch007/shareit/types"
	"os"
	"strconv"
	"strings"
	"time"
)

//ServeContent(w ResponseWriter, req *Request, name string, modtime time.Time, content io.ReadSeeker)
//CommandHandler is used to keep information about issued commands
type CommandHandler struct {
	config          *types.Configuration
	shareLink       *share_link.ShareLinkHandler
	browser         *browse.BrowseHandler
	UploadChunkSize int64 `json:"upload_chunk_size"`
}

func (c *CommandHandler) save(command *types.Command) error {
	return c.config.Db.SaveCommand(command)
}

// CommandHandler constructor
func NewCommandHandler(config *types.Configuration) (c *CommandHandler) {
	c = new(CommandHandler)
	c.config = config
	c.shareLink = share_link.NewShareLinkHandler(config)
	c.browser = browse.NewBrowseHandler(config)
	c.UploadChunkSize = int64(config.UploadChunkSize)
	return c
}

func (c *CommandHandler) getHandler(command *api.Command) types.CommandHandler {
	if strings.Contains(string(command.Name), "browser.") {
		return c.browser
	} else if strings.Contains(string(command.Name), share_link.COMMAND_PREFIX) {
		return c.shareLink
	} else {
		return nil
	}
}

//Handle Request on /commands
//Only GET and POST request are available
func (c *CommandHandler) Commands(w http.ResponseWriter, r *http.Request) {
	user, err := c.config.Auth.GetAuthenticatedUser(w, r)
	if nil != err {
		http.Error(w, err.Error(), http.StatusUnauthorized)
		return
	}

	if "GET" == r.Method {
		// We want to list the commands that have been already answered
		var userName *string = nil
		if nil != user {
			userName = &user.Id
		}
		commands, _, err := c.config.Db.ListCommands(userName, 0, -1, nil)
		if nil != err {
			errMessage := fmt.Sprintf("Invalid Input: %s", err)
			tools.LOG_ERROR.Println(errMessage)
			http.Error(w, errMessage, http.StatusInternalServerError)
			return
		}
		b, _ := json.Marshal(commands)
		io.WriteString(w, string(b))
		return
	}
	// Extract the POST body
	command := new(api.Command)

	input, err := ioutil.ReadAll(r.Body)
	if nil != err {
		errMessage := fmt.Sprintf("1 Failed with error code: %s", err)
		tools.LOG_ERROR.Println(errMessage)
		http.Error(w, errMessage, http.StatusBadRequest)
		return
	}
	err = json.Unmarshal(input, command)

	backendCommand := new(types.Command)
	backendCommand.ApiCommand = command
	if nil != user {
		backendCommand.User = &user.Id //Store current user
	} else {
		backendCommand.User = nil
	}
	if nil != err {
		//TODO Set erro Code
	}
	channel := make(chan types.EnumCommandHandlerStatus)
	command.State.Progress = 0
	command.State.ErrorCode = 0
	command.State.Status = api.COMMAND_STATUS_IN_PROGRESS
	err = c.save(backendCommand)
	if nil != err {
		http.Error(w, "Couldn't save this command", http.StatusInternalServerError)
		return
	}

	handler := c.getHandler(command)
	if nil == handler {
		http.Error(w, "Unknown Request Type", http.StatusBadRequest)
		return
	}
	commandContext := types.CommandContext{backendCommand, user, r}
	hErr := handler.Handle(&commandContext, channel)
	if nil != hErr {
		http.Error(w, hErr.Err.Error(), hErr.Status)
		return
	}
	timeout := time.Duration(command.Timeout)
	if 0 == timeout {
		timeout = 5
	}
	//timer := time.NewTimer(1)
	timer := time.NewTimer(timeout * time.Second)

	select {
	case a := <-channel:
		tools.LOG_DEBUG.Println("Got answer from command")
		timer.Stop()
		if types.EnumCommandHandlerDone == a {
			command.State.Status = api.COMMAND_STATUS_DONE
			command.State.Progress = 100
		} else if types.EnumCommandHandlerError == a {
			command.State.Status = api.COMMAND_STATUS_ERROR
			command.State.Progress = 100
		}
		c.save(backendCommand)
	case <-timer.C:
		tools.LOG_DEBUG.Println("Timer just elapsed")
		go func() {
			//Wait for the command to end
			a := <-channel
			if types.EnumCommandHandlerDone == a {
				command.State.Status = api.COMMAND_STATUS_DONE
				command.State.Progress = 100
			} else if types.EnumCommandHandlerError == a {
				command.State.Status = api.COMMAND_STATUS_ERROR
				command.State.Progress = 100
			}
			c.save(backendCommand)
		}()
	}
	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	b, _ := json.Marshal(command)
	io.WriteString(w, string(b))
}

//This is extracted from the net/http/fs.go file
type httpRange struct {
	start, length int64
}

func parseRange(ra string, size int64) (*httpRange, error) {
	ra = strings.TrimSpace(ra)
	if ra == "" {
		return nil, errors.New("invalid range 1")
	}
	if !strings.HasPrefix(ra, "bytes") {
		return nil, errors.New("invalid range 1.1")
	}
	ra = ra[6:]
	i := strings.Index(ra, "-")
	if i < 0 {
		return nil, errors.New("invalid range 2")
	}
	start, endAndSize := strings.TrimSpace(ra[:i]), strings.TrimSpace(ra[i+1:])

	i = strings.Index(endAndSize, "/")

	end, rSizeStr := strings.TrimSpace(endAndSize[:i]), strings.TrimSpace(endAndSize[i+1:])

	value, err := strconv.ParseInt(rSizeStr, 10, 64)
	if err != nil {
		return nil, errors.New("invalid range 2.5")
	}
	rSize := value

	if rSize != size {
		return nil, errors.New("Invalid range 3")
	}

	var r httpRange
	value, err = strconv.ParseInt(start, 10, 64)
	if err != nil || value > size || value < 0 {
		return nil, errors.New("invalid range 4")
	}
	r.start = value
	value, err = strconv.ParseInt(end, 10, 64)
	if err != nil || r.start > value {
		return nil, errors.New("invalid range 5")
	}
	if value >= size {
		value = size - 1
	}
	r.length = value - r.start + 1
	return &r, nil
}

func (c *CommandHandler) Command(w http.ResponseWriter, r *http.Request) {
	user, err := c.config.Auth.GetAuthenticatedUser(w, r)
	if nil != err {
		http.Error(w, err.Error(), http.StatusUnauthorized)
		return
	}
	vars := mux.Vars(r)
	ref := vars["command_id"]
	command, err := c.config.Db.GetCommand(ref)
	if nil != err {
		http.Error(w, fmt.Sprintf("Couldn't get this command ref %s", ref), http.StatusBadRequest)
		return
	}
	if nil != command.User && (nil == user || *command.User != user.Id) {
		http.Error(w, "You are trying to access some resources that do not belong to you", http.StatusUnauthorized)
		return
	}

	if "GET" == r.Method {
		b, _ := json.Marshal(command)
		io.WriteString(w, string(b))
	} else if "PUT" == r.Method {

		if 100 == command.ApiCommand.State.Progress {
			http.Error(w, "Command already completed", http.StatusUnauthorized)
			return
		}
		// make a buffer to keep chunks that are read
		buf := make([]byte, c.UploadChunkSize)
		total_size := r.ContentLength
		var chunk_offset int64
		chunk_offset = int64(0)
		for {
			read_size, err := io.ReadFull(r.Body, buf)
			tools.LOG_DEBUG.Println("Received ", read_size, "bytes")
			if read_size == 0 {
				// EOF case
				break
			}
			if chunk_offset+int64(read_size) != int64(total_size) {
				if nil != err {
					errMessage := fmt.Sprintf("1 Failed with error code: %s", err)
					tools.LOG_ERROR.Println(errMessage)
					http.Error(w, errMessage, http.StatusBadRequest)
					return
				}
			}
			h := c.getHandler(command.ApiCommand)
			commandContext := types.CommandContext{command, user, r}
			uploadPath, size, hErr := h.GetUploadPath(&commandContext)

			if nil != hErr {
				errMessage := fmt.Sprintf("Failed to get upload path with error code: %s", hErr.Err)
				tools.LOG_ERROR.Println(errMessage)
				http.Error(w, errMessage, hErr.Status)
				return
			}
			rangeHeader := r.Header.Get("Content-Range")

			if _, err := os.Stat(*uploadPath); err != nil {
				if os.IsNotExist(err) {
					fo, err := os.Create(*uploadPath)
					if nil != err {
						errMessage := fmt.Sprintf("Couldn't create File with error %s", err)
						http.Error(w, errMessage, http.StatusInternalServerError)
						return
					}
					fo.Close()
				} else {
					errMessage := fmt.Sprintf("Couldn't read stat with error %s", err)
					tools.LOG_ERROR.Println(errMessage)
					http.Error(w, errMessage, http.StatusInternalServerError)
					return
				}
			}
			var offset int64 = 0
			if 0 != len(rangeHeader) {
				rangeValue, err := parseRange(rangeHeader, size)
				if nil != err {
					errMessage := fmt.Sprintf("Incorrect Range header %s", err.Error())
					tools.LOG_ERROR.Println(errMessage)
					http.Error(w, errMessage, http.StatusBadRequest)
					return
				}
				if size < rangeValue.start {
					errMessage := fmt.Sprintf("Couldn't seek to requested offset %d", rangeValue.start)
					tools.LOG_ERROR.Println(errMessage)
					http.Error(w, errMessage, http.StatusBadRequest)
					return
				}

				offset = rangeValue.start
			}
			f, err := os.OpenFile(*uploadPath, os.O_RDWR, os.ModePerm)
			if nil != err {
				errMessage := fmt.Sprintf("Failed to open file with error %s", err)
				tools.LOG_ERROR.Println(errMessage)
				http.Error(w, errMessage, http.StatusInternalServerError)
				return
			}
			defer f.Close()
			f.Seek(offset+chunk_offset, os.SEEK_SET)
			io.WriteString(f, string(buf[:read_size]))
			command.ApiCommand.State.Progress = int((offset + chunk_offset + int64(read_size)) * 100 / size)
			if 100 == command.ApiCommand.State.Progress {
				command.ApiCommand.State.Status = api.COMMAND_STATUS_DONE
			}
			chunk_offset += int64(read_size)
		}
	}
}

func (c *CommandHandler) Download(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)
	file := vars["file"]

	link, err := c.config.Db.GetDownloadLink(file)
	//Get the realpath depending on the configuration and the sharelink or direct download
	if nil == err {
		tools.LOG_DEBUG.Println("Serving file ", *link.RealPath)
		fileInfo, err := os.Lstat(*link.RealPath)
		if nil != err {
			http.Error(w, "Download link doesn't point to a valid path", http.StatusNotFound)
			return
		}
		if fileInfo.IsDir() {
			zipFileName := fileInfo.Name() + ".zip"
			w.Header().Set("Content-Type", "application/zip")
			w.Header().Set("Content-Disposition", `attachment; filename="`+zipFileName+`"`)
			zw := zip.NewWriter(w)
			defer zw.Close()
			// Walk directory.
			filepath.Walk(*link.RealPath, func(path string, info os.FileInfo, err error) error {
				if info.IsDir() {
					return nil
				}
				// Remove base path, convert to forward slash.
				zipPath := path[len(*link.RealPath):]
				zipPath = strings.TrimLeft(strings.Replace(zipPath, `\`, "/", -1), `/`)
				ze, err := zw.Create(zipPath)
				if err != nil {
					fmt.Fprintf(os.Stderr, "Cannot create zip entry <%s>: %s\n", zipPath, err)
					return err
				}
				file, err := os.Open(path)
				if err != nil {
					fmt.Fprintf(os.Stderr, "Cannot open file <%s>: %s\n", path, err)
					return err
				}
				defer file.Close()
				io.Copy(ze, file)
				return nil
			})

		} else {
			w.Header().Set("Content-Disposition", "attachment; filename="+filepath.Base(*link.RealPath))
			http.ServeFile(w, r, *link.RealPath)
		}

	} else {
		http.Error(w, "Download link is unavailable. Try renewing link", http.StatusNotFound)
	}
}
<commit_msg>[shareit] commnand: Fix read error management during upload in case of EOF<commit_after>package shareit

import (
	"encoding/json"
	"fmt"
	"github.com/gorilla/mux"
	"io"
	"io/ioutil"
	"net/http"
	"path/filepath"
	//	"strconv"
	"archive/zip"
	"errors"
	"github.com/scritch007/ShareMinatorApiGenerator/api"
	"github.com/scritch007/go-tools"
	"github.com/scritch007/shareit/browse"
	"github.com/scritch007/shareit/share_link"
	"github.com/scritch007/shareit/types"
	"os"
	"strconv"
	"strings"
	"time"
)

//ServeContent(w ResponseWriter, req *Request, name string, modtime time.Time, content io.ReadSeeker)
//CommandHandler is used to keep information about issued commands
type CommandHandler struct {
	config          *types.Configuration
	shareLink       *share_link.ShareLinkHandler
	browser         *browse.BrowseHandler
	UploadChunkSize int64 `json:"upload_chunk_size"`
}

func (c *CommandHandler) save(command *types.Command) error {
	return c.config.Db.SaveCommand(command)
}

// CommandHandler constructor
func NewCommandHandler(config *types.Configuration) (c *CommandHandler) {
	c = new(CommandHandler)
	c.config = config
	c.shareLink = share_link.NewShareLinkHandler(config)
	c.browser = browse.NewBrowseHandler(config)
	c.UploadChunkSize = int64(config.UploadChunkSize)
	return c
}

func (c *CommandHandler) getHandler(command *api.Command) types.CommandHandler {
	if strings.Contains(string(command.Name), "browser.") {
		return c.browser
	} else if strings.Contains(string(command.Name), share_link.COMMAND_PREFIX) {
		return c.shareLink
	} else {
		return nil
	}
}

//Handle Request on /commands
//Only GET and POST request are available
func (c *CommandHandler) Commands(w http.ResponseWriter, r *http.Request) {
	user, err := c.config.Auth.GetAuthenticatedUser(w, r)
	if nil != err {
		http.Error(w, err.Error(), http.StatusUnauthorized)
		return
	}

	if "GET" == r.Method {
		// We want to list the commands that have been already answered
		var userName *string = nil
		if nil != user {
			userName = &user.Id
		}
		commands, _, err := c.config.Db.ListCommands(userName, 0, -1, nil)
		if nil != err {
			errMessage := fmt.Sprintf("Invalid Input: %s", err)
			tools.LOG_ERROR.Println(errMessage)
			http.Error(w, errMessage, http.StatusInternalServerError)
			return
		}
		b, _ := json.Marshal(commands)
		io.WriteString(w, string(b))
		return
	}
	// Extract the POST body
	command := new(api.Command)

	input, err := ioutil.ReadAll(r.Body)
	if nil != err {
		errMessage := fmt.Sprintf("1 Failed with error code: %s", err)
		tools.LOG_ERROR.Println(errMessage)
		http.Error(w, errMessage, http.StatusBadRequest)
		return
	}
	err = json.Unmarshal(input, command)

	backendCommand := new(types.Command)
	backendCommand.ApiCommand = command
	if nil != user {
		backendCommand.User = &user.Id //Store current user
	} else {
		backendCommand.User = nil
	}
	if nil != err {
		//TODO Set erro Code
	}
	channel := make(chan types.EnumCommandHandlerStatus)
	command.State.Progress = 0
	command.State.ErrorCode = 0
	command.State.Status = api.COMMAND_STATUS_IN_PROGRESS
	err = c.save(backendCommand)
	if nil != err {
		http.Error(w, "Couldn't save this command", http.StatusInternalServerError)
		return
	}

	handler := c.getHandler(command)
	if nil == handler {
		http.Error(w, "Unknown Request Type", http.StatusBadRequest)
		return
	}
	commandContext := types.CommandContext{backendCommand, user, r}
	hErr := handler.Handle(&commandContext, channel)
	if nil != hErr {
		http.Error(w, hErr.Err.Error(), hErr.Status)
		return
	}
	timeout := time.Duration(command.Timeout)
	if 0 == timeout {
		timeout = 5
	}
	//timer := time.NewTimer(1)
	timer := time.NewTimer(timeout * time.Second)

	select {
	case a := <-channel:
		tools.LOG_DEBUG.Println("Got answer from command")
		timer.Stop()
		if types.EnumCommandHandlerDone == a {
			command.State.Status = api.COMMAND_STATUS_DONE
			command.State.Progress = 100
		} else if types.EnumCommandHandlerError == a {
			command.State.Status = api.COMMAND_STATUS_ERROR
			command.State.Progress = 100
		}
		c.save(backendCommand)
	case <-timer.C:
		tools.LOG_DEBUG.Println("Timer just elapsed")
		go func() {
			//Wait for the command to end
			a := <-channel
			if types.EnumCommandHandlerDone == a {
				command.State.Status = api.COMMAND_STATUS_DONE
				command.State.Progress = 100
			} else if types.EnumCommandHandlerError == a {
				command.State.Status = api.COMMAND_STATUS_ERROR
				command.State.Progress = 100
			}
			c.save(backendCommand)
		}()
	}
	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	b, _ := json.Marshal(command)
	io.WriteString(w, string(b))
}

//This is extracted from the net/http/fs.go file
type httpRange struct {
	start, length int64
}

func parseRange(ra string, size int64) (*httpRange, error) {
	ra = strings.TrimSpace(ra)
	if ra == "" {
		return nil, errors.New("invalid range 1")
	}
	if !strings.HasPrefix(ra, "bytes") {
		return nil, errors.New("invalid range 1.1")
	}
	ra = ra[6:]
	i := strings.Index(ra, "-")
	if i < 0 {
		return nil, errors.New("invalid range 2")
	}
	start, endAndSize := strings.TrimSpace(ra[:i]), strings.TrimSpace(ra[i+1:])

	i = strings.Index(endAndSize, "/")

	end, rSizeStr := strings.TrimSpace(endAndSize[:i]), strings.TrimSpace(endAndSize[i+1:])

	value, err := strconv.ParseInt(rSizeStr, 10, 64)
	if err != nil {
		return nil, errors.New("invalid range 2.5")
	}
	rSize := value

	if rSize != size {
		return nil, errors.New("Invalid range 3")
	}

	var r httpRange
	value, err = strconv.ParseInt(start, 10, 64)
	if err != nil || value > size || value < 0 {
		return nil, errors.New("invalid range 4")
	}
	r.start = value
	value, err = strconv.ParseInt(end, 10, 64)
	if err != nil || r.start > value {
		return nil, errors.New("invalid range 5")
	}
	if value >= size {
		value = size - 1
	}
	r.length = value - r.start + 1
	return &r, nil
}

func (c *CommandHandler) Command(w http.ResponseWriter, r *http.Request) {
	user, err := c.config.Auth.GetAuthenticatedUser(w, r)
	if nil != err {
		http.Error(w, err.Error(), http.StatusUnauthorized)
		return
	}
	vars := mux.Vars(r)
	ref := vars["command_id"]
	command, err := c.config.Db.GetCommand(ref)
	if nil != err {
		http.Error(w, fmt.Sprintf("Couldn't get this command ref %s", ref), http.StatusBadRequest)
		return
	}
	if nil != command.User && (nil == user || *command.User != user.Id) {
		http.Error(w, "You are trying to access some resources that do not belong to you", http.StatusUnauthorized)
		return
	}

	if "GET" == r.Method {
		b, _ := json.Marshal(command)
		io.WriteString(w, string(b))
	} else if "PUT" == r.Method {

		if 100 == command.ApiCommand.State.Progress {
			http.Error(w, "Command already completed", http.StatusUnauthorized)
			return
		}
		// make a buffer to keep chunks that are read
		buf := make([]byte, 0, c.UploadChunkSize)
		total_size := r.ContentLength
		var chunk_offset int64
		var buf_dim int64
		chunk_offset = int64(0)
		for {
			rest := total_size - chunk_offset
			if rest == 0 {
				// EOF
				break
			}
			if rest > c.UploadChunkSize {
				buf_dim = c.UploadChunkSize
			} else {
				buf_dim = rest
			}

			read_size, err := io.ReadFull(r.Body, buf[:buf_dim])
			if nil != err {
				errMessage := fmt.Sprintf("1 Failed with error code: %s", err)
				tools.LOG_ERROR.Println(errMessage)
				http.Error(w, errMessage, http.StatusBadRequest)
				return
			}
			tools.LOG_DEBUG.Println("Received ", read_size, "bytes")
			h := c.getHandler(command.ApiCommand)
			commandContext := types.CommandContext{command, user, r}
			uploadPath, size, hErr := h.GetUploadPath(&commandContext)

			if nil != hErr {
				errMessage := fmt.Sprintf("Failed to get upload path with error code: %s", hErr.Err)
				tools.LOG_ERROR.Println(errMessage)
				http.Error(w, errMessage, hErr.Status)
				return
			}
			rangeHeader := r.Header.Get("Content-Range")

			if _, err := os.Stat(*uploadPath); err != nil {
				if os.IsNotExist(err) {
					fo, err := os.Create(*uploadPath)
					if nil != err {
						errMessage := fmt.Sprintf("Couldn't create File with error %s", err)
						http.Error(w, errMessage, http.StatusInternalServerError)
						return
					}
					fo.Close()
				} else {
					errMessage := fmt.Sprintf("Couldn't read stat with error %s", err)
					tools.LOG_ERROR.Println(errMessage)
					http.Error(w, errMessage, http.StatusInternalServerError)
					return
				}
			}
			var offset int64 = 0
			if 0 != len(rangeHeader) {
				rangeValue, err := parseRange(rangeHeader, size)
				if nil != err {
					errMessage := fmt.Sprintf("Incorrect Range header %s", err.Error())
					tools.LOG_ERROR.Println(errMessage)
					http.Error(w, errMessage, http.StatusBadRequest)
					return
				}
				if size < rangeValue.start {
					errMessage := fmt.Sprintf("Couldn't seek to requested offset %d", rangeValue.start)
					tools.LOG_ERROR.Println(errMessage)
					http.Error(w, errMessage, http.StatusBadRequest)
					return
				}

				offset = rangeValue.start
			}
			f, err := os.OpenFile(*uploadPath, os.O_RDWR, os.ModePerm)
			if nil != err {
				errMessage := fmt.Sprintf("Failed to open file with error %s", err)
				tools.LOG_ERROR.Println(errMessage)
				http.Error(w, errMessage, http.StatusInternalServerError)
				return
			}
			defer f.Close()
			f.Seek(offset+chunk_offset, os.SEEK_SET)
			io.WriteString(f, string(buf[:read_size]))
			command.ApiCommand.State.Progress = int((offset + chunk_offset + int64(read_size)) * 100 / size)
			if 100 == command.ApiCommand.State.Progress {
				command.ApiCommand.State.Status = api.COMMAND_STATUS_DONE
			}
			chunk_offset += int64(read_size)
		}
	}
}

func (c *CommandHandler) Download(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)
	file := vars["file"]

	link, err := c.config.Db.GetDownloadLink(file)
	//Get the realpath depending on the configuration and the sharelink or direct download
	if nil == err {
		tools.LOG_DEBUG.Println("Serving file ", *link.RealPath)
		fileInfo, err := os.Lstat(*link.RealPath)
		if nil != err {
			http.Error(w, "Download link doesn't point to a valid path", http.StatusNotFound)
			return
		}
		if fileInfo.IsDir() {
			zipFileName := fileInfo.Name() + ".zip"
			w.Header().Set("Content-Type", "application/zip")
			w.Header().Set("Content-Disposition", `attachment; filename="`+zipFileName+`"`)
			zw := zip.NewWriter(w)
			defer zw.Close()
			// Walk directory.
			filepath.Walk(*link.RealPath, func(path string, info os.FileInfo, err error) error {
				if info.IsDir() {
					return nil
				}
				// Remove base path, convert to forward slash.
				zipPath := path[len(*link.RealPath):]
				zipPath = strings.TrimLeft(strings.Replace(zipPath, `\`, "/", -1), `/`)
				ze, err := zw.Create(zipPath)
				if err != nil {
					fmt.Fprintf(os.Stderr, "Cannot create zip entry <%s>: %s\n", zipPath, err)
					return err
				}
				file, err := os.Open(path)
				if err != nil {
					fmt.Fprintf(os.Stderr, "Cannot open file <%s>: %s\n", path, err)
					return err
				}
				defer file.Close()
				io.Copy(ze, file)
				return nil
			})

		} else {
			w.Header().Set("Content-Disposition", "attachment; filename="+filepath.Base(*link.RealPath))
			http.ServeFile(w, r, *link.RealPath)
		}

	} else {
		http.Error(w, "Download link is unavailable. Try renewing link", http.StatusNotFound)
	}
}
<|endoftext|>


<commit_before>package main

import (
	"github.com/buildkite/agent/command"
	"github.com/codegangsta/cli"
)

var Commands []cli.Command

var AgentDescription = `Usage:

   buildkite-agent start [arguments...]

Description:

   When a job is ready to run it will call the "bootstrap-script"
   and pass it all the environment variables required for the job to run.
   This script is responsible for checking out the code, and running the
   actual build script defined in the project.

   The agent will run any jobs within a PTY (pseudo terminal) if available.

Example:

   $ buildkite-agent start --token xxx`

var ShasumHelpDescription = `Usage:

   buildkite-agent artifact shasum [arguments...]

Description:

   Prints to STDOUT the SHA-1 for the artifact provided. If your search query
   for artifacts matches multiple agents, and error will be raised.

   Note: You need to ensure that your search query is surrounded by quotes if
   using a wild card as the built-in shell path globbing will provide files,
   which will break the download.

Example:

   $ buildkite-agent artifact shasum "pkg/release.tar.gz" --build xxx

   This will search for all the files in the build with the path "pkg/release.tar.gz" and will
   print to STDOUT it's SHA-1 checksum.

   If you would like to target artifacts from a specific build step, you can do
   so by using the --job argument.

   $ buildkite-agent artifact shasum "pkg/release.tar.gz" --job "release" --build xxx

   You can also use the job's id (provided by the environment variable $BUILDKITE_JOB_ID)`

var DownloadHelpDescription = `Usage:

   buildkite-agent artifact download [arguments...]

Description:

   Downloads artifacts from Buildkite to the local machine.

   Note: You need to ensure that your search query is surrounded by quotes if
   using a wild card as the built-in shell path globbing will provide files,
   which will break the download.

Example:

   $ buildkite-agent artifact download "pkg/*.tar.gz" . --build xxx

   This will search across all the artifacts for the build with files that match that part.
   The first argument is the search query, and the second argument is the download destination.

   If you're trying to download a specific file, and there are multiple artifacts from different
   jobs, you can target the particular job you want to download the artifact from:

   $ buildkite-agent artifact download "pkg/*.tar.gz" . --job "tests" --build xxx

   You can also use the job's id (provided by the environment variable $BUILDKITE_JOB_ID)`

var UploadHelpDescription = `Usage:

   buildkite-agent artifact upload <pattern> <destination> [arguments...]

Description:

   Uploads files to a job as artifacts.

   You need to ensure that the paths are surrounded by quotes otherwise the
   built-in shell path globbing will provide the files, which is currently not
   supported.

Example:

   $ buildkite-agent artifact upload "log/**/*.log"

   You can also upload directly to Amazon S3 if you'd like to host your own artifacts:

   $ export BUILDKITE_S3_ACCESS_KEY_ID=xxx
   $ export BUILDKITE_S3_SECRET_ACCESS_KEY=yyy
   $ export BUILDKITE_S3_DEFAULT_REGION=eu-central-1 # default is us-east-1
   $ export BUILDKITE_S3_ACL=private # default is public-read
   $ buildkite-agent artifact upload "log/**/*.log" s3://name-of-your-s3-bucket/$BUILDKITE_JOB_ID`

var SetHelpDescription = `Usage:

   buildkite-agent meta-data set <key> <value> [arguments...]

Description:

   Set arbitrary data on a build using a basic key/value store.

Example:

   $ buildkite-agent meta-data set "foo" "bar"`

var GetHelpDescription = `Usage:

   buildkite-agent meta-data get <key> [arguments...]

Description:

   Get data from a builds key/value store.

Example:

   $ buildkite-agent meta-data get "foo"`

var DefaultEndpoint = "https://agent.buildkite.com/v2"

func init() {
	Commands = []cli.Command{
		{
			Name:        "start",
			Usage:       "Starts a Buildkite agent",
			Description: AgentDescription,
			Flags: []cli.Flag{
				cli.StringFlag{
					Name:   "config",
					Value:  "",
					Usage:  "Path to a configration file",
					EnvVar: "BUILDKITE_AGENT_CONFIG",
				},
				cli.StringFlag{
					Name:   "token",
					Value:  "",
					Usage:  "Your account agent token",
					EnvVar: "BUILDKITE_AGENT_TOKEN",
				},
				cli.StringFlag{
					Name:   "name",
					Value:  "",
					Usage:  "The name of the agent",
					EnvVar: "BUILDKITE_AGENT_NAME",
				},
				cli.StringFlag{
					Name:   "priority",
					Value:  "",
					Usage:  "The priority of the agent (higher priorities are assigned work first)",
					EnvVar: "BUILDKITE_AGENT_PRIORITY",
				},
				cli.StringSliceFlag{
					Name:   "meta-data",
					Value:  &cli.StringSlice{},
					Usage:  "Meta data for the agent (default is \"queue=default\")",
					EnvVar: "BUILDKITE_AGENT_META_DATA",
				},
				cli.BoolFlag{
					Name:  "meta-data-ec2-tags",
					Usage: "Populate the meta data from the current instances EC2 Tags",
				},
				cli.StringFlag{
					Name:   "bootstrap-script",
					Value:  "",
					Usage:  "Path to the bootstrap script",
					EnvVar: "BUILDKITE_BOOTSTRAP_SCRIPT_PATH",
				},
				cli.StringFlag{
					Name:   "build-path",
					Value:  "",
					Usage:  "Path to where the builds will run from",
					EnvVar: "BUILDKITE_BUILD_PATH",
				},
				cli.StringFlag{
					Name:   "hooks-path",
					Value:  "",
					Usage:  "Directory where the hook scripts are found",
					EnvVar: "BUILDKITE_HOOKS_PATH",
				},
				cli.BoolFlag{
					Name:   "no-pty",
					Usage:  "Do not run jobs within a pseudo terminal",
					EnvVar: "BUILDKITE_NO_PTY",
				},
				cli.BoolFlag{
					Name:   "no-automatic-ssh-fingerprint-verification",
					Usage:  "Don't automatically verify SSH fingerprints",
					EnvVar: "BUILDKITE_NO_AUTOMATIC_SSH_FINGERPRINT_VERIFICATION",
				},
				cli.BoolFlag{
					Name:   "no-command-eval",
					Usage:  "Don't allow this agent to run arbitrary console commands",
					EnvVar: "BUILDKITE_NO_COMMAND_EVAL",
				},
				cli.StringFlag{
					Name:   "endpoint",
					Value:  DefaultEndpoint,
					Usage:  "The Agent API endpoint",
					EnvVar: "BUILDKITE_AGENT_ENDPOINT",
				},
				cli.BoolFlag{
					Name:   "debug",
					Usage:  "Enable debug mode",
					EnvVar: "BUILDKITE_AGENT_DEBUG",
				},
				cli.BoolFlag{
					Name:   "no-color",
					Usage:  "Don't show colors in logging",
					EnvVar: "BUILDKITE_AGENT_NO_COLOR",
				},
			},
			Action: command.AgentStartCommandAction,
		},
		{
			Name:  "artifact",
			Usage: "Upload/download artifacts from Buildkite jobs",
			Subcommands: []cli.Command{
				{
					Name:        "download",
					Usage:       "Downloads artifacts from Buildkite to the local machine",
					Description: DownloadHelpDescription,
					Flags: []cli.Flag{
						cli.StringFlag{
							Name:  "step",
							Value: "",
							Usage: "Scope the search to a paticular step by using either it's name of job ID",
						},
						cli.StringFlag{
							Name:  "build",
							Value: "BUILDKITE_JOB_ID",
							Usage: "The build that the artifacts were uploaded to",
						},
						cli.StringFlag{
							Name:   "agent-access-token",
							Value:  "",
							Usage:  "The access token used to identify the agent",
							EnvVar: "BUILDKITE_AGENT_ACCESS_TOKEN",
						},
						cli.StringFlag{
							Name:   "endpoint",
							Value:  DefaultEndpoint,
							Usage:  "The Agent API endpoint",
							EnvVar: "BUILDKITE_AGENT_ENDPOINT",
						},
						cli.BoolFlag{
							Name:   "debug",
							Usage:  "Enable debug mode",
							EnvVar: "BUILDKITE_AGENT_DEBUG",
						},
						cli.BoolFlag{
							Name:   "no-color",
							Usage:  "Don't show colors in logging",
							EnvVar: "BUILDKITE_AGENT_NO_COLOR",
						},
					},
					Action: command.ArtifactDownloadCommandAction,
				},
				{
					Name:        "upload",
					Usage:       "Uploads files to a job as artifacts",
					Description: UploadHelpDescription,
					Flags: []cli.Flag{
						cli.StringFlag{
							Name:   "job",
							Value:  "",
							Usage:  "Which job should the artifacts be uploaded to",
							EnvVar: "BUILDKITE_JOB_ID",
						},
						cli.StringFlag{
							Name:   "agent-access-token",
							Value:  "",
							Usage:  "The access token used to identify the agent",
							EnvVar: "BUILDKITE_AGENT_ACCESS_TOKEN",
						},
						cli.StringFlag{
							Name:   "endpoint",
							Value:  DefaultEndpoint,
							Usage:  "The Agent API endpoint",
							EnvVar: "BUILDKITE_AGENT_ENDPOINT",
						},
						cli.BoolFlag{
							Name:   "debug",
							Usage:  "Enable debug mode",
							EnvVar: "BUILDKITE_AGENT_DEBUG",
						},
						cli.BoolFlag{
							Name:   "no-color",
							Usage:  "Don't show colors in logging",
							EnvVar: "BUILDKITE_AGENT_NO_COLOR",
						},
					},
					Action: command.ArtifactUploadCommandAction,
				},
				{
					Name:        "shasum",
					Usage:       "Prints the SHA-1 checksum for the artifact provided to STDOUT",
					Description: ShasumHelpDescription,
					Flags: []cli.Flag{
						cli.StringFlag{
							Name:  "step",
							Value: "",
							Usage: "Scope the search to a paticular step by using either it's name of job ID",
						},
						cli.StringFlag{
							Name:  "build",
							Value: "BUILDKITE_JOB_ID",
							Usage: "The build that the artifacts were uploaded to",
						},
						cli.StringFlag{
							Name:   "agent-access-token",
							Value:  "",
							Usage:  "The access token used to identify the agent",
							EnvVar: "BUILDKITE_AGENT_ACCESS_TOKEN",
						},
						cli.StringFlag{
							Name:   "endpoint",
							Value:  DefaultEndpoint,
							Usage:  "The Agent API endpoint",
							EnvVar: "BUILDKITE_AGENT_ENDPOINT",
						},
						cli.BoolFlag{
							Name:   "debug",
							Usage:  "Enable debug mode",
							EnvVar: "BUILDKITE_AGENT_DEBUG",
						},
						cli.BoolFlag{
							Name:   "no-color",
							Usage:  "Don't show colors in logging",
							EnvVar: "BUILDKITE_AGENT_NO_COLOR",
						},
					},
					Action: command.ArtifactShasumCommandAction,
				},
			},
		},
		{
			Name:  "meta-data",
			Usage: "Get/set data from Buildkite jobs",
			Subcommands: []cli.Command{
				{
					Name:        "set",
					Usage:       "Set data on a build",
					Description: SetHelpDescription,
					Flags: []cli.Flag{
						cli.StringFlag{
							Name:   "job",
							Value:  "",
							Usage:  "Which job should the meta-data be set on",
							EnvVar: "BUILDKITE_JOB_ID",
						},
						cli.StringFlag{
							Name:   "agent-access-token",
							Value:  "",
							Usage:  "The access token used to identify the agent",
							EnvVar: "BUILDKITE_AGENT_ACCESS_TOKEN",
						},
						cli.StringFlag{
							Name:   "endpoint",
							Value:  DefaultEndpoint,
							Usage:  "The Agent API endpoint",
							EnvVar: "BUILDKITE_AGENT_ENDPOINT",
						},
						cli.BoolFlag{
							Name:   "debug",
							Usage:  "Enable debug mode",
							EnvVar: "BUILDKITE_AGENT_DEBUG",
						},
						cli.BoolFlag{
							Name:   "no-color",
							Usage:  "Don't show colors in logging",
							EnvVar: "BUILDKITE_AGENT_NO_COLOR",
						},
					},
					Action: command.DataSetCommandAction,
				},
				{
					Name:        "get",
					Usage:       "Get data from a build",
					Description: GetHelpDescription,
					Flags: []cli.Flag{
						cli.StringFlag{
							Name:   "job",
							Value:  "",
							Usage:  "Which job should the meta-data be retrieved from",
							EnvVar: "BUILDKITE_JOB_ID",
						},
						cli.StringFlag{
							Name:   "agent-access-token",
							Value:  "",
							Usage:  "The access token used to identify the agent",
							EnvVar: "BUILDKITE_AGENT_ACCESS_TOKEN",
						},
						cli.StringFlag{
							Name:   "endpoint",
							Value:  DefaultEndpoint,
							Usage:  "The Agent API endpoint",
							EnvVar: "BUILDKITE_AGENT_ENDPOINT",
						},
						cli.BoolFlag{
							Name:   "debug",
							Usage:  "Enable debug mode",
							EnvVar: "BUILDKITE_AGENT_DEBUG",
						},
						cli.BoolFlag{
							Name:   "no-color",
							Usage:  "Don't show colors in logging",
							EnvVar: "BUILDKITE_AGENT_NO_COLOR",
						},
					},
					Action: command.DataGetCommandAction,
				},
			},
		},
	}
}
<commit_msg>Fie the build option in the artifact download command.<commit_after>package main

import (
	"github.com/buildkite/agent/command"
	"github.com/codegangsta/cli"
)

var Commands []cli.Command

var AgentDescription = `Usage:

   buildkite-agent start [arguments...]

Description:

   When a job is ready to run it will call the "bootstrap-script"
   and pass it all the environment variables required for the job to run.
   This script is responsible for checking out the code, and running the
   actual build script defined in the project.

   The agent will run any jobs within a PTY (pseudo terminal) if available.

Example:

   $ buildkite-agent start --token xxx`

var ShasumHelpDescription = `Usage:

   buildkite-agent artifact shasum [arguments...]

Description:

   Prints to STDOUT the SHA-1 for the artifact provided. If your search query
   for artifacts matches multiple agents, and error will be raised.

   Note: You need to ensure that your search query is surrounded by quotes if
   using a wild card as the built-in shell path globbing will provide files,
   which will break the download.

Example:

   $ buildkite-agent artifact shasum "pkg/release.tar.gz" --build xxx

   This will search for all the files in the build with the path "pkg/release.tar.gz" and will
   print to STDOUT it's SHA-1 checksum.

   If you would like to target artifacts from a specific build step, you can do
   so by using the --job argument.

   $ buildkite-agent artifact shasum "pkg/release.tar.gz" --job "release" --build xxx

   You can also use the job's id (provided by the environment variable $BUILDKITE_JOB_ID)`

var DownloadHelpDescription = `Usage:

   buildkite-agent artifact download [arguments...]

Description:

   Downloads artifacts from Buildkite to the local machine.

   Note: You need to ensure that your search query is surrounded by quotes if
   using a wild card as the built-in shell path globbing will provide files,
   which will break the download.

Example:

   $ buildkite-agent artifact download "pkg/*.tar.gz" . --build xxx

   This will search across all the artifacts for the build with files that match that part.
   The first argument is the search query, and the second argument is the download destination.

   If you're trying to download a specific file, and there are multiple artifacts from different
   jobs, you can target the particular job you want to download the artifact from:

   $ buildkite-agent artifact download "pkg/*.tar.gz" . --job "tests" --build xxx

   You can also use the job's id (provided by the environment variable $BUILDKITE_JOB_ID)`

var UploadHelpDescription = `Usage:

   buildkite-agent artifact upload <pattern> <destination> [arguments...]

Description:

   Uploads files to a job as artifacts.

   You need to ensure that the paths are surrounded by quotes otherwise the
   built-in shell path globbing will provide the files, which is currently not
   supported.

Example:

   $ buildkite-agent artifact upload "log/**/*.log"

   You can also upload directly to Amazon S3 if you'd like to host your own artifacts:

   $ export BUILDKITE_S3_ACCESS_KEY_ID=xxx
   $ export BUILDKITE_S3_SECRET_ACCESS_KEY=yyy
   $ export BUILDKITE_S3_DEFAULT_REGION=eu-central-1 # default is us-east-1
   $ export BUILDKITE_S3_ACL=private # default is public-read
   $ buildkite-agent artifact upload "log/**/*.log" s3://name-of-your-s3-bucket/$BUILDKITE_JOB_ID`

var SetHelpDescription = `Usage:

   buildkite-agent meta-data set <key> <value> [arguments...]

Description:

   Set arbitrary data on a build using a basic key/value store.

Example:

   $ buildkite-agent meta-data set "foo" "bar"`

var GetHelpDescription = `Usage:

   buildkite-agent meta-data get <key> [arguments...]

Description:

   Get data from a builds key/value store.

Example:

   $ buildkite-agent meta-data get "foo"`

var DefaultEndpoint = "https://agent.buildkite.com/v2"

func init() {
	Commands = []cli.Command{
		{
			Name:        "start",
			Usage:       "Starts a Buildkite agent",
			Description: AgentDescription,
			Flags: []cli.Flag{
				cli.StringFlag{
					Name:   "config",
					Value:  "",
					Usage:  "Path to a configration file",
					EnvVar: "BUILDKITE_AGENT_CONFIG",
				},
				cli.StringFlag{
					Name:   "token",
					Value:  "",
					Usage:  "Your account agent token",
					EnvVar: "BUILDKITE_AGENT_TOKEN",
				},
				cli.StringFlag{
					Name:   "name",
					Value:  "",
					Usage:  "The name of the agent",
					EnvVar: "BUILDKITE_AGENT_NAME",
				},
				cli.StringFlag{
					Name:   "priority",
					Value:  "",
					Usage:  "The priority of the agent (higher priorities are assigned work first)",
					EnvVar: "BUILDKITE_AGENT_PRIORITY",
				},
				cli.StringSliceFlag{
					Name:   "meta-data",
					Value:  &cli.StringSlice{},
					Usage:  "Meta data for the agent (default is \"queue=default\")",
					EnvVar: "BUILDKITE_AGENT_META_DATA",
				},
				cli.BoolFlag{
					Name:  "meta-data-ec2-tags",
					Usage: "Populate the meta data from the current instances EC2 Tags",
				},
				cli.StringFlag{
					Name:   "bootstrap-script",
					Value:  "",
					Usage:  "Path to the bootstrap script",
					EnvVar: "BUILDKITE_BOOTSTRAP_SCRIPT_PATH",
				},
				cli.StringFlag{
					Name:   "build-path",
					Value:  "",
					Usage:  "Path to where the builds will run from",
					EnvVar: "BUILDKITE_BUILD_PATH",
				},
				cli.StringFlag{
					Name:   "hooks-path",
					Value:  "",
					Usage:  "Directory where the hook scripts are found",
					EnvVar: "BUILDKITE_HOOKS_PATH",
				},
				cli.BoolFlag{
					Name:   "no-pty",
					Usage:  "Do not run jobs within a pseudo terminal",
					EnvVar: "BUILDKITE_NO_PTY",
				},
				cli.BoolFlag{
					Name:   "no-automatic-ssh-fingerprint-verification",
					Usage:  "Don't automatically verify SSH fingerprints",
					EnvVar: "BUILDKITE_NO_AUTOMATIC_SSH_FINGERPRINT_VERIFICATION",
				},
				cli.BoolFlag{
					Name:   "no-command-eval",
					Usage:  "Don't allow this agent to run arbitrary console commands",
					EnvVar: "BUILDKITE_NO_COMMAND_EVAL",
				},
				cli.StringFlag{
					Name:   "endpoint",
					Value:  DefaultEndpoint,
					Usage:  "The Agent API endpoint",
					EnvVar: "BUILDKITE_AGENT_ENDPOINT",
				},
				cli.BoolFlag{
					Name:   "debug",
					Usage:  "Enable debug mode",
					EnvVar: "BUILDKITE_AGENT_DEBUG",
				},
				cli.BoolFlag{
					Name:   "no-color",
					Usage:  "Don't show colors in logging",
					EnvVar: "BUILDKITE_AGENT_NO_COLOR",
				},
			},
			Action: command.AgentStartCommandAction,
		},
		{
			Name:  "artifact",
			Usage: "Upload/download artifacts from Buildkite jobs",
			Subcommands: []cli.Command{
				{
					Name:        "download",
					Usage:       "Downloads artifacts from Buildkite to the local machine",
					Description: DownloadHelpDescription,
					Flags: []cli.Flag{
						cli.StringFlag{
							Name:  "step",
							Value: "",
							Usage: "Scope the search to a paticular step by using either it's name of job ID",
						},
						cli.StringFlag{
							Name:   "build",
							Value:  "",
							EnvVar: "BUILDKITE_BUILD_ID",
							Usage:  "The build that the artifacts were uploaded to",
						},
						cli.StringFlag{
							Name:   "agent-access-token",
							Value:  "",
							Usage:  "The access token used to identify the agent",
							EnvVar: "BUILDKITE_AGENT_ACCESS_TOKEN",
						},
						cli.StringFlag{
							Name:   "endpoint",
							Value:  DefaultEndpoint,
							Usage:  "The Agent API endpoint",
							EnvVar: "BUILDKITE_AGENT_ENDPOINT",
						},
						cli.BoolFlag{
							Name:   "debug",
							Usage:  "Enable debug mode",
							EnvVar: "BUILDKITE_AGENT_DEBUG",
						},
						cli.BoolFlag{
							Name:   "no-color",
							Usage:  "Don't show colors in logging",
							EnvVar: "BUILDKITE_AGENT_NO_COLOR",
						},
					},
					Action: command.ArtifactDownloadCommandAction,
				},
				{
					Name:        "upload",
					Usage:       "Uploads files to a job as artifacts",
					Description: UploadHelpDescription,
					Flags: []cli.Flag{
						cli.StringFlag{
							Name:   "job",
							Value:  "",
							Usage:  "Which job should the artifacts be uploaded to",
							EnvVar: "BUILDKITE_JOB_ID",
						},
						cli.StringFlag{
							Name:   "agent-access-token",
							Value:  "",
							Usage:  "The access token used to identify the agent",
							EnvVar: "BUILDKITE_AGENT_ACCESS_TOKEN",
						},
						cli.StringFlag{
							Name:   "endpoint",
							Value:  DefaultEndpoint,
							Usage:  "The Agent API endpoint",
							EnvVar: "BUILDKITE_AGENT_ENDPOINT",
						},
						cli.BoolFlag{
							Name:   "debug",
							Usage:  "Enable debug mode",
							EnvVar: "BUILDKITE_AGENT_DEBUG",
						},
						cli.BoolFlag{
							Name:   "no-color",
							Usage:  "Don't show colors in logging",
							EnvVar: "BUILDKITE_AGENT_NO_COLOR",
						},
					},
					Action: command.ArtifactUploadCommandAction,
				},
				{
					Name:        "shasum",
					Usage:       "Prints the SHA-1 checksum for the artifact provided to STDOUT",
					Description: ShasumHelpDescription,
					Flags: []cli.Flag{
						cli.StringFlag{
							Name:  "step",
							Value: "",
							Usage: "Scope the search to a paticular step by using either it's name of job ID",
						},
						cli.StringFlag{
							Name:   "build",
							Value:  "",
							EnvVar: "BUILDKITE_BUILD_ID",
							Usage:  "The build that the artifacts were uploaded to",
						},
						cli.StringFlag{
							Name:   "agent-access-token",
							Value:  "",
							Usage:  "The access token used to identify the agent",
							EnvVar: "BUILDKITE_AGENT_ACCESS_TOKEN",
						},
						cli.StringFlag{
							Name:   "endpoint",
							Value:  DefaultEndpoint,
							Usage:  "The Agent API endpoint",
							EnvVar: "BUILDKITE_AGENT_ENDPOINT",
						},
						cli.BoolFlag{
							Name:   "debug",
							Usage:  "Enable debug mode",
							EnvVar: "BUILDKITE_AGENT_DEBUG",
						},
						cli.BoolFlag{
							Name:   "no-color",
							Usage:  "Don't show colors in logging",
							EnvVar: "BUILDKITE_AGENT_NO_COLOR",
						},
					},
					Action: command.ArtifactShasumCommandAction,
				},
			},
		},
		{
			Name:  "meta-data",
			Usage: "Get/set data from Buildkite jobs",
			Subcommands: []cli.Command{
				{
					Name:        "set",
					Usage:       "Set data on a build",
					Description: SetHelpDescription,
					Flags: []cli.Flag{
						cli.StringFlag{
							Name:   "job",
							Value:  "",
							Usage:  "Which job should the meta-data be set on",
							EnvVar: "BUILDKITE_JOB_ID",
						},
						cli.StringFlag{
							Name:   "agent-access-token",
							Value:  "",
							Usage:  "The access token used to identify the agent",
							EnvVar: "BUILDKITE_AGENT_ACCESS_TOKEN",
						},
						cli.StringFlag{
							Name:   "endpoint",
							Value:  DefaultEndpoint,
							Usage:  "The Agent API endpoint",
							EnvVar: "BUILDKITE_AGENT_ENDPOINT",
						},
						cli.BoolFlag{
							Name:   "debug",
							Usage:  "Enable debug mode",
							EnvVar: "BUILDKITE_AGENT_DEBUG",
						},
						cli.BoolFlag{
							Name:   "no-color",
							Usage:  "Don't show colors in logging",
							EnvVar: "BUILDKITE_AGENT_NO_COLOR",
						},
					},
					Action: command.DataSetCommandAction,
				},
				{
					Name:        "get",
					Usage:       "Get data from a build",
					Description: GetHelpDescription,
					Flags: []cli.Flag{
						cli.StringFlag{
							Name:   "job",
							Value:  "",
							Usage:  "Which job should the meta-data be retrieved from",
							EnvVar: "BUILDKITE_JOB_ID",
						},
						cli.StringFlag{
							Name:   "agent-access-token",
							Value:  "",
							Usage:  "The access token used to identify the agent",
							EnvVar: "BUILDKITE_AGENT_ACCESS_TOKEN",
						},
						cli.StringFlag{
							Name:   "endpoint",
							Value:  DefaultEndpoint,
							Usage:  "The Agent API endpoint",
							EnvVar: "BUILDKITE_AGENT_ENDPOINT",
						},
						cli.BoolFlag{
							Name:   "debug",
							Usage:  "Enable debug mode",
							EnvVar: "BUILDKITE_AGENT_DEBUG",
						},
						cli.BoolFlag{
							Name:   "no-color",
							Usage:  "Don't show colors in logging",
							EnvVar: "BUILDKITE_AGENT_NO_COLOR",
						},
					},
					Action: command.DataGetCommandAction,
				},
			},
		},
	}
}
<|endoftext|>


<commit_before>package astilectron

import (
	"fmt"
	"net"
	"os/exec"
	"runtime"
	"time"

	"github.com/asticode/go-astikit"
)

// Versions
const (
	DefaultAcceptTCPTimeout   = 30 * time.Second
	DefaultVersionAstilectron = "0.39.0"
	DefaultVersionElectron    = "7.1.10"
)

// Misc vars
var (
	validOSes = map[string]bool{
		"darwin":  true,
		"linux":   true,
		"windows": true,
	}
)

// App event names
const (
	EventNameAppClose         = "app.close"
	EventNameAppCmdQuit       = "app.cmd.quit" // Sends an event to Electron to properly quit the app
	EventNameAppCmdStop       = "app.cmd.stop" // Cancel the context which results in exiting abruptly Electron's app
	EventNameAppCrash         = "app.crash"
	EventNameAppErrorAccept   = "app.error.accept"
	EventNameAppEventReady    = "app.event.ready"
	EventNameAppNoAccept      = "app.no.accept"
	EventNameAppTooManyAccept = "app.too.many.accept"
)

// Astilectron represents an object capable of interacting with Astilectron
type Astilectron struct {
	dispatcher   *dispatcher
	displayPool  *displayPool
	dock         *Dock
	executer     Executer
	identifier   *identifier
	l            astikit.SeverityLogger
	listener     net.Listener
	options      Options
	paths        *Paths
	provisioner  Provisioner
	reader       *reader
	stderrWriter *astikit.WriterAdapter
	stdoutWriter *astikit.WriterAdapter
	supported    *Supported
	worker       *astikit.Worker
	writer       *writer
}

// Options represents Astilectron options
type Options struct {
	AcceptTCPTimeout   time.Duration
	AppName            string
	AppIconDarwinPath  string // Darwin systems requires a specific .icns file
	AppIconDefaultPath string
	BaseDirectoryPath  string
	DataDirectoryPath  string
	ElectronSwitches   []string
	SingleInstance     bool
	SkipSetup          bool // If true, the user must handle provisioning and executing astilectron.
	TCPPort            *int // The port to listen on.
	VersionAstilectron string
	VersionElectron    string
}

// Supported represents Astilectron supported features
type Supported struct {
	Notification *bool `json:"notification"`
}

// New creates a new Astilectron instance
func New(l astikit.StdLogger, o Options) (a *Astilectron, err error) {
	// Validate the OS
	if !IsValidOS(runtime.GOOS) {
		err = fmt.Errorf("OS %s is invalid", runtime.GOOS)
		return
	}

	if o.VersionAstilectron == "" {
		o.VersionAstilectron = DefaultVersionAstilectron
	}
	if o.VersionElectron == "" {
		o.VersionElectron = DefaultVersionElectron
	}

	// Init
	a = &Astilectron{
		dispatcher:  newDispatcher(),
		displayPool: newDisplayPool(),
		executer:    DefaultExecuter,
		identifier:  newIdentifier(),
		l:           astikit.AdaptStdLogger(l),
		options:     o,
		provisioner: newDefaultProvisioner(l),
		worker:      astikit.NewWorker(astikit.WorkerOptions{Logger: l}),
	}

	// Set paths
	if a.paths, err = newPaths(runtime.GOOS, runtime.GOARCH, o); err != nil {
		err = fmt.Errorf("creating new paths failed: %w", err)
		return
	}

	// Add default listeners
	a.On(EventNameAppCmdStop, func(e Event) (deleteListener bool) {
		a.Stop()
		return
	})
	a.On(EventNameDisplayEventAdded, func(e Event) (deleteListener bool) {
		a.displayPool.update(e.Displays)
		return
	})
	a.On(EventNameDisplayEventMetricsChanged, func(e Event) (deleteListener bool) {
		a.displayPool.update(e.Displays)
		return
	})
	a.On(EventNameDisplayEventRemoved, func(e Event) (deleteListener bool) {
		a.displayPool.update(e.Displays)
		return
	})
	a.On(EventNameAppCmdQuit, func(e Event) (deleteListener bool) {
		a.Stop()
		return
	})
	return
}

// IsValidOS validates the OS
func IsValidOS(os string) (ok bool) {
	_, ok = validOSes[os]
	return
}

// SetProvisioner sets the provisioner
func (a *Astilectron) SetProvisioner(p Provisioner) *Astilectron {
	a.provisioner = p
	return a
}

// SetExecuter sets the executer
func (a *Astilectron) SetExecuter(e Executer) *Astilectron {
	a.executer = e
	return a
}

// On implements the Listenable interface
func (a *Astilectron) On(eventName string, l Listener) {
	a.dispatcher.addListener(targetIDApp, eventName, l)
}

// Start starts Astilectron
func (a *Astilectron) Start() (err error) {
	// Log
	a.l.Debug("Starting...")

	// Provision
	if !a.options.SkipSetup {
		if err = a.provision(); err != nil {
			return fmt.Errorf("provisioning failed: %w", err)
		}
	}

	// Unfortunately communicating with Electron through stdin/stdout doesn't work on Windows so all communications
	// will be done through TCP
	if err = a.listenTCP(); err != nil {
		return fmt.Errorf("listening failed: %w", err)
	}

	// Execute
	if !a.options.SkipSetup {
		if err = a.execute(); err != nil {
			return fmt.Errorf("executing failed: %w", err)
		}
	} else {
		synchronousFunc(a.worker.Context(), a, nil, "app.event.ready")
	}
	return nil
}

// provision provisions Astilectron
func (a *Astilectron) provision() error {
	a.l.Debug("Provisioning...")
	return a.provisioner.Provision(a.worker.Context(), a.options.AppName, runtime.GOOS, runtime.GOARCH, a.options.VersionAstilectron, a.options.VersionElectron, *a.paths)
}

// listenTCP creates a TCP server for astilectron to connect to
// and listens to the first TCP connection coming its way (this should be Astilectron).
func (a *Astilectron) listenTCP() (err error) {
	// Log
	a.l.Debug("Listening...")

	addr := "127.0.0.1:"
	if a.options.TCPPort != nil {
		addr += fmt.Sprint(*a.options.TCPPort)
	}
	// Listen
	if a.listener, err = net.Listen("tcp", addr); err != nil {
		return fmt.Errorf("tcp net.Listen failed: %w", err)
	}

	// Check a connection has been accepted quickly enough
	var chanAccepted = make(chan bool)
	go a.watchNoAccept(a.options.AcceptTCPTimeout, chanAccepted)

	// Accept connections
	go a.acceptTCP(chanAccepted)
	return
}

// watchNoAccept checks whether a TCP connection is accepted quickly enough
func (a *Astilectron) watchNoAccept(timeout time.Duration, chanAccepted chan bool) {
	//check timeout
	if timeout == 0 {
		timeout = DefaultAcceptTCPTimeout
	}
	var t = time.NewTimer(timeout)
	defer t.Stop()
	for {
		select {
		case <-chanAccepted:
			return
		case <-t.C:
			a.l.Errorf("No TCP connection has been accepted in the past %s", timeout)
			a.dispatcher.dispatch(Event{Name: EventNameAppNoAccept, TargetID: targetIDApp})
			a.dispatcher.dispatch(Event{Name: EventNameAppCmdStop, TargetID: targetIDApp})
			return
		}
	}
}

// watchAcceptTCP accepts TCP connections
func (a *Astilectron) acceptTCP(chanAccepted chan bool) {
	for i := 0; i <= 1; i++ {
		// Accept
		var conn net.Conn
		var err error
		if conn, err = a.listener.Accept(); err != nil {
			a.l.Errorf("%s while TCP accepting", err)
			a.dispatcher.dispatch(Event{Name: EventNameAppErrorAccept, TargetID: targetIDApp})
			a.dispatcher.dispatch(Event{Name: EventNameAppCmdStop, TargetID: targetIDApp})
			return
		}

		// We only accept the first connection which should be Astilectron, close the next one and stop
		// the app
		if i > 0 {
			a.l.Errorf("Too many TCP connections")
			a.dispatcher.dispatch(Event{Name: EventNameAppTooManyAccept, TargetID: targetIDApp})
			a.dispatcher.dispatch(Event{Name: EventNameAppCmdStop, TargetID: targetIDApp})
			conn.Close()
			return
		}

		// Let the timer know a connection has been accepted
		chanAccepted <- true

		// Create reader and writer
		a.writer = newWriter(conn, a.l)
		a.reader = newReader(a.worker.Context(), a.l, a.dispatcher, conn)
		go a.reader.read()
	}
}

// execute executes Astilectron in Electron
func (a *Astilectron) execute() (err error) {
	// Log
	a.l.Debug("Executing...")

	// Create command
	var singleInstance string
	if a.options.SingleInstance {
		singleInstance = "true"
	} else {
		singleInstance = "false"
	}
	var cmd = exec.CommandContext(a.worker.Context(), a.paths.AppExecutable(), append([]string{a.paths.AstilectronApplication(), a.listener.Addr().String(), singleInstance}, a.options.ElectronSwitches...)...)
	a.stderrWriter = astikit.NewWriterAdapter(astikit.WriterAdapterOptions{
		Callback: func(i []byte) { a.l.Debugf("Stderr says: %s", i) },
		Split:    []byte("\n"),
	})
	a.stdoutWriter = astikit.NewWriterAdapter(astikit.WriterAdapterOptions{
		Callback: func(i []byte) { a.l.Debugf("Stdout says: %s", i) },
		Split:    []byte("\n"),
	})
	cmd.Stderr = a.stderrWriter
	cmd.Stdout = a.stdoutWriter

	// Execute command
	if err = a.executeCmd(cmd); err != nil {
		return fmt.Errorf("executing cmd failed: %w", err)
	}
	return
}

// executeCmd executes the command
func (a *Astilectron) executeCmd(cmd *exec.Cmd) (err error) {
	// Execute
	var e Event
	if e, err = synchronousFunc(a.worker.Context(), a, func() error { return a.executer(a.l, a, cmd) }, EventNameAppEventReady); err != nil {
		err = fmt.Errorf("executer failed: %w", err)
		return
	}

	// Update display pool
	if e.Displays != nil {
		a.displayPool.update(e.Displays)
	}

	// Create dock
	a.dock = newDock(a.worker.Context(), a.dispatcher, a.identifier, a.writer)

	// Update supported features
	a.supported = e.Supported
	return
}

// watchCmd watches the cmd execution
func (a *Astilectron) watchCmd(cmd *exec.Cmd) {
	a.worker.NewTask().Do(func() {
		// Wait
		if err := cmd.Wait(); err != nil {
			a.l.Errorf("'%v' exited with code: %v", cmd.Path, cmd.ProcessState.ExitCode())
		}

		// Check the context to determine whether it was a crash
		if a.worker.Context().Err() == nil {
			a.l.Debug("App has crashed")
			a.dispatcher.dispatch(Event{Name: EventNameAppCrash, TargetID: targetIDApp})
		} else {
			a.l.Debug("App has closed")
			a.dispatcher.dispatch(Event{Name: EventNameAppClose, TargetID: targetIDApp})
		}
		a.dispatcher.dispatch(Event{Name: EventNameAppCmdStop, TargetID: targetIDApp})
	})
}

// Close closes Astilectron properly
func (a *Astilectron) Close() {
	a.l.Debug("Closing...")
	a.worker.Stop()
	if a.listener != nil {
		a.listener.Close()
	}
	if a.reader != nil {
		a.reader.close()
	}
	if a.stderrWriter != nil {
		a.stderrWriter.Close()
	}
	if a.stdoutWriter != nil {
		a.stdoutWriter.Close()
	}
	if a.writer != nil {
		a.writer.close()
	}
}

// HandleSignals handles signals
func (a *Astilectron) HandleSignals(hs ...astikit.SignalHandler) {
	a.worker.HandleSignals(hs...)
}

// Stop orders Astilectron to stop
func (a *Astilectron) Stop() {
	a.l.Debug("Stopping...")
	a.worker.Stop()
}

// Wait is a blocking pattern
func (a *Astilectron) Wait() {
	a.worker.Wait()
}

// Quit quits the app
func (a *Astilectron) Quit() error {
	return a.writer.write(Event{Name: EventNameAppCmdQuit})
}

// Paths returns the paths
func (a *Astilectron) Paths() Paths {
	return *a.paths
}

// Displays returns the displays
func (a *Astilectron) Displays() []*Display {
	return a.displayPool.all()
}

// Dock returns the dock
func (a *Astilectron) Dock() *Dock {
	return a.dock
}

// PrimaryDisplay returns the primary display
func (a *Astilectron) PrimaryDisplay() *Display {
	return a.displayPool.primary()
}

// NewMenu creates a new app menu
func (a *Astilectron) NewMenu(i []*MenuItemOptions) *Menu {
	return newMenu(a.worker.Context(), targetIDApp, i, a.dispatcher, a.identifier, a.writer)
}

// NewWindow creates a new window
func (a *Astilectron) NewWindow(url string, o *WindowOptions) (*Window, error) {
	return newWindow(a.worker.Context(), a.l, a.options, a.Paths(), url, o, a.dispatcher, a.identifier, a.writer)
}

// NewWindowInDisplay creates a new window in a specific display
// This overrides the center attribute
func (a *Astilectron) NewWindowInDisplay(d *Display, url string, o *WindowOptions) (*Window, error) {
	if o.X != nil {
		*o.X += d.Bounds().X
	} else {
		o.X = astikit.IntPtr(d.Bounds().X)
	}
	if o.Y != nil {
		*o.Y += d.Bounds().Y
	} else {
		o.Y = astikit.IntPtr(d.Bounds().Y)
	}
	return newWindow(a.worker.Context(), a.l, a.options, a.Paths(), url, o, a.dispatcher, a.identifier, a.writer)
}

// NewTray creates a new tray
func (a *Astilectron) NewTray(o *TrayOptions) *Tray {
	return newTray(a.worker.Context(), o, a.dispatcher, a.identifier, a.writer)
}

// NewNotification creates a new notification
func (a *Astilectron) NewNotification(o *NotificationOptions) *Notification {
	return newNotification(a.worker.Context(), o, a.supported != nil && a.supported.Notification != nil && *a.supported.Notification, a.dispatcher, a.identifier, a.writer)
}
<commit_msg>Bumped astilectron<commit_after>package astilectron

import (
	"fmt"
	"net"
	"os/exec"
	"runtime"
	"time"

	"github.com/asticode/go-astikit"
)

// Versions
const (
	DefaultAcceptTCPTimeout   = 30 * time.Second
	DefaultVersionAstilectron = "0.40.0"
	DefaultVersionElectron    = "7.1.10"
)

// Misc vars
var (
	validOSes = map[string]bool{
		"darwin":  true,
		"linux":   true,
		"windows": true,
	}
)

// App event names
const (
	EventNameAppClose         = "app.close"
	EventNameAppCmdQuit       = "app.cmd.quit" // Sends an event to Electron to properly quit the app
	EventNameAppCmdStop       = "app.cmd.stop" // Cancel the context which results in exiting abruptly Electron's app
	EventNameAppCrash         = "app.crash"
	EventNameAppErrorAccept   = "app.error.accept"
	EventNameAppEventReady    = "app.event.ready"
	EventNameAppNoAccept      = "app.no.accept"
	EventNameAppTooManyAccept = "app.too.many.accept"
)

// Astilectron represents an object capable of interacting with Astilectron
type Astilectron struct {
	dispatcher   *dispatcher
	displayPool  *displayPool
	dock         *Dock
	executer     Executer
	identifier   *identifier
	l            astikit.SeverityLogger
	listener     net.Listener
	options      Options
	paths        *Paths
	provisioner  Provisioner
	reader       *reader
	stderrWriter *astikit.WriterAdapter
	stdoutWriter *astikit.WriterAdapter
	supported    *Supported
	worker       *astikit.Worker
	writer       *writer
}

// Options represents Astilectron options
type Options struct {
	AcceptTCPTimeout   time.Duration
	AppName            string
	AppIconDarwinPath  string // Darwin systems requires a specific .icns file
	AppIconDefaultPath string
	BaseDirectoryPath  string
	DataDirectoryPath  string
	ElectronSwitches   []string
	SingleInstance     bool
	SkipSetup          bool // If true, the user must handle provisioning and executing astilectron.
	TCPPort            *int // The port to listen on.
	VersionAstilectron string
	VersionElectron    string
}

// Supported represents Astilectron supported features
type Supported struct {
	Notification *bool `json:"notification"`
}

// New creates a new Astilectron instance
func New(l astikit.StdLogger, o Options) (a *Astilectron, err error) {
	// Validate the OS
	if !IsValidOS(runtime.GOOS) {
		err = fmt.Errorf("OS %s is invalid", runtime.GOOS)
		return
	}

	if o.VersionAstilectron == "" {
		o.VersionAstilectron = DefaultVersionAstilectron
	}
	if o.VersionElectron == "" {
		o.VersionElectron = DefaultVersionElectron
	}

	// Init
	a = &Astilectron{
		dispatcher:  newDispatcher(),
		displayPool: newDisplayPool(),
		executer:    DefaultExecuter,
		identifier:  newIdentifier(),
		l:           astikit.AdaptStdLogger(l),
		options:     o,
		provisioner: newDefaultProvisioner(l),
		worker:      astikit.NewWorker(astikit.WorkerOptions{Logger: l}),
	}

	// Set paths
	if a.paths, err = newPaths(runtime.GOOS, runtime.GOARCH, o); err != nil {
		err = fmt.Errorf("creating new paths failed: %w", err)
		return
	}

	// Add default listeners
	a.On(EventNameAppCmdStop, func(e Event) (deleteListener bool) {
		a.Stop()
		return
	})
	a.On(EventNameDisplayEventAdded, func(e Event) (deleteListener bool) {
		a.displayPool.update(e.Displays)
		return
	})
	a.On(EventNameDisplayEventMetricsChanged, func(e Event) (deleteListener bool) {
		a.displayPool.update(e.Displays)
		return
	})
	a.On(EventNameDisplayEventRemoved, func(e Event) (deleteListener bool) {
		a.displayPool.update(e.Displays)
		return
	})
	a.On(EventNameAppCmdQuit, func(e Event) (deleteListener bool) {
		a.Stop()
		return
	})
	return
}

// IsValidOS validates the OS
func IsValidOS(os string) (ok bool) {
	_, ok = validOSes[os]
	return
}

// SetProvisioner sets the provisioner
func (a *Astilectron) SetProvisioner(p Provisioner) *Astilectron {
	a.provisioner = p
	return a
}

// SetExecuter sets the executer
func (a *Astilectron) SetExecuter(e Executer) *Astilectron {
	a.executer = e
	return a
}

// On implements the Listenable interface
func (a *Astilectron) On(eventName string, l Listener) {
	a.dispatcher.addListener(targetIDApp, eventName, l)
}

// Start starts Astilectron
func (a *Astilectron) Start() (err error) {
	// Log
	a.l.Debug("Starting...")

	// Provision
	if !a.options.SkipSetup {
		if err = a.provision(); err != nil {
			return fmt.Errorf("provisioning failed: %w", err)
		}
	}

	// Unfortunately communicating with Electron through stdin/stdout doesn't work on Windows so all communications
	// will be done through TCP
	if err = a.listenTCP(); err != nil {
		return fmt.Errorf("listening failed: %w", err)
	}

	// Execute
	if !a.options.SkipSetup {
		if err = a.execute(); err != nil {
			return fmt.Errorf("executing failed: %w", err)
		}
	} else {
		synchronousFunc(a.worker.Context(), a, nil, "app.event.ready")
	}
	return nil
}

// provision provisions Astilectron
func (a *Astilectron) provision() error {
	a.l.Debug("Provisioning...")
	return a.provisioner.Provision(a.worker.Context(), a.options.AppName, runtime.GOOS, runtime.GOARCH, a.options.VersionAstilectron, a.options.VersionElectron, *a.paths)
}

// listenTCP creates a TCP server for astilectron to connect to
// and listens to the first TCP connection coming its way (this should be Astilectron).
func (a *Astilectron) listenTCP() (err error) {
	// Log
	a.l.Debug("Listening...")

	addr := "127.0.0.1:"
	if a.options.TCPPort != nil {
		addr += fmt.Sprint(*a.options.TCPPort)
	}
	// Listen
	if a.listener, err = net.Listen("tcp", addr); err != nil {
		return fmt.Errorf("tcp net.Listen failed: %w", err)
	}

	// Check a connection has been accepted quickly enough
	var chanAccepted = make(chan bool)
	go a.watchNoAccept(a.options.AcceptTCPTimeout, chanAccepted)

	// Accept connections
	go a.acceptTCP(chanAccepted)
	return
}

// watchNoAccept checks whether a TCP connection is accepted quickly enough
func (a *Astilectron) watchNoAccept(timeout time.Duration, chanAccepted chan bool) {
	//check timeout
	if timeout == 0 {
		timeout = DefaultAcceptTCPTimeout
	}
	var t = time.NewTimer(timeout)
	defer t.Stop()
	for {
		select {
		case <-chanAccepted:
			return
		case <-t.C:
			a.l.Errorf("No TCP connection has been accepted in the past %s", timeout)
			a.dispatcher.dispatch(Event{Name: EventNameAppNoAccept, TargetID: targetIDApp})
			a.dispatcher.dispatch(Event{Name: EventNameAppCmdStop, TargetID: targetIDApp})
			return
		}
	}
}

// watchAcceptTCP accepts TCP connections
func (a *Astilectron) acceptTCP(chanAccepted chan bool) {
	for i := 0; i <= 1; i++ {
		// Accept
		var conn net.Conn
		var err error
		if conn, err = a.listener.Accept(); err != nil {
			a.l.Errorf("%s while TCP accepting", err)
			a.dispatcher.dispatch(Event{Name: EventNameAppErrorAccept, TargetID: targetIDApp})
			a.dispatcher.dispatch(Event{Name: EventNameAppCmdStop, TargetID: targetIDApp})
			return
		}

		// We only accept the first connection which should be Astilectron, close the next one and stop
		// the app
		if i > 0 {
			a.l.Errorf("Too many TCP connections")
			a.dispatcher.dispatch(Event{Name: EventNameAppTooManyAccept, TargetID: targetIDApp})
			a.dispatcher.dispatch(Event{Name: EventNameAppCmdStop, TargetID: targetIDApp})
			conn.Close()
			return
		}

		// Let the timer know a connection has been accepted
		chanAccepted <- true

		// Create reader and writer
		a.writer = newWriter(conn, a.l)
		a.reader = newReader(a.worker.Context(), a.l, a.dispatcher, conn)
		go a.reader.read()
	}
}

// execute executes Astilectron in Electron
func (a *Astilectron) execute() (err error) {
	// Log
	a.l.Debug("Executing...")

	// Create command
	var singleInstance string
	if a.options.SingleInstance {
		singleInstance = "true"
	} else {
		singleInstance = "false"
	}
	var cmd = exec.CommandContext(a.worker.Context(), a.paths.AppExecutable(), append([]string{a.paths.AstilectronApplication(), a.listener.Addr().String(), singleInstance}, a.options.ElectronSwitches...)...)
	a.stderrWriter = astikit.NewWriterAdapter(astikit.WriterAdapterOptions{
		Callback: func(i []byte) { a.l.Debugf("Stderr says: %s", i) },
		Split:    []byte("\n"),
	})
	a.stdoutWriter = astikit.NewWriterAdapter(astikit.WriterAdapterOptions{
		Callback: func(i []byte) { a.l.Debugf("Stdout says: %s", i) },
		Split:    []byte("\n"),
	})
	cmd.Stderr = a.stderrWriter
	cmd.Stdout = a.stdoutWriter

	// Execute command
	if err = a.executeCmd(cmd); err != nil {
		return fmt.Errorf("executing cmd failed: %w", err)
	}
	return
}

// executeCmd executes the command
func (a *Astilectron) executeCmd(cmd *exec.Cmd) (err error) {
	// Execute
	var e Event
	if e, err = synchronousFunc(a.worker.Context(), a, func() error { return a.executer(a.l, a, cmd) }, EventNameAppEventReady); err != nil {
		err = fmt.Errorf("executer failed: %w", err)
		return
	}

	// Update display pool
	if e.Displays != nil {
		a.displayPool.update(e.Displays)
	}

	// Create dock
	a.dock = newDock(a.worker.Context(), a.dispatcher, a.identifier, a.writer)

	// Update supported features
	a.supported = e.Supported
	return
}

// watchCmd watches the cmd execution
func (a *Astilectron) watchCmd(cmd *exec.Cmd) {
	a.worker.NewTask().Do(func() {
		// Wait
		if err := cmd.Wait(); err != nil {
			a.l.Errorf("'%v' exited with code: %v", cmd.Path, cmd.ProcessState.ExitCode())
		}

		// Check the context to determine whether it was a crash
		if a.worker.Context().Err() == nil {
			a.l.Debug("App has crashed")
			a.dispatcher.dispatch(Event{Name: EventNameAppCrash, TargetID: targetIDApp})
		} else {
			a.l.Debug("App has closed")
			a.dispatcher.dispatch(Event{Name: EventNameAppClose, TargetID: targetIDApp})
		}
		a.dispatcher.dispatch(Event{Name: EventNameAppCmdStop, TargetID: targetIDApp})
	})
}

// Close closes Astilectron properly
func (a *Astilectron) Close() {
	a.l.Debug("Closing...")
	a.worker.Stop()
	if a.listener != nil {
		a.listener.Close()
	}
	if a.reader != nil {
		a.reader.close()
	}
	if a.stderrWriter != nil {
		a.stderrWriter.Close()
	}
	if a.stdoutWriter != nil {
		a.stdoutWriter.Close()
	}
	if a.writer != nil {
		a.writer.close()
	}
}

// HandleSignals handles signals
func (a *Astilectron) HandleSignals(hs ...astikit.SignalHandler) {
	a.worker.HandleSignals(hs...)
}

// Stop orders Astilectron to stop
func (a *Astilectron) Stop() {
	a.l.Debug("Stopping...")
	a.worker.Stop()
}

// Wait is a blocking pattern
func (a *Astilectron) Wait() {
	a.worker.Wait()
}

// Quit quits the app
func (a *Astilectron) Quit() error {
	return a.writer.write(Event{Name: EventNameAppCmdQuit})
}

// Paths returns the paths
func (a *Astilectron) Paths() Paths {
	return *a.paths
}

// Displays returns the displays
func (a *Astilectron) Displays() []*Display {
	return a.displayPool.all()
}

// Dock returns the dock
func (a *Astilectron) Dock() *Dock {
	return a.dock
}

// PrimaryDisplay returns the primary display
func (a *Astilectron) PrimaryDisplay() *Display {
	return a.displayPool.primary()
}

// NewMenu creates a new app menu
func (a *Astilectron) NewMenu(i []*MenuItemOptions) *Menu {
	return newMenu(a.worker.Context(), targetIDApp, i, a.dispatcher, a.identifier, a.writer)
}

// NewWindow creates a new window
func (a *Astilectron) NewWindow(url string, o *WindowOptions) (*Window, error) {
	return newWindow(a.worker.Context(), a.l, a.options, a.Paths(), url, o, a.dispatcher, a.identifier, a.writer)
}

// NewWindowInDisplay creates a new window in a specific display
// This overrides the center attribute
func (a *Astilectron) NewWindowInDisplay(d *Display, url string, o *WindowOptions) (*Window, error) {
	if o.X != nil {
		*o.X += d.Bounds().X
	} else {
		o.X = astikit.IntPtr(d.Bounds().X)
	}
	if o.Y != nil {
		*o.Y += d.Bounds().Y
	} else {
		o.Y = astikit.IntPtr(d.Bounds().Y)
	}
	return newWindow(a.worker.Context(), a.l, a.options, a.Paths(), url, o, a.dispatcher, a.identifier, a.writer)
}

// NewTray creates a new tray
func (a *Astilectron) NewTray(o *TrayOptions) *Tray {
	return newTray(a.worker.Context(), o, a.dispatcher, a.identifier, a.writer)
}

// NewNotification creates a new notification
func (a *Astilectron) NewNotification(o *NotificationOptions) *Notification {
	return newNotification(a.worker.Context(), o, a.supported != nil && a.supported.Notification != nil && *a.supported.Notification, a.dispatcher, a.identifier, a.writer)
}
<|endoftext|>


<commit_before>package main

import (
	"testing"
	"encoding/json"
	"fmt"
	"os"
)

var theEvent Event

func TestChannelInference(t *testing.T) {

	tables := []struct {
        json string
        channel string
    }{
        {"{\"channel\":\"algo\"}", "algo"},
        {"{\"otherfields\":\"somevals\"}", "undefined"},
    }

    for _, table := range tables {
		var rawEvent map[string]interface{} = nil
		thejson := []byte(table.json)
		json.Unmarshal(thejson, &rawEvent)
		theEvent = getEvent(rawEvent)
		inferredchan := string(theEvent.Channel)
		if inferredchan != table.channel {
			t.Errorf("Wrong inferred channel, got: %s, want: %s.", inferredchan, table.channel)
		}
	}
}

func TestSumAggSignal(t *testing.T) {
	aggSignal := AggregatedSignal{[]Signal{}, aggregatorsMap["avg"]}
	sample := aggSignal.Sample()
	if (sample != UNDEFINED) {
			t.Errorf("Wrong aggregated value, got: %s, wanted nil.", (*sample).(int))
	}
}

var five interface{} = 5
var sampledSignal SampledSignal = SampledSignal{ &five }

func TestSampleSampled(t *testing.T) {
	sample := sampledSignal.Sample()
	if ((*sample).(int) != 5) {
		t.Errorf("Wrong sampledsignal sample value, got: %v, want: %v.", sample, 5)
	}
}

var aggSignal AggregatedSignal = AggregatedSignal{ nil, func(vals []interface{}) *interface{} {return nil} }

func TestAddSource(t *testing.T) {
	aggSignal.AddSource(sampledSignal)
	aggSignal.Sample()
}

var baseSession BaseSessionSignal = BaseSessionSignal{true}
var condSignal ConditionalSignal = ConditionalSignal{aggSignal, baseSession}
var signalNameAndPars SignalNameAndPars = SignalNameAndPars{"signal", map[Param]string{}}
var otherSignalNameAndPars SignalNameAndPars = SignalNameAndPars{"othersignal", map[Param]string{}}

func TestCreate(t *testing.T) {
	theGlobalAggregatedSignalDefs = map[SignalName]AggregatedSignalDefinition {
	"signal" : AggregatedSignalDefinition {
				"signal",
				[]Param{},
				"avg",
				[]Param{"x"},
				"cpuload",
				[]Param{"x"},
			},
	}
	createBaseSession(signalNameAndPars)
	createSampledSignal(signalNameAndPars)
	createConditionalSignal(signalNameAndPars, baseSession, aggSignal)
	createAggregatedSignal(signalNameAndPars)
}

func TestRest(t *testing.T) {
	condSignal.Sample()
	checkWriteDefs("ts")
	checkSamples(theEvent)
	extractParamsMap(theEvent, map[Param]JSONPath{})
	extractFromMap(map[string]interface{} {"a":5,}, "a")
	//readAndRegister(map[string]interface{}{})
	baseSession.getState()
	getSessionSignals("sessionName", map[Param]string{})
	registerBaseSessionSignal(signalNameAndPars, &baseSession)
	getBaseSession(signalNameAndPars)
	updateBaseSession(signalNameAndPars, false)
	reportSessionSignalCreation(otherSignalNameAndPars, baseSession)
fmt.Println("")
	signalNameAndPars.equals(signalNameAndPars)
	getSignals("signal", map[Param]string{})
	registerSampledSignal(signalNameAndPars, &sampledSignal)
	getSampledSignal(signalNameAndPars)
	registerAggregatedSignal(signalNameAndPars, &aggSignal)
	getAggregatedSignal(signalNameAndPars)
	registerConditionalSignal(signalNameAndPars, &condSignal)
	reportSample(signalNameAndPars, 8)

	theGlobalWriteDefs = []SignalWriteDefinition {
		SignalWriteDefinition{
			"signal",
			"out",
			map[JSONPath]WriteValue{
			},
			SNameAndRebound{
				"signal",
				map[Param]Param{
				},
			},
		},
	}
	registerWriteDefs(signalNameAndPars, aggSignal)
	getWriters(signalNameAndPars)[0]("ts")
	//main()
	file, err := os.Open("testinputs/testdefs.json")
    if err != nil {
        panic(err)
    }
	scanAPIPipe(file)
	file, err = os.Open("testinputs/testEvents.txt")
    if err != nil {
        panic(err)
    }
	scanStdIn(file)
}

var ssdef SampledSignalDefinition = SampledSignalDefinition {
            "cpuload",
            map[Param]JSONPath {
                "x": "beat.hostname",
            },
            "in",
            "system.load.1",
        }

var bsdef BaseSessionDefinition = BaseSessionDefinition {
        "timeIsEven",
        []EventDefinition {
            EventDefinition {
                "in_condition_true",
                nil,
            },
        },
        []EventDefinition {
            EventDefinition {
                "in_condition_false",
                nil,
            },
        },
    }

func TestGetParams(t *testing.T) {
	ssdef.getParams()
	bsdef.getParams()
}

/*
*/
<commit_msg>Changed %s for %v, required by new version of go (#101)<commit_after>package main

import (
	"testing"
	"encoding/json"
	"fmt"
	"os"
)

var theEvent Event

func TestChannelInference(t *testing.T) {

	tables := []struct {
        json string
        channel string
    }{
        {"{\"channel\":\"algo\"}", "algo"},
        {"{\"otherfields\":\"somevals\"}", "undefined"},
    }

    for _, table := range tables {
		var rawEvent map[string]interface{} = nil
		thejson := []byte(table.json)
		json.Unmarshal(thejson, &rawEvent)
		theEvent = getEvent(rawEvent)
		inferredchan := string(theEvent.Channel)
		if inferredchan != table.channel {
			t.Errorf("Wrong inferred channel, got: %s, want: %s.", inferredchan, table.channel)
		}
	}
}

func TestSumAggSignal(t *testing.T) {
	aggSignal := AggregatedSignal{[]Signal{}, aggregatorsMap["avg"]}
	sample := aggSignal.Sample()
	if (sample != UNDEFINED) {
			t.Errorf("Wrong aggregated value, got: %v, wanted nil.", sample)
	}
}

var five interface{} = 5
var sampledSignal SampledSignal = SampledSignal{ &five }

func TestSampleSampled(t *testing.T) {
	sample := sampledSignal.Sample()
	if ((*sample).(int) != 5) {
		t.Errorf("Wrong sampledsignal sample value, got: %v, want: %v.", sample, 5)
	}
}

var aggSignal AggregatedSignal = AggregatedSignal{ nil, func(vals []interface{}) *interface{} {return nil} }

func TestAddSource(t *testing.T) {
	aggSignal.AddSource(sampledSignal)
	aggSignal.Sample()
}

var baseSession BaseSessionSignal = BaseSessionSignal{true}
var condSignal ConditionalSignal = ConditionalSignal{aggSignal, baseSession}
var signalNameAndPars SignalNameAndPars = SignalNameAndPars{"signal", map[Param]string{}}
var otherSignalNameAndPars SignalNameAndPars = SignalNameAndPars{"othersignal", map[Param]string{}}

func TestCreate(t *testing.T) {
	theGlobalAggregatedSignalDefs = map[SignalName]AggregatedSignalDefinition {
	"signal" : AggregatedSignalDefinition {
				"signal",
				[]Param{},
				"avg",
				[]Param{"x"},
				"cpuload",
				[]Param{"x"},
			},
	}
	createBaseSession(signalNameAndPars)
	createSampledSignal(signalNameAndPars)
	createConditionalSignal(signalNameAndPars, baseSession, aggSignal)
	createAggregatedSignal(signalNameAndPars)
}

func TestRest(t *testing.T) {
	condSignal.Sample()
	checkWriteDefs("ts")
	checkSamples(theEvent)
	extractParamsMap(theEvent, map[Param]JSONPath{})
	extractFromMap(map[string]interface{} {"a":5,}, "a")
	//readAndRegister(map[string]interface{}{})
	baseSession.getState()
	getSessionSignals("sessionName", map[Param]string{})
	registerBaseSessionSignal(signalNameAndPars, &baseSession)
	getBaseSession(signalNameAndPars)
	updateBaseSession(signalNameAndPars, false)
	reportSessionSignalCreation(otherSignalNameAndPars, baseSession)
fmt.Println("")
	signalNameAndPars.equals(signalNameAndPars)
	getSignals("signal", map[Param]string{})
	registerSampledSignal(signalNameAndPars, &sampledSignal)
	getSampledSignal(signalNameAndPars)
	registerAggregatedSignal(signalNameAndPars, &aggSignal)
	getAggregatedSignal(signalNameAndPars)
	registerConditionalSignal(signalNameAndPars, &condSignal)
	reportSample(signalNameAndPars, 8)

	theGlobalWriteDefs = []SignalWriteDefinition {
		SignalWriteDefinition{
			"signal",
			"out",
			map[JSONPath]WriteValue{
			},
			SNameAndRebound{
				"signal",
				map[Param]Param{
				},
			},
		},
	}
	registerWriteDefs(signalNameAndPars, aggSignal)
	getWriters(signalNameAndPars)[0]("ts")
	//main()
	file, err := os.Open("testinputs/testdefs.json")
    if err != nil {
        panic(err)
    }
	scanAPIPipe(file)
	file, err = os.Open("testinputs/testEvents.txt")
    if err != nil {
        panic(err)
    }
	scanStdIn(file)
}

var ssdef SampledSignalDefinition = SampledSignalDefinition {
            "cpuload",
            map[Param]JSONPath {
                "x": "beat.hostname",
            },
            "in",
            "system.load.1",
        }

var bsdef BaseSessionDefinition = BaseSessionDefinition {
        "timeIsEven",
        []EventDefinition {
            EventDefinition {
                "in_condition_true",
                nil,
            },
        },
        []EventDefinition {
            EventDefinition {
                "in_condition_false",
                nil,
            },
        },
    }

func TestGetParams(t *testing.T) {
	ssdef.getParams()
	bsdef.getParams()
}

/*
*/
<|endoftext|>


<commit_before>package gocli

import (
	"fmt"
	"os"
)

func main() {
	fmt.Println(Green("gocli test script"))
	router := NewRouter(
		map[string]*Action{
			"container/start": {
				Description: "Start container",
				Usage:       "<container_id>",
				Handler: func(args *Args) error {
					fmt.Println("ACTION: start container")
					return nil
				},
			},
			"container/stop": {
				Description: "Stop container",
				Usage:       "<container_id>",
				Handler: func(args *Args) error {
					fmt.Println("ACTION: stop container", args.Args)
					return nil
				},
			},
			"image/list": {
				Description: "List Images",
				Handler: func(args *Args) error {
					fmt.Println("ACITON: list images")
					return nil
				},
			},
		},
	)
	router.Separator = " "
	router.Handle(os.Args)
}
<commit_msg>make the test program standalone and depend on dynport/gocli<commit_after>package main

import (
	"github.com/dynport/gocli"
	"fmt"
	"os"
)

func main() {
	fmt.Println(gocli.Green("gocli test script"))
	router := gocli.NewRouter(
		map[string]*gocli.Action{
			"container/start": {
				Description: "Start container",
				Usage:       "<container_id>",
				Handler: func(args *gocli.Args) error {
					fmt.Println("ACTION: start container")
					return nil
				},
			},
			"container/stop": {
				Description: "Stop container",
				Usage:       "<container_id>",
				Handler: func(args *gocli.Args) error {
					fmt.Println("ACTION: stop container", args.Args)
					return nil
				},
			},
			"image/list": {
				Description: "List Images",
				Handler: func(args *gocli.Args) error {
					fmt.Println("ACITON: list images")
					return nil
				},
			},
		},
	)
	router.Separator = " "
	router.Handle(os.Args)
}
<|endoftext|>


<commit_before>package webhook

import (
	"errors"
	"regexp"
	"strings"
)

const (
	// AttachmentColorGood define good color (green) for color fields of attachment
	AttachmentColorGood = "good"
	// AttachmentColorWarning define warning color (yellow) for color fields of attachment
	AttachmentColorWarning = "warning"
	// AttachmentColorDanger define danger color (red) for color fields of attachment
	AttachmentColorDanger = "danger"
)

var (
	// supportedColorName contains list of named color that are supported by Slack
	supportedColorName = map[string]bool{
		AttachmentColorGood:    true,
		AttachmentColorWarning: true,
		AttachmentColorDanger:  true,
	}

	// colorRegex to check if hex color is valid
	colorRegex = regexp.MustCompile("^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$")

	// ErrColorInvalid return when set invalid color to attachment
	ErrColorInvalid = errors.New("inputed color is invalid, it should be 'good', 'warning', 'danger' or a hex color")
)

// Field represent a field object in attachment fields array
// see https://api.slack.com/docs/attachments
type Field struct {
	Title string `json:"title"`
	Value string `json:"value"`
	Short bool   `json:"short"`
}

// NewField create new Field with given params
func NewField(title, value string) *Field {
	return &Field{
		Title: title,
		Value: value,
	}
}

// Attachment represent an attachment object
// see https://api.slack.com/docs/attachments
type Attachment struct {
	Fallback   string   `json:"fallback,omitempty"`
	Color      string   `json:"color,omitemtpy"`
	PreText    string   `json:"pretext,omitemtpy"`
	AuthorName string   `json:"author_name,omitemtpy"`
	AuthorLink string   `json:"author_link,omitemtpy"`
	AuthorIcon string   `json:"author_icon,omitemtpy"`
	Title      string   `json:"title,omitemtpy"`
	TitleLink  string   `json:"title_link,omitemtpy"`
	Text       string   `json:"text,omitemtpy"`
	ImageURL   string   `json:"image_url,omitemtpy"`
	ThumbURL   string   `json:"thumb_url,omitemtpy"`
	Fields     []*Field `json:"fields,omitemtpy"`
	MrkDwnIn   []string `json:"mrkdwn_in"`
}

// NewAttachment create new attachment object, with good color by default
func NewAttachment(text, title, titleURL string) *Attachment {
	attachment := &Attachment{}
	attachment.Text = text
	attachment.Title = title
	attachment.TitleLink = titleURL

	attachment.SetColorToGood()
	attachment.setDefaultMrkDwnIn()
	return attachment
}

func (a *Attachment) setDefaultMrkDwnIn() {
	a.MrkDwnIn = []string{"pretext", "text", "fields"}
}

// AddField will append an attachment field to list of fields
func (a *Attachment) AddField(title, value string) {
	a.Fields = append(a.Fields, NewField(title, value))
}

// AddShortField will append an attachment field with short is true to list of fields
func (a *Attachment) AddShortField(title, value string) {
	field := NewField(title, value)
	field.Short = true
	a.Fields = append(a.Fields, field)
}

// SetColor is used to set color to field color of attachment
// color can 'good', 'warning', 'danger' or a hex color
func (a *Attachment) SetColor(color string) error {
	if strings.HasPrefix(color, "#") {
		if !colorRegex.MatchString(color) {
			return ErrColorInvalid
		}
	} else {
		if _, found := supportedColorName[color]; !found {
			return ErrColorInvalid
		}
	}

	a.Color = color
	return nil
}

// SetColorToGood will set color to green color
func (a *Attachment) SetColorToGood() {
	a.SetColor(AttachmentColorGood)
}

// SetColorToWarning will set color to yellow color
func (a *Attachment) SetColorToWarning() {
	a.SetColor(AttachmentColorWarning)
}

// SetColorToDanger will set color to red color
func (a *Attachment) SetColorToDanger() {
	a.SetColor(AttachmentColorDanger)
}
<commit_msg>Function to set or add markdown fields in attachment payload object.<commit_after>package webhook

import (
	"errors"
	"regexp"
	"strings"
)

const (
	// AttachmentColorGood define good color (green) for color fields of attachment
	AttachmentColorGood = "good"
	// AttachmentColorWarning define warning color (yellow) for color fields of attachment
	AttachmentColorWarning = "warning"
	// AttachmentColorDanger define danger color (red) for color fields of attachment
	AttachmentColorDanger = "danger"
)

var (
	// supportedColorName contains list of named color that are supported by Slack
	supportedColorName = map[string]bool{
		AttachmentColorGood:    true,
		AttachmentColorWarning: true,
		AttachmentColorDanger:  true,
	}

	// colorRegex to check if hex color is valid
	colorRegex = regexp.MustCompile("^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$")

	// ErrColorInvalid return when set invalid color to attachment
	ErrColorInvalid = errors.New("inputed color is invalid, it should be 'good', 'warning', 'danger' or a hex color")
)

// Field represent a field object in attachment fields array
// see https://api.slack.com/docs/attachments
type Field struct {
	Title string `json:"title"`
	Value string `json:"value"`
	Short bool   `json:"short"`
}

// NewField create new Field with given params
func NewField(title, value string) *Field {
	return &Field{
		Title: title,
		Value: value,
	}
}

// Attachment represent an attachment object
// see https://api.slack.com/docs/attachments
type Attachment struct {
	Fallback   string   `json:"fallback,omitempty"`
	Color      string   `json:"color,omitemtpy"`
	PreText    string   `json:"pretext,omitemtpy"`
	AuthorName string   `json:"author_name,omitemtpy"`
	AuthorLink string   `json:"author_link,omitemtpy"`
	AuthorIcon string   `json:"author_icon,omitemtpy"`
	Title      string   `json:"title,omitemtpy"`
	TitleLink  string   `json:"title_link,omitemtpy"`
	Text       string   `json:"text,omitemtpy"`
	ImageURL   string   `json:"image_url,omitemtpy"`
	ThumbURL   string   `json:"thumb_url,omitemtpy"`
	Fields     []*Field `json:"fields,omitemtpy"`
	MrkDwnIn   []string `json:"mrkdwn_in"`
}

// NewAttachment create new attachment object, with good color by default
func NewAttachment(text, title, titleURL string) *Attachment {
	attachment := &Attachment{}
	attachment.Text = text
	attachment.Title = title
	attachment.TitleLink = titleURL

	attachment.SetColorToGood()
	attachment.SetMarkDownFields("pretext", "text", "fields")
	return attachment
}

// SetMarkDownFields set fields which are in MarkDown format,
// note that it will overwrite previous values.
// Valid values: "pretext", "text", "fields".
func (a *Attachment) SetMarkDownFields(fields ...string) {
	a.MrkDwnIn = fields
}

// AddMarkDownField will append new field to current list of markdown fields.
// Valid values: "pretext", "text", "fields".
func (a *Attachment) AddMarkDownField(field string) {
	a.MrkDwnIn = append(a.MrkDwnIn, field)
}

// AddField will append an attachment field to list of fields
func (a *Attachment) AddField(title, value string) {
	a.Fields = append(a.Fields, NewField(title, value))
}

// AddShortField will append an attachment field with short is true to list of fields
func (a *Attachment) AddShortField(title, value string) {
	field := NewField(title, value)
	field.Short = true
	a.Fields = append(a.Fields, field)
}

// SetColor is used to set color to field color of attachment
// color can 'good', 'warning', 'danger' or a hex color
func (a *Attachment) SetColor(color string) error {
	if strings.HasPrefix(color, "#") {
		if !colorRegex.MatchString(color) {
			return ErrColorInvalid
		}
	} else {
		if _, found := supportedColorName[color]; !found {
			return ErrColorInvalid
		}
	}

	a.Color = color
	return nil
}

// SetColorToGood will set color to green color
func (a *Attachment) SetColorToGood() {
	a.SetColor(AttachmentColorGood)
}

// SetColorToWarning will set color to yellow color
func (a *Attachment) SetColorToWarning() {
	a.SetColor(AttachmentColorWarning)
}

// SetColorToDanger will set color to red color
func (a *Attachment) SetColorToDanger() {
	a.SetColor(AttachmentColorDanger)
}
<|endoftext|>