<commit_before>package main

import (
	"fmt"
	"io"
	"net/url"
	"os"
	"os/exec"
	"strings"

	cors "github.com/gin-contrib/cors"

	"github.com/gin-gonic/gin"
	"github.com/grandcat/zeroconf"
	"github.com/sirupsen/logrus"
)

const (
	version     = "0.0.5b3"
	defaultPort = 8080

	zeroConfName    = "OMX Remote"
	zeroConfService = "_omx-remote-api._tcp"
	zeroConfDomain  = "local."
)

var (
	// Commands mapping to control OMXPlayer, these are piped via STDIN to omxplayer process
	Commands = map[string]string{
		"pause":             "p",            // Pause/continue playback
		"stop":              "q",            // Stop playback and exit
		"volume_up":         "+",            // Change volume by +3dB
		"volume_down":       "-",            // Change volume by -3dB
		"subtitles":         "s",            // Enable/disable subtitles
		"seek_back":         "\x1b\x5b\x44", // Seek -30 seconds
		"seek_back_fast":    "\x1b\x5b\x42", // Seek -600 second
		"seek_forward":      "\x1b\x5b\x43", // Seek +30 second
		"seek_forward_fast": "\x1b\x5b\x41", // Seek +600 seconds
		"next_audio_stream": "k",            // next audio stream
		"prev_audio_stream": "j",            // previous audio stream
	}

	// OmxPath is path to omxplayer executable
	OmxPath string

	// Omx is a child process for spawning omxplayer
	Omx *exec.Cmd

	// OmxIn is a child process STDIN pipe to send commands
	OmxIn io.WriteCloser

	// Command is a channel to pass along commands to the player routine
	Command chan string

	// StatusStream channel to broadcast any changes in playing media via SSE
	StatusStream = make(chan *MediaEntry)

	// PlayingMedia represents currently playing media
	PlayingMedia *MediaEntry

	// PlayList is a list of media entries to play sequentially
	PlayList = NewPlayList(make([]MediaEntry, 0))

	// LOG is a global app logger
	LOG *logrus.Logger
)

// MediaEntry describes model of currently playable video.
type MediaEntry struct {
	RawURL    string                 `json:"url,omitempty"`
	MediaInfo map[string]interface{} `json:"media_info,omitempty"`
}

// APIErr is a generic structure for all errors returned from API
type APIErr struct {
	Message string `json:"message,omitempty"`
}

// Determine the full path to omxplayer executable. Returns error if not found.
func omxDetect() error {
	buff, err := exec.Command("which", "omxplayer").Output()
	if err != nil {
		return err
	}

	// Set path in global variable
	OmxPath = strings.TrimSpace(string(buff))

	return nil
}

// Start command listener. Commands are coming in through a channel.
func omxListen() {
	Command = make(chan string)

	for {
		command := <-Command

		// Skip command handling of omx player is not active
		if Omx == nil {
			continue
		}

		// Send command to the player
		omxWrite(command)

		// Attempt to kill the process if stop command is requested
		if command == "stop" {
			omxStop()
		}
	}
}

// Start omxplayer playback for a given video file. Returns error if start fails.
func omxPlay(c MediaEntry) error {
	// reset autoplay flag
	// PlayList.AutoPlay = true

	contentURL, err := url.Parse(c.RawURL)
	if err != nil {
		return err
	}

	Omx = exec.Command(
		OmxPath,   // path to omxplayer executable
		"--blank", // set background to black
		// "--stats",           // Pts and buffer stats
		// "--with-info",       // dump stream format before playback
		contentURL.String(), // path to video file
	)

	// Grab child process STDIN
	stdin, err := Omx.StdinPipe()
	if err != nil {
		return err
	}

	defer stdin.Close()

	// Grab child process STDOUT
	stdout, err := Omx.StdoutPipe()
	if err != nil {
		return err
	}

	defer stdout.Close()

	// Grab child process STDERR
	stderr, err := Omx.StderrPipe()
	if err != nil {
		return err
	}

	defer stderr.Close()

	// read child process STDOUT to get status
	// status := OmxProcessStatus{Stdout: stdout, Stderr: stderr, Logger: LOG}
	// status.Start()

	// Start omxplayer execution.
	// If successful, something will appear on HDMI display.
	err = Omx.Start()
	if err != nil {
		return err
	}

	setPlayingMedia(&c)

	// Make child's STDIN globally available
	OmxIn = stdin

	// Wait until child process is finished
	err = Omx.Wait()
	if err != nil {
		LOG.Error(fmt.Sprintln("Process exited with error:", err))
	} else {
		LOG.Info("Process exited without errors.")
	}

	omxCleanup()

	// if PlayList.AutoPlay {

	// 	if next := PlayList.Next(); next != nil {
	// 		go omxPlay(*next)
	// 	}
	// }

	return nil
}

// Write a command string to the omxplayer process's STDIN
func omxWrite(command string) {
	if OmxIn != nil {
		LOG.Debug("Write omx command: " + command)
		n, err := io.WriteString(OmxIn, Commands[command])
		if err != nil {
			LOG.Error(err.Error())
			return
		}

		LOG.Debug(fmt.Sprintf("%d bytes succsessfully written", n))
	}
}

func omxStop() {
	if !omxIsActive() {
		return
	}

	PlayList.AutoPlay = false
	PlayList.CurrentIndex = positionNone

	err := Omx.Process.Kill()
	if err != nil {
		LOG.Error(err.Error())
	}
	omxCleanup()
}

// Terminate any running omxplayer processes. Fixes random hangs.
func omxKill() {
	exec.Command("killall", "omxplayer.bin").Output()
	exec.Command("killall", "omxplayer").Output()
}

// Reset internal state and stop any running processes
func omxCleanup() {
	Omx = nil
	OmxIn = nil
	setPlayingMedia(nil)

	omxKill()
}

// Check if player is currently active
func omxIsActive() bool {
	return Omx != nil
}

func setPlayingMedia(m *MediaEntry) {
	LOG.WithField("prefix", "state").Debug("new state: %+v", m)

	PlayingMedia = m

	select {
	case StatusStream <- m:
		LOG.WithField("prefix", "broadcaster").Debug("sent update: %+v", m)
	default:
	}
}

func terminate(message string, code int) {
	fmt.Println(message)
	os.Exit(code)
}

func main() {
	LOG = newLogger()
	LOG.Printf("omx-remote-api v%v", version)

	// Check if player is installed
	if omxDetect() != nil {
		terminate("omxplayer is not installed", 1)
	}

	// Make sure nothing is running
	omxCleanup()

	// Start a remote command listener
	go omxListen()

	// Register as a zero config service
	LOG.Infof("Starting zeroconf service [%s]", zeroConfName)
	server, err := zeroconf.Register(zeroConfName, zeroConfService, zeroConfDomain, defaultPort, nil, nil)
	if err != nil {
		LOG.Errorf("Cannot start zeroconf service: %s", err.Error())
	}
	defer server.Shutdown()

	// Disable debugging mode
	gin.SetMode("release")

	// Setup HTTP server
	router := gin.New()
	router.Use(gin.Recovery())

	// CORS
	router.Use(cors.Default())

	// Logger
	router.Use(HTTPLogger(LOG))

	router.GET("/status", httpStatus)
	router.GET("/status/stream", streamStatus)
	router.POST("/play", httpPlay)
	router.POST("/commands/:command", httpCommand)

	// playlist management
	router.PUT("/plist", httpNewPList)
	router.POST("/plist/commands/next", httpPListNext)
	router.POST("/plist/commands/select", httpPListSelect)
	router.POST("/plist/entries", httpPListAddEntry)
	router.DELETE("/plist", httpPlistDelete)

	LOG.Printf("Starting http server on 0.0.0.0:%d", defaultPort)
	router.Run(fmt.Sprintf(":%d", defaultPort))
}
<commit_msg>set bigger timeout to work correctly with HLS<commit_after>package main

import (
	"fmt"
	"io"
	"net/url"
	"os"
	"os/exec"
	"strings"

	cors "github.com/gin-contrib/cors"

	"github.com/gin-gonic/gin"
	"github.com/grandcat/zeroconf"
	"github.com/sirupsen/logrus"
)

const (
	version     = "0.0.5b3"
	defaultPort = 8080

	zeroConfName    = "OMX Remote"
	zeroConfService = "_omx-remote-api._tcp"
	zeroConfDomain  = "local."
)

var (
	// Commands mapping to control OMXPlayer, these are piped via STDIN to omxplayer process
	Commands = map[string]string{
		"pause":             "p",            // Pause/continue playback
		"stop":              "q",            // Stop playback and exit
		"volume_up":         "+",            // Change volume by +3dB
		"volume_down":       "-",            // Change volume by -3dB
		"subtitles":         "s",            // Enable/disable subtitles
		"seek_back":         "\x1b\x5b\x44", // Seek -30 seconds
		"seek_back_fast":    "\x1b\x5b\x42", // Seek -600 second
		"seek_forward":      "\x1b\x5b\x43", // Seek +30 second
		"seek_forward_fast": "\x1b\x5b\x41", // Seek +600 seconds
		"next_audio_stream": "k",            // next audio stream
		"prev_audio_stream": "j",            // previous audio stream
	}

	// OmxPath is path to omxplayer executable
	OmxPath string

	// Omx is a child process for spawning omxplayer
	Omx *exec.Cmd

	// OmxIn is a child process STDIN pipe to send commands
	OmxIn io.WriteCloser

	// Command is a channel to pass along commands to the player routine
	Command chan string

	// StatusStream channel to broadcast any changes in playing media via SSE
	StatusStream = make(chan *MediaEntry)

	// PlayingMedia represents currently playing media
	PlayingMedia *MediaEntry

	// PlayList is a list of media entries to play sequentially
	PlayList = NewPlayList(make([]MediaEntry, 0))

	// LOG is a global app logger
	LOG *logrus.Logger
)

// MediaEntry describes model of currently playable video.
type MediaEntry struct {
	RawURL    string                 `json:"url,omitempty"`
	MediaInfo map[string]interface{} `json:"media_info,omitempty"`
}

// APIErr is a generic structure for all errors returned from API
type APIErr struct {
	Message string `json:"message,omitempty"`
}

// Determine the full path to omxplayer executable. Returns error if not found.
func omxDetect() error {
	buff, err := exec.Command("which", "omxplayer").Output()
	if err != nil {
		return err
	}

	// Set path in global variable
	OmxPath = strings.TrimSpace(string(buff))

	return nil
}

// Start command listener. Commands are coming in through a channel.
func omxListen() {
	Command = make(chan string)

	for {
		command := <-Command

		// Skip command handling of omx player is not active
		if Omx == nil {
			continue
		}

		// Send command to the player
		omxWrite(command)

		// Attempt to kill the process if stop command is requested
		if command == "stop" {
			omxStop()
		}
	}
}

// Start omxplayer playback for a given video file. Returns error if start fails.
func omxPlay(c MediaEntry) error {
	// reset autoplay flag
	// PlayList.AutoPlay = true

	contentURL, err := url.Parse(c.RawURL)
	if err != nil {
		return err
	}

	Omx = exec.Command(
		OmxPath,           // path to omxplayer executable
		"--blank",         // set background to black
		"--timeout", "60", // set  bigger timeout, default is 10
		// "--stats",           // Pts and buffer stats
		// "--with-info",       // dump stream format before playback
		contentURL.String(), // path to video file
	)

	// Grab child process STDIN
	stdin, err := Omx.StdinPipe()
	if err != nil {
		return err
	}

	defer stdin.Close()

	// Grab child process STDOUT
	stdout, err := Omx.StdoutPipe()
	if err != nil {
		return err
	}

	defer stdout.Close()

	// Grab child process STDERR
	stderr, err := Omx.StderrPipe()
	if err != nil {
		return err
	}

	defer stderr.Close()

	// read child process STDOUT to get status
	// status := OmxProcessStatus{Stdout: stdout, Stderr: stderr, Logger: LOG}
	// status.Start()

	// Start omxplayer execution.
	// If successful, something will appear on HDMI display.
	err = Omx.Start()
	if err != nil {
		return err
	}

	setPlayingMedia(&c)

	// Make child's STDIN globally available
	OmxIn = stdin

	// Wait until child process is finished
	err = Omx.Wait()
	if err != nil {
		LOG.Error(fmt.Sprintln("Process exited with error:", err))
	} else {
		LOG.Info("Process exited without errors.")
	}

	omxCleanup()

	// if PlayList.AutoPlay {

	// 	if next := PlayList.Next(); next != nil {
	// 		go omxPlay(*next)
	// 	}
	// }

	return nil
}

// Write a command string to the omxplayer process's STDIN
func omxWrite(command string) {
	if OmxIn != nil {
		LOG.Debug("Write omx command: " + command)
		n, err := io.WriteString(OmxIn, Commands[command])
		if err != nil {
			LOG.Error(err.Error())
			return
		}

		LOG.Debug(fmt.Sprintf("%d bytes succsessfully written", n))
	}
}

func omxStop() {
	if !omxIsActive() {
		return
	}

	PlayList.AutoPlay = false
	PlayList.CurrentIndex = positionNone

	err := Omx.Process.Kill()
	if err != nil {
		LOG.Error(err.Error())
	}
	omxCleanup()
}

// Terminate any running omxplayer processes. Fixes random hangs.
func omxKill() {
	exec.Command("killall", "omxplayer.bin").Output()
	exec.Command("killall", "omxplayer").Output()
}

// Reset internal state and stop any running processes
func omxCleanup() {
	Omx = nil
	OmxIn = nil
	setPlayingMedia(nil)

	omxKill()
}

// Check if player is currently active
func omxIsActive() bool {
	return Omx != nil
}

func setPlayingMedia(m *MediaEntry) {
	LOG.WithField("prefix", "state").Debug("new state: %+v", m)

	PlayingMedia = m

	select {
	case StatusStream <- m:
		LOG.WithField("prefix", "broadcaster").Debug("sent update: %+v", m)
	default:
	}
}

func terminate(message string, code int) {
	fmt.Println(message)
	os.Exit(code)
}

func main() {
	LOG = newLogger()
	LOG.Printf("omx-remote-api v%v", version)

	// Check if player is installed
	if omxDetect() != nil {
		terminate("omxplayer is not installed", 1)
	}

	// Make sure nothing is running
	omxCleanup()

	// Start a remote command listener
	go omxListen()

	// Register as a zero config service
	LOG.Infof("Starting zeroconf service [%s]", zeroConfName)
	server, err := zeroconf.Register(zeroConfName, zeroConfService, zeroConfDomain, defaultPort, nil, nil)
	if err != nil {
		LOG.Errorf("Cannot start zeroconf service: %s", err.Error())
	}
	defer server.Shutdown()

	// Disable debugging mode
	gin.SetMode("release")

	// Setup HTTP server
	router := gin.New()
	router.Use(gin.Recovery())

	// CORS
	router.Use(cors.Default())

	// Logger
	router.Use(HTTPLogger(LOG))

	router.GET("/status", httpStatus)
	router.GET("/status/stream", streamStatus)
	router.POST("/play", httpPlay)
	router.POST("/commands/:command", httpCommand)

	// playlist management
	router.PUT("/plist", httpNewPList)
	router.POST("/plist/commands/next", httpPListNext)
	router.POST("/plist/commands/select", httpPListSelect)
	router.POST("/plist/entries", httpPListAddEntry)
	router.DELETE("/plist", httpPlistDelete)

	LOG.Printf("Starting http server on 0.0.0.0:%d", defaultPort)
	router.Run(fmt.Sprintf(":%d", defaultPort))
}
<|endoftext|>


<commit_before>package main

import (
	"crypto/rand"
	"encoding/base64"
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"io"
	"log"
	"net/http"
	"net/url"
	"os"
	"os/exec"
	"strings"

	"golang.org/x/crypto/bcrypt"
)

var rpcListenPort = flag.Int("rpc-listen-port", 7800, "Specify a port number for JSON-RPC server to listen to. Possible values: 1024-65535")
var rpcSecret = flag.String("rpc-secret", "", "Set RPC secret authorization token (required)")

// Info is used for logging information.
var Info = log.New(os.Stdout, "INFO: ", log.Ldate|log.Ltime|log.Lshortfile)

// Error is used for logging errors.
var Error = log.New(os.Stderr, "ERROR: ", log.Ldate|log.Ltime|log.Lshortfile)

// Request represents single request for mirroring one FTP directory or a file.
type Request struct {
	Path     string `json:"path"`
	Username string `json:"username"`
	Password string `json:"password"`
	Secret   string `json:"secret"`
}

// Response represents response to a client with ID for a created job or error message in case of error.
type Response struct {
	ID      string `json:"id"`
	Message string `json:"message"`
}

// Handler implements http.Handler interface and processes download requests sequentially.
type Handler struct {
	Jobs      chan *Job
	TokenHash []byte
}

// Job is single download request with associated ID and LFTP command.
type Job struct {
	ID      string
	Request *Request
	Command *exec.Cmd
}

func (request *Request) makeCmd() (*exec.Cmd, error) {
	if request.Path == "" {
		return nil, errors.New("No URL specified in a request")
	}

	url, err := url.Parse(request.Path)

	if err != nil {
		return nil, fmt.Errorf("Invalid URL: %s", request.Path)
	}

	if url.Scheme != "ftp" {
		return nil, fmt.Errorf("Only FTP downloads are supported")
	}

	lftpCmd := makeLftpCmd(url.Path)
	var args []string

	if request.Username != "" && request.Password != "" {
		args = []string{"--user", request.Username, "--password", request.Password, "-e", lftpCmd, url.Host}
	} else {
		args = []string{"-e", lftpCmd, url.Host}
	}

	cmd := exec.Command("lftp", args...)

	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr

	return cmd, nil
}

func generateID() string {
	b := make([]byte, 32)

	if _, err := rand.Read(b); err != nil {
		panic("Random number generator failed")
	}

	return base64.StdEncoding.EncodeToString(b)
}

func makeLftpCmd(path string) string {
	if path == "" {
		return "mirror && exit"
	}

	lftpCmd := "pget"

	if strings.HasSuffix(path, "/") {
		lftpCmd = "mirror"
	}

	escaped := strings.Replace(path, "\"", "\\\"", -1)
	return fmt.Sprintf("%s \"%s\" && exit", lftpCmd, escaped)
}

func decodeRequest(r io.Reader) (*Request, error) {
	var request Request
	decoder := json.NewDecoder(r)

	if err := decoder.Decode(&request); err != nil {
		return nil, fmt.Errorf("Invalid request received: %v", err)
	}

	return &request, nil
}

func (handler *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	id := generateID()

	Info.Printf("Received download request %s from %s\n", id, r.RemoteAddr)

	request, err := decodeRequest(r.Body)

	if err != nil {
		serveError(w, http.StatusBadRequest, err.Error())
		return
	}

	if err = bcrypt.CompareHashAndPassword(handler.TokenHash, []byte(request.Secret)); err != nil {
		serveError(w, http.StatusUnauthorized, "Secret token does not match")
		return
	}

	cmd, err := request.makeCmd()

	if err != nil {
		serveError(w, http.StatusBadRequest, err.Error())
		return
	}

	job := Job{ID: id, Request: request, Command: cmd}

	Info.Printf("Download request %s has URL %s\n", id, request.Path)
	json.NewEncoder(w).Encode(Response{ID: id})

	go func() {
		handler.Jobs <- &job
	}()
}

func serveError(w http.ResponseWriter, status int, err string) {
	w.WriteHeader(status)
	json.NewEncoder(w).Encode(Response{Message: err})
	Error.Println(err)
}

func (handler *Handler) worker() {
	for job := range handler.Jobs {
		Info.Printf("Begin LFTP output for request %s", job.ID)
		err := job.Command.Run()
		Info.Printf("End LFTP output for request %s", job.ID)

		if err != nil {
			Error.Printf("Failed to execute request %s with error: %v\n", job.ID, err)
		} else {
			Info.Printf("Request %s completed", job.ID)
		}
	}
}

func main() {
	flag.Parse()

	if (*rpcListenPort < 1024 || *rpcListenPort > 65535) || *rpcSecret == "" {
		flag.Usage()
		os.Exit(1)
	}

	tokenHash, err := bcrypt.GenerateFromPassword([]byte(*rpcSecret), bcrypt.DefaultCost)

	if err != nil {
		log.Fatal("bcrypt failed to generate token hash")
	}

	if _, err := exec.LookPath("lftp"); err != nil {
		log.Fatal("LFTP not found")
	}

	handler := &Handler{
		Jobs:      make(chan *Job, 10),
		TokenHash: tokenHash,
	}

	http.Handle("/jsonrpc", handler)
	go handler.worker()

	Info.Printf("Starting LFTP server on port %d\n", *rpcListenPort)
	log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *rpcListenPort), nil))
}
<commit_msg>Test credentials  before running LFTP job<commit_after>package main

import (
	"crypto/rand"
	"encoding/base64"
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"io"
	"log"
	"net/http"
	"net/url"
	"os"
	"os/exec"
	"strings"
	"time"

	"github.com/jlaffaye/ftp"
	"golang.org/x/crypto/bcrypt"
)

var rpcListenPort = flag.Int("rpc-listen-port", 7800, "Specify a port number for JSON-RPC server to listen to. Possible values: 1024-65535")
var rpcSecret = flag.String("rpc-secret", "", "Set RPC secret authorization token (required)")

// Info is used for logging information.
var Info = log.New(os.Stdout, "INFO: ", log.Ldate|log.Ltime|log.Lshortfile)

// Error is used for logging errors.
var Error = log.New(os.Stderr, "ERROR: ", log.Ldate|log.Ltime|log.Lshortfile)

// Request represents single request for mirroring one FTP directory or a file.
type Request struct {
	Path     string `json:"path"`
	Username string `json:"username"`
	Password string `json:"password"`
	Secret   string `json:"secret"`
}

// Response represents response to a client with ID for a created job or error message in case of error.
type Response struct {
	ID      string `json:"id"`
	Message string `json:"message"`
}

// Handler implements http.Handler interface and processes download requests sequentially.
type Handler struct {
	Jobs      chan *Job
	TokenHash []byte
}

// Job is single download request with associated ID and LFTP command.
type Job struct {
	ID      string
	Request *Request
	Command *exec.Cmd
}

func (request *Request) extractURL() (*url.URL, error) {
	if request.Path == "" {
		return nil, errors.New("No URL specified in a request")
	}

	url, err := url.Parse(request.Path)

	if err != nil {
		return nil, fmt.Errorf("Invalid URL: %s", request.Path)
	}

	if url.Scheme != "ftp" {
		return nil, fmt.Errorf("Only FTP downloads are supported")
	}

	return url, nil
}

func (request *Request) makeCmd(url *url.URL) (*exec.Cmd, error) {
	lftpCmd := makeLftpCmd(url.Path)
	var args []string

	if request.Username != "" && request.Password != "" {
		args = []string{"--user", request.Username, "--password", request.Password, "-e", lftpCmd, url.Host}
	} else {
		args = []string{"-e", lftpCmd, url.Host}
	}

	cmd := exec.Command("lftp", args...)

	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr

	return cmd, nil
}

func generateID() string {
	b := make([]byte, 32)

	if _, err := rand.Read(b); err != nil {
		panic("Random number generator failed")
	}

	return base64.StdEncoding.EncodeToString(b)
}

func makeLftpCmd(path string) string {
	if path == "" {
		return "mirror && exit"
	}

	lftpCmd := "pget"

	if strings.HasSuffix(path, "/") {
		lftpCmd = "mirror"
	}

	escaped := strings.Replace(path, "\"", "\\\"", -1)
	return fmt.Sprintf("%s \"%s\" && exit", lftpCmd, escaped)
}

func decodeRequest(r io.Reader) (*Request, error) {
	var request Request
	decoder := json.NewDecoder(r)

	if err := decoder.Decode(&request); err != nil {
		return nil, fmt.Errorf("Invalid request received: %v", err)
	}

	return &request, nil
}

func (handler *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	id := generateID()

	Info.Printf("Received download request %s from %s\n", id, r.RemoteAddr)

	request, err := decodeRequest(r.Body)

	if err != nil {
		serveError(w, http.StatusBadRequest, err.Error())
		return
	}

	if err = bcrypt.CompareHashAndPassword(handler.TokenHash, []byte(request.Secret)); err != nil {
		serveError(w, http.StatusBadRequest, "Secret token does not match")
		return
	}

	url, err := request.extractURL()

	if err != nil {
		serveError(w, http.StatusBadRequest, err.Error())
		return
	}

	cmd, err := request.makeCmd(url)

	if err != nil {
		serveError(w, http.StatusBadRequest, err.Error())
		return
	}

	conn, err := ftp.DialTimeout(fmt.Sprintf("%s:21", url.Host), 5*time.Second)

	if err != nil {
		serveError(w, http.StatusBadRequest, fmt.Sprintf("Unable to connect to FTP server at %s", url.Host))
		return
	}

	if request.Username != "" && request.Password != "" {
		err = conn.Login(request.Username, request.Password)
	} else {
		err = conn.Login("anonymous", "anonymous")
	}

	if err != nil {
		serveError(w, http.StatusUnauthorized, "Missing or invalid credentials")
		return
	}

	conn.Logout()
	job := Job{ID: id, Request: request, Command: cmd}

	Info.Printf("Download request %s has URL %s\n", id, request.Path)
	json.NewEncoder(w).Encode(Response{ID: id})

	go func() {
		handler.Jobs <- &job
	}()
}

func serveError(w http.ResponseWriter, status int, err string) {
	w.WriteHeader(status)
	json.NewEncoder(w).Encode(Response{Message: err})
	Error.Println(err)
}

func (handler *Handler) worker() {
	for job := range handler.Jobs {
		Info.Printf("Begin LFTP output for request %s", job.ID)
		err := job.Command.Run()
		Info.Printf("End LFTP output for request %s", job.ID)

		if err != nil {
			Error.Printf("Failed to execute request %s with error: %v\n", job.ID, err)
		} else {
			Info.Printf("Request %s completed", job.ID)
		}
	}
}

func main() {
	flag.Parse()

	if (*rpcListenPort < 1024 || *rpcListenPort > 65535) || *rpcSecret == "" {
		flag.Usage()
		os.Exit(1)
	}

	tokenHash, err := bcrypt.GenerateFromPassword([]byte(*rpcSecret), bcrypt.DefaultCost)

	if err != nil {
		log.Fatal("bcrypt failed to generate token hash")
	}

	if _, err := exec.LookPath("lftp"); err != nil {
		log.Fatal("LFTP not found")
	}

	handler := &Handler{
		Jobs:      make(chan *Job, 10),
		TokenHash: tokenHash,
	}

	http.Handle("/jsonrpc", handler)
	go handler.worker()

	Info.Printf("Starting LFTP server on port %d\n", *rpcListenPort)
	log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *rpcListenPort), nil))
}
<|endoftext|>


<commit_before>// Copyright 2016 Marc-Antoine Ruel. All rights reserved.
// Use of this source code is governed under the Apache License, Version 2.0
// that can be found in the LICENSE file.

// sci is a shameful CI.
//
// It is a simple Github webhook that runs a Go build and an hardcoded
// command upon PR or push from whitelisted users.
//
// It posts the stdout to a Github gist and updates the PR status.
//
// It doesn't stream data so it cannot be used for slow task.
package main

import (
	"bytes"
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"net"
	"net/http"
	"os"
	"os/exec"
	"path"
	"path/filepath"
	"reflect"
	"runtime"
	"strings"
	"sync"
	"time"

	"github.com/bugsnag/osext"
	"github.com/google/go-github/github"
	"golang.org/x/oauth2"
)

type config struct {
	Port              int      // TCP port number for HTTP server.
	WebHookSecret     string   // https://developer.github.com/webhooks/
	Oauth2AccessToken string   // https://github.com/settings/tokens, check "repo:status" and "gist"
	UseSSH            bool     // Use ssh (instead of https) for checkout. Required for private repositories.
	Name              string   // Display name to use in the status report on Github.
	Check             []string // Command to run to test the repository. It is run from the repository's root.
}

func loadConfig() (*config, error) {
	c := &config{
		Port:              8080,
		WebHookSecret:     "Create a secret and set it at github.com/'name'/'repo'/settings/hooks",
		Oauth2AccessToken: "Get one at https://github.com/settings/tokens",
		UseSSH:            false,
		Name:              "sci",
		Check:             []string{"go", "test", "./..."},
	}
	b, err := ioutil.ReadFile("sci.json")
	if err != nil {
		b, err = json.MarshalIndent(c, "", "  ")
		if err != nil {
			return nil, err
		}
		if err = ioutil.WriteFile("sci.json", b, 0600); err != nil {
			return nil, err
		}
		return nil, errors.New("wrote new sci.json")
	}
	if err = json.Unmarshal(b, c); err != nil {
		return nil, err
	}
	d, err := json.MarshalIndent(c, "", "  ")
	if err != nil {
		return nil, err
	}
	if !bytes.Equal(b, d) {
		log.Printf("Updating sci.json in canonical format")
		if err := ioutil.WriteFile("sci.json", d, 0600); err != nil {
			return nil, err
		}
	}
	return c, nil
}

func run(cwd string, cmd ...string) (string, bool) {
	cmds := strings.Join(cmd, " ")
	log.Printf("- cwd=%s : %s", cwd, cmds)
	c := exec.Command(cmd[0], cmd[1:]...)
	c.Dir = cwd
	start := time.Now()
	out, err := c.CombinedOutput()
	duration := time.Since(start)
	// Assumes UTF-8.
	return fmt.Sprintf("$ %s\nin %s\n%s", cmds, duration, string(out)), err == nil
}

// runCheck syncs then runs the check and returns task's metadata and stdout.
func runCheck(cmd []string, repoName string, useSSH bool, commit, gopath string) (string, string, bool) {
	metadata := fmt.Sprintf("Commit: %s\nVersion: %s\nGOROOT: %s\nGOPATH: %s\nCPUs: %d\n---\n",
		commit, runtime.Version(), runtime.GOROOT(), gopath, runtime.NumCPU())
	repoPath := "github.com/" + repoName
	base := filepath.Join(gopath, "src", repoPath)
	if _, err := os.Stat(base); err != nil {
		up := path.Dir(base)
		if err := os.MkdirAll(up, 0700); err != nil && !os.IsExist(err) {
			log.Printf("- %v", err)
		}
		url := "https://" + repoPath
		if useSSH {
			url = "git@github.com:" + repoName
		}
		out1, ok := run(up, "git", "clone", "--quiet", url)
		metadata += out1
		if !ok {
			return metadata, "", ok
		}
	} else {
		out1, ok := run(base, "git", "fetch", "--prune", "--quiet")
		metadata += out1
		if !ok {
			return metadata, "", ok
		}
	}
	out1, ok := run(base, "git", "checkout", "--quiet", commit)
	metadata += out1
	if !ok {
		return metadata, "", ok
	}
	// TODO(maruel): update dependencies manually!
	out1, ok = run(gopath, "go", "get", "-v", "-d", "-t", repoPath+"/...")
	metadata += out1
	if !ok {
		return metadata, "", ok
	}
	// Precompilation has a dramatic effect on a Raspberry Pi.
	out1, ok = run(gopath, "go", "test", "-i", repoPath+"/...")
	metadata += out1
	if !ok {
		return metadata, "", ok
	}
	out, ok := run(base, cmd...)
	return metadata, out, ok
}

type server struct {
	c       *config
	client  *github.Client
	gopath  string
	mu      sync.Mutex
	collabs map[string]map[string]bool
}

func (s *server) canCollab(owner, repo, user string) bool {
	key := owner + "/" + repo
	s.mu.Lock()
	defer s.mu.Unlock()
	if _, ok := s.collabs[key]; !ok {
		s.collabs[key] = map[string]bool{}
	}
	if v, ok := s.collabs[key][user]; ok {
		return v
	}
	v, _, _ := s.client.Repositories.IsCollaborator(owner, repo, user)
	if v {
		// Only cache hits because otherwise adding a collaborator would mean
		// restarting every sci instances.
		s.collabs[key][user] = v
	}
	log.Printf("- %s: %s access: %t", key, user, v)
	return v
}

func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	log.Printf("HTTP: %s %s", r.RemoteAddr, r.URL.Path)
	defer r.Body.Close()
	if r.Method != "POST" {
		http.Error(w, "Invalid method", http.StatusMethodNotAllowed)
		log.Printf("- invalid method")
		return
	}
	payload, err := github.ValidatePayload(r, []byte(s.c.WebHookSecret))
	if err != nil {
		http.Error(w, "Invalid secret", http.StatusUnauthorized)
		log.Printf("- invalid secret")
		return
	}
	if t := github.WebHookType(r); t != "ping" {
		event, err := github.ParseWebHook(t, payload)
		if err != nil {
			http.Error(w, "Invalid payload", http.StatusBadRequest)
			log.Printf("- invalid payload")
			return
		}
		// Process the rest asynchronously so the hook doesn't take too long.
		go func() {
			switch event := event.(type) {
			// TODO(maruel): For *github.CommitCommentEvent and
			// *github.IssueCommentEvent, when the comment is 'run tests' from a
			// collaborator, run the tests.
			case *github.PullRequestEvent:
				log.Printf("- PR %s #%d %s %s", *event.Repo.FullName, *event.PullRequest.ID, *event.Sender.Login, *event.Action)
				if *event.Action != "opened" && *event.Action != "synchronized" {
					log.Printf("- ignoring action %q for PR from %q", *event.Action, *event.Sender.Login)
				} else if !s.canCollab(*event.Repo.Owner.Login, *event.Repo.Name, *event.Sender.Login) {
					log.Printf("- ignoring owner %q for PR", *event.Sender.Login)
				} else if err = s.runCheck(*event.Repo.FullName, *event.PullRequest.Head.SHA); err != nil {
					log.Printf("- %v", err)
				}
			case *github.PushEvent:
				if event.HeadCommit == nil {
					log.Printf("- Push %s %s <deleted>", *event.Repo.FullName, *event.Ref)
				} else {
					log.Printf("- Push %s %s %s", *event.Repo.FullName, *event.Ref, *event.HeadCommit.ID)
					if !strings.HasPrefix(*event.Ref, "refs/heads/") {
						log.Printf("- ignoring branch %q for push", *event.Ref)
					} else if err = s.runCheck(*event.Repo.FullName, *event.HeadCommit.ID); err != nil {
						log.Printf("- %v", err)
					}
				}
			default:
				log.Printf("- ignoring hook type %s", reflect.TypeOf(event).Elem().Name())
			}
		}()
	}
	io.WriteString(w, "{}")
}

func (s *server) runCheck(repo, commit string) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	log.Printf("- Running test for %s at %s", repo, commit)
	// TODO(maruel): Update the gist as the task is running;
	// https://developer.github.com/v3/gists/#edit-a-gist
	metadata, out, success := runCheck(s.c.Check, repo, s.c.UseSSH, commit, s.gopath)
	if metadata == "" {
		metadata = "<missing>"
	}
	if out == "" {
		out = "<missing>"
	}
	// https://developer.github.com/v3/gists/#create-a-gist
	gist := &github.Gist{
		Description: github.String("Output for https://github.com/" + repo + "/commit/" + commit),
		// It is still accessible via the URL;
		Public: github.Bool(false),
		Files: map[github.GistFilename]github.GistFile{
			"metadata": github.GistFile{Content: &metadata},
			"stdout":   github.GistFile{Content: &out},
		},
	}
	var err error
	if gist, _, err = s.client.Gists.Create(gist); err != nil {
		return err
	}
	log.Printf("- Gist at %s", *gist.HTMLURL)

	// https://developer.github.com/v3/repos/statuses/#create-a-status
	status := &github.RepoStatus{
		State:       github.String("success"),
		TargetURL:   gist.HTMLURL,
		Description: github.String(fmt.Sprintf("Ran: %s", strings.Join(s.c.Check, " "))),
		Context:     github.String("sci"),
	}
	if !success {
		status.State = github.String("failure")
	}
	parts := strings.SplitN(repo, "/", 2)
	_, _, err = s.client.Repositories.CreateStatus(parts[0], parts[1], commit, status)
	return err
}

func mainImpl() error {
	test := flag.String("test", "", "runs a simulation locally, specify the git repository name (not URL) to test, e.g. 'maruel/sci'")
	commit := flag.String("commit", "HEAD", "commit ID to test and update; will only update if not 'HEAD'")
	flag.Parse()
	c, err := loadConfig()
	if err != nil {
		return err
	}
	wd, err := os.Getwd()
	if err != nil {
		return err
	}
	gopath := filepath.Join(wd, "sci-gopath")
	os.Setenv("GOPATH", gopath)
	tc := oauth2.NewClient(oauth2.NoContext, oauth2.StaticTokenSource(&oauth2.Token{AccessToken: c.Oauth2AccessToken}))
	s := server{c: c, client: github.NewClient(tc), gopath: gopath, collabs: map[string]map[string]bool{}}
	if len(*test) != 0 {
		if *commit == "HEAD" {
			// Only run locally.
			metadata, out, success := runCheck(c.Check, *test, c.UseSSH, *commit, gopath)
			_, err := fmt.Printf("%s---\n%sSuccess: %t\n", metadata, out, success)
			return err

		}
		return s.runCheck(*test, *commit)
	}
	http.Handle("/", &s)
	thisFile, err := osext.Executable()
	if err != nil {
		return err
	}
	log.Printf("Running in: %s", wd)
	log.Printf("Executable: %s", thisFile)
	log.Printf("Listening on: %d", c.Port)
	ln, err := net.Listen("tcp", fmt.Sprintf(":%d", c.Port))
	if err != nil {
		return err
	}
	server := &http.Server{Addr: ln.Addr().String()}
	go server.ListenAndServe()
	err = watchFile(thisFile)
	// Ensures no task is running.
	s.mu.Lock()
	return err
}

func main() {
	if err := mainImpl(); err != nil {
		fmt.Fprintf(os.Stderr, "sci: %s.\n", err)
		os.Exit(1)
	}
}
<commit_msg>Use more constants.<commit_after>// Copyright 2016 Marc-Antoine Ruel. All rights reserved.
// Use of this source code is governed under the Apache License, Version 2.0
// that can be found in the LICENSE file.

// sci is a shameful CI.
//
// It is a simple Github webhook that runs a Go build and an hardcoded
// command upon PR or push from whitelisted users.
//
// It posts the stdout to a Github gist and updates the PR status.
//
// It doesn't stream data so it cannot be used for slow task.
package main

import (
	"bytes"
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"net"
	"net/http"
	"os"
	"os/exec"
	"path"
	"path/filepath"
	"reflect"
	"runtime"
	"strings"
	"sync"
	"time"

	"github.com/bugsnag/osext"
	"github.com/google/go-github/github"
	"golang.org/x/oauth2"
)

type config struct {
	Port              int      // TCP port number for HTTP server.
	WebHookSecret     string   // https://developer.github.com/webhooks/
	Oauth2AccessToken string   // https://github.com/settings/tokens, check "repo:status" and "gist"
	UseSSH            bool     // Use ssh (instead of https) for checkout. Required for private repositories.
	Name              string   // Display name to use in the status report on Github.
	Check             []string // Command to run to test the repository. It is run from the repository's root.
}

func loadConfig() (*config, error) {
	c := &config{
		Port:              8080,
		WebHookSecret:     "Create a secret and set it at github.com/'name'/'repo'/settings/hooks",
		Oauth2AccessToken: "Get one at https://github.com/settings/tokens",
		UseSSH:            false,
		Name:              "sci",
		Check:             []string{"go", "test", "./..."},
	}
	b, err := ioutil.ReadFile("sci.json")
	if err != nil {
		b, err = json.MarshalIndent(c, "", "  ")
		if err != nil {
			return nil, err
		}
		if err = ioutil.WriteFile("sci.json", b, 0600); err != nil {
			return nil, err
		}
		return nil, errors.New("wrote new sci.json")
	}
	if err = json.Unmarshal(b, c); err != nil {
		return nil, err
	}
	d, err := json.MarshalIndent(c, "", "  ")
	if err != nil {
		return nil, err
	}
	if !bytes.Equal(b, d) {
		log.Printf("Updating sci.json in canonical format")
		if err := ioutil.WriteFile("sci.json", d, 0600); err != nil {
			return nil, err
		}
	}
	return c, nil
}

func run(cwd string, cmd ...string) (string, bool) {
	cmds := strings.Join(cmd, " ")
	log.Printf("- cwd=%s : %s", cwd, cmds)
	c := exec.Command(cmd[0], cmd[1:]...)
	c.Dir = cwd
	start := time.Now()
	out, err := c.CombinedOutput()
	duration := time.Since(start)
	// Assumes UTF-8.
	return fmt.Sprintf("$ %s\nin %s\n%s", cmds, duration, string(out)), err == nil
}

// runCheck syncs then runs the check and returns task's metadata and stdout.
func runCheck(cmd []string, repoName string, useSSH bool, commit, gopath string) (string, string, bool) {
	metadata := fmt.Sprintf("Commit: %s\nVersion: %s\nGOROOT: %s\nGOPATH: %s\nCPUs: %d\n---\n",
		commit, runtime.Version(), runtime.GOROOT(), gopath, runtime.NumCPU())
	repoPath := "github.com/" + repoName
	base := filepath.Join(gopath, "src", repoPath)
	if _, err := os.Stat(base); err != nil {
		up := path.Dir(base)
		if err := os.MkdirAll(up, 0700); err != nil && !os.IsExist(err) {
			log.Printf("- %v", err)
		}
		url := "https://" + repoPath
		if useSSH {
			url = "git@github.com:" + repoName
		}
		out1, ok := run(up, "git", "clone", "--quiet", url)
		metadata += out1
		if !ok {
			return metadata, "", ok
		}
	} else {
		out1, ok := run(base, "git", "fetch", "--prune", "--quiet")
		metadata += out1
		if !ok {
			return metadata, "", ok
		}
	}
	out1, ok := run(base, "git", "checkout", "--quiet", commit)
	metadata += out1
	if !ok {
		return metadata, "", ok
	}
	// TODO(maruel): update dependencies manually!
	out1, ok = run(base, "go", "get", "-v", "-d", "-t", "./...")
	metadata += out1
	if !ok {
		return metadata, "", ok
	}
	// Precompilation has a dramatic effect on a Raspberry Pi.
	out1, ok = run(base, "go", "test", "-i", "./...")
	metadata += out1
	if !ok {
		return metadata, "", ok
	}
	out, ok := run(base, cmd...)
	return metadata, out, ok
}

type server struct {
	c       *config
	client  *github.Client
	gopath  string
	mu      sync.Mutex
	collabs map[string]map[string]bool
}

func (s *server) canCollab(owner, repo, user string) bool {
	key := owner + "/" + repo
	s.mu.Lock()
	defer s.mu.Unlock()
	if _, ok := s.collabs[key]; !ok {
		s.collabs[key] = map[string]bool{}
	}
	if v, ok := s.collabs[key][user]; ok {
		return v
	}
	v, _, _ := s.client.Repositories.IsCollaborator(owner, repo, user)
	if v {
		// Only cache hits because otherwise adding a collaborator would mean
		// restarting every sci instances.
		s.collabs[key][user] = v
	}
	log.Printf("- %s: %s access: %t", key, user, v)
	return v
}

func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	log.Printf("HTTP: %s %s", r.RemoteAddr, r.URL.Path)
	defer r.Body.Close()
	if r.Method != "POST" {
		http.Error(w, "Invalid method", http.StatusMethodNotAllowed)
		log.Printf("- invalid method")
		return
	}
	payload, err := github.ValidatePayload(r, []byte(s.c.WebHookSecret))
	if err != nil {
		http.Error(w, "Invalid secret", http.StatusUnauthorized)
		log.Printf("- invalid secret")
		return
	}
	if t := github.WebHookType(r); t != "ping" {
		event, err := github.ParseWebHook(t, payload)
		if err != nil {
			http.Error(w, "Invalid payload", http.StatusBadRequest)
			log.Printf("- invalid payload")
			return
		}
		// Process the rest asynchronously so the hook doesn't take too long.
		go func() {
			switch event := event.(type) {
			// TODO(maruel): For *github.CommitCommentEvent and
			// *github.IssueCommentEvent, when the comment is 'run tests' from a
			// collaborator, run the tests.
			case *github.PullRequestEvent:
				log.Printf("- PR %s #%d %s %s", *event.Repo.FullName, *event.PullRequest.ID, *event.Sender.Login, *event.Action)
				if *event.Action != "opened" && *event.Action != "synchronized" {
					log.Printf("- ignoring action %q for PR from %q", *event.Action, *event.Sender.Login)
				} else if !s.canCollab(*event.Repo.Owner.Login, *event.Repo.Name, *event.Sender.Login) {
					log.Printf("- ignoring owner %q for PR", *event.Sender.Login)
				} else if err = s.runCheck(*event.Repo.FullName, *event.PullRequest.Head.SHA); err != nil {
					log.Printf("- %v", err)
				}
			case *github.PushEvent:
				if event.HeadCommit == nil {
					log.Printf("- Push %s %s <deleted>", *event.Repo.FullName, *event.Ref)
				} else {
					log.Printf("- Push %s %s %s", *event.Repo.FullName, *event.Ref, *event.HeadCommit.ID)
					if !strings.HasPrefix(*event.Ref, "refs/heads/") {
						log.Printf("- ignoring branch %q for push", *event.Ref)
					} else if err = s.runCheck(*event.Repo.FullName, *event.HeadCommit.ID); err != nil {
						log.Printf("- %v", err)
					}
				}
			default:
				log.Printf("- ignoring hook type %s", reflect.TypeOf(event).Elem().Name())
			}
		}()
	}
	io.WriteString(w, "{}")
}

func (s *server) runCheck(repo, commit string) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	log.Printf("- Running test for %s at %s", repo, commit)
	// TODO(maruel): Update the gist as the task is running;
	// https://developer.github.com/v3/gists/#edit-a-gist
	metadata, out, success := runCheck(s.c.Check, repo, s.c.UseSSH, commit, s.gopath)
	if metadata == "" {
		metadata = "<missing>"
	}
	if out == "" {
		out = "<missing>"
	}
	// https://developer.github.com/v3/gists/#create-a-gist
	gist := &github.Gist{
		Description: github.String("Output for https://github.com/" + repo + "/commit/" + commit),
		// It is still accessible via the URL;
		Public: github.Bool(false),
		Files: map[github.GistFilename]github.GistFile{
			"metadata": github.GistFile{Content: &metadata},
			"stdout":   github.GistFile{Content: &out},
		},
	}
	var err error
	if gist, _, err = s.client.Gists.Create(gist); err != nil {
		return err
	}
	log.Printf("- Gist at %s", *gist.HTMLURL)

	// https://developer.github.com/v3/repos/statuses/#create-a-status
	status := &github.RepoStatus{
		State:       github.String("success"),
		TargetURL:   gist.HTMLURL,
		Description: github.String(fmt.Sprintf("Ran: %s", strings.Join(s.c.Check, " "))),
		Context:     github.String("sci"),
	}
	if !success {
		status.State = github.String("failure")
	}
	parts := strings.SplitN(repo, "/", 2)
	_, _, err = s.client.Repositories.CreateStatus(parts[0], parts[1], commit, status)
	return err
}

func mainImpl() error {
	test := flag.String("test", "", "runs a simulation locally, specify the git repository name (not URL) to test, e.g. 'maruel/sci'")
	commit := flag.String("commit", "HEAD", "commit ID to test and update; will only update if not 'HEAD'")
	flag.Parse()
	c, err := loadConfig()
	if err != nil {
		return err
	}
	wd, err := os.Getwd()
	if err != nil {
		return err
	}
	gopath := filepath.Join(wd, "sci-gopath")
	os.Setenv("GOPATH", gopath)
	tc := oauth2.NewClient(oauth2.NoContext, oauth2.StaticTokenSource(&oauth2.Token{AccessToken: c.Oauth2AccessToken}))
	s := server{c: c, client: github.NewClient(tc), gopath: gopath, collabs: map[string]map[string]bool{}}
	if len(*test) != 0 {
		if *commit == "HEAD" {
			// Only run locally.
			metadata, out, success := runCheck(c.Check, *test, c.UseSSH, *commit, gopath)
			_, err := fmt.Printf("%s---\n%sSuccess: %t\n", metadata, out, success)
			return err

		}
		return s.runCheck(*test, *commit)
	}
	http.Handle("/", &s)
	thisFile, err := osext.Executable()
	if err != nil {
		return err
	}
	log.Printf("Running in: %s", wd)
	log.Printf("Executable: %s", thisFile)
	log.Printf("Listening on: %d", c.Port)
	ln, err := net.Listen("tcp", fmt.Sprintf(":%d", c.Port))
	if err != nil {
		return err
	}
	server := &http.Server{Addr: ln.Addr().String()}
	go server.ListenAndServe()
	err = watchFile(thisFile)
	// Ensures no task is running.
	s.mu.Lock()
	return err
}

func main() {
	if err := mainImpl(); err != nil {
		fmt.Fprintf(os.Stderr, "sci: %s.\n", err)
		os.Exit(1)
	}
}
<|endoftext|>


<commit_before>package main

import (
	"bufio"
	"flag"
	"fmt"
	"io"
	"log"
	"os"
	"strconv"
	"strings"
)

var (
	month = flag.String("m", "", "year/month in format YYYYMM")
)

type Transaction struct {
	Cost float64
	Name string
}

type Budget struct {
	Total        float64
	Remaining    float64
	Transactions []Transaction
}

func parseFile(name string) (Budget, error) {
	file, err := os.Open(name)
	if err != nil {
		return Budget{}, err
	}
	r := bufio.NewReader(file)
	b := Budget{}
	// first line is the total for the month
	line, _, err := r.ReadLine()
	if err != nil {
		return Budget{}, err
	}
	total, err := strconv.ParseFloat(string(line), 64)
	if err != nil {
		return Budget{}, err
	}
	b.Total = total
	for {
		line, _, err := r.ReadLine()
		if err == io.EOF {
			break
		}
		if err != nil {
			return Budget{}, err
		}
		t := strings.Split(string(line), ",")
		if len(t) != 2 {
			return Budget{}, fmt.Errorf("invalid line %q", line)
		}
		cost, err := strconv.ParseFloat(t[0], 64)
		if err != nil {
			return Budget{}, err
		}
		trans := Transaction{Cost: cost, Name: t[1]}

		b.Transactions = append(b.Transactions, trans)
	}

	for _, trans := range b.Transactions {
		b.Remaining = b.Total + trans.Cost
	}
	return b, nil
}

func main() {
	flag.Parse()
	b, err := parseFile(fmt.Sprintf("budget/%s.txt", *month))
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("Total:", b.Total)
	fmt.Println("Remaining:", b.Remaining)
}
<commit_msg>add -d flag to specify data directory<commit_after>package main

import (
	"bufio"
	"flag"
	"fmt"
	"io"
	"log"
	"os"
	"strconv"
	"strings"
)

var (
	dir   = flag.String("d", "budget", "budget data directory")
	month = flag.String("m", "", "year/month in format YYYYMM")
)

type Transaction struct {
	Cost float64
	Name string
}

type Budget struct {
	Total        float64
	Remaining    float64
	Transactions []Transaction
}

func parseFile(name string) (Budget, error) {
	file, err := os.Open(name)
	if err != nil {
		return Budget{}, err
	}
	r := bufio.NewReader(file)
	b := Budget{}
	// first line is the total for the month
	line, _, err := r.ReadLine()
	if err != nil {
		return Budget{}, err
	}
	total, err := strconv.ParseFloat(string(line), 64)
	if err != nil {
		return Budget{}, err
	}
	b.Total = total
	for {
		line, _, err := r.ReadLine()
		if err == io.EOF {
			break
		}
		if err != nil {
			return Budget{}, err
		}
		t := strings.Split(string(line), ",")
		if len(t) != 2 {
			return Budget{}, fmt.Errorf("invalid line %q", line)
		}
		cost, err := strconv.ParseFloat(t[0], 64)
		if err != nil {
			return Budget{}, err
		}
		trans := Transaction{Cost: cost, Name: t[1]}

		b.Transactions = append(b.Transactions, trans)
	}

	for _, trans := range b.Transactions {
		b.Remaining = b.Total + trans.Cost
	}
	return b, nil
}

func main() {
	flag.Parse()
	b, err := parseFile(fmt.Sprintf("%s/%s.txt", *dir, *month))
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("Total:", b.Total)
	fmt.Println("Remaining:", b.Remaining)
}
<|endoftext|>


<commit_before>// Force12.io is a package that monitors demand for resource in a system and then scales and repurposes
// containers, based on agreed "quality of service" contracts, to best handle that demand within the constraints of your existing VM
// or physical infrastructure (for v1).
//
// Force12 is defined to optimize the use of existing physical and VM resources instantly. VMs cannot be scaled in real time (it takes
// several minutes) and new physical machines take even longer. However, containers can be started or stopped at sub second speeds,
// allowing your infrastructure to adapt itself in real time to meet system demands.
//
// Force12 is aimed at effectively using the resources you have right now - your existing VMs or physical servers - by using them as
// optimally as possible.
//
// The Force12 approach is analogous to the way that a router dynamically optimises the use of a physical network. A router is limited
// by the capacity of the lines physically connected to it. Adding additional capacity is a physical process and takes time. Routers
// therefore make decisions in real time about which packets will be prioritized on a particular line based on the packet's priority
// (defined by a "quality of service" contract).
//
// For example, at times of high bandwidth usage a router might prioritize VOIP traffic over web browsing in real time.
//
// Containers allow Force12 to make similar "instant" judgements on service prioritisation within your existing infrastructure. Routers
// make very simplistic judgments because they have limited time and cpu and they act at a per packet level. Force12 has the capability
// of making far more sophisticated judgements, although even fairly simple ones will still provide a significant new service.
//
// This prototype is a bare bones implementation of Force12.io that recognises only 1 demand type:
// randomised demand for a priority 1 service. Resources are allocated to meet this demand for priority 1, and spare resource can
// be used for a priority 2 service.
//
// These demand type examples have been chosen purely for simplicity of demonstration. In the future more demand types
// will be offered
//
// V1 - Force12.io reacts to increased demand by starting/stopping containers on the slaves already in play.
//
// This version of Force12 starts and stops containers on a Mesos cluser using Marathon as the scheduler
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"log"
	"math"
	"net/http"
	"os"
	"time"

	"bitbucket.org/force12io/force12-scheduler/marathon"
	"bitbucket.org/force12io/force12-scheduler/scheduler"
)

type sendStatePayload struct {
	CreatedAt          int64 `json:"createdAt"`
	Priority1Requested int   `json:"priority1Requested"`
	Priority1Running   int   `json:"priority1Running"`
	Priority2Running   int   `json:"priority2Running"`
}

const const_sleep = 100     //milliseconds
const const_sendstate_sleeps = 5 // number of sleeps before we send state on the API
const const_stopsleep = 250 //milliseconds pause between stopping and restarting containers
const const_p1demandstart int = 5
const const_p2demandstart int = 4
const const_maxcontainers int = 9

type Demand struct {
	sched scheduler.Scheduler

	p1demand    int // number of Priority 1 tasks demanded
	p2demand    int
	p1requested int // indicates how many P1 tasks we've tried to kick off.
	p2requested int
}

// set returns values that were there (p1, p2)
// if provided value is -1 don't update, demand will always be between 0 and const_maxcontainers
func (d *Demand) set(p1, p2 int) (int, int) {
	//d.mu.Lock()
	p1old := d.p1demand
	p2old := d.p2demand
	if p2 != -1 {
		d.p2demand = p2
	}
	if p1 != -1 {
		d.p1demand = p1
	}
	//d.mu.Unlock()
	return p1old, p2old
}

// get returns client, server AEC - Combine this with the set to reduce code
func (d *Demand) get() (int, int) {
	return d.p1demand, d.p2demand
}

// handle processes a change in demand
// Note that handle will make any judgment on what to do with a demand
// change, including potentially nothing.
func (d *Demand) handle() error {
	var err error
	// AEC NOTE THIS FUNCTION NEEDS TO BE HEAVILY REWRITTEN TO HANDLE ECS
	// WHEN WE PORT THAT OVER TO THE SAME STRUCTURE.
	// THe reason is that all the code we wrote to handle stopping before
	// starting etc.. is handled directly by Marathon so that code
	// from the old scheduler needs to go behind the scheduler interface
	err = d.sched.StopStartNTasks(os.Getenv("CLIENT_TASK"), os.Getenv("CLIENT_FAMILY"), d.p1demand, d.p1requested)
	if err != nil {
		log.Printf("Failed to start Priority1 tasks. %v", err)
	}
	d.sched.StopStartNTasks(os.Getenv("SERVER_TASK"), os.Getenv("SERVER_FAMILY"), d.p2demand, d.p2requested)
	if err != nil {
		log.Printf("Failed to start Priority2 tasks. %v", err)
	}

	return err
}

// update checks for changes in demand, returning true if demand changed
// Note that this function makes no judgement on whether a demand change is
// significant. handle() will determine that.
func (d *Demand) update() bool {
	//log.Println("demand update check.")
	var demandchange bool = false

	container_count, err := d.sched.GetContainerCount("priority1-demand")
	if err != nil {
		log.Printf("Failed to get container count. %v", err)
		return false
	}
	//log.Printf("container count %v\n", container_count)

	//Update our saved p1 demand
	oldP1, _ := d.set(container_count, const_maxcontainers-container_count)

	//Has the demand changed?
	demandchange = (container_count != oldP1)

	if demandchange {
		log.Println("demandchange from, to ", oldP1, container_count)
	}

	return demandchange
}

// sendStateToAPI checks the current state of cluster (or single node) and sends that
// state to the f12 API
func sendStateToAPI(currentdemand *Demand) error {
	count1, count2, err := currentdemand.sched.CountAllTasks()
	if err != nil {
		return fmt.Errorf("Failed to get state err %v", err)
	}

	// Submit a PUT request to the API
	// Note the magic hardcoded string is the user ID, we need to pass this in in some way. ENV VAR?
	url := getBaseF12APIUrl() + "/metrics/" + "5k5gk"
	log.Printf("API PUT: %s", url)

	payload := sendStatePayload{
		CreatedAt:          time.Now().Unix(),
		Priority1Requested: currentdemand.p1demand,
		Priority1Running:   count1,
		Priority2Running:   count2,
	}

	w := &bytes.Buffer{}
	encoder := json.NewEncoder(w)
	err = encoder.Encode(&payload)
	if err != nil {
		return fmt.Errorf("Failed to encode API json. %v", err)
	}

	req, err := http.NewRequest("PUT", url, w)

	if err != nil {
		return fmt.Errorf("Failed to build API PUT request err %v", err)
	}
	//req.Header.Set("X-Custom-Header", "myvalue")
	req.Header.Set("Content-Type", "application/json")

	resp, err := http.DefaultClient.Do(req)
	defer resp.Body.Close()

	if err != nil {
		// handle error
		return fmt.Errorf("API state err %v", err)
	}

	if resp.StatusCode > 204 {
		return fmt.Errorf("error response from API. %s", resp.Status)
	}
	return err
}

func getBaseF12APIUrl() string {
	baseUrl := os.Getenv("API_ADDRESS")
	if baseUrl == "" {
		baseUrl = "https://force12-windtunnel.herokuapp.com"
	}
	return baseUrl
}

// For the simple prototype, Force12.io sits in a loop checking for demand changes every X milliseconds
// In phase 2 we'll add a reactive mode where appropriate.
//
// Note - we don't route messages from demandcheckers to demandhandlers using channels because we want new values
// to override old values. Queued history is of no importance here.
//
// Also for simplicity this first release is concurrency free (single threaded)
func main() {
	currentdemand := Demand{
		sched: marathon.NewScheduler(),
	}
	currentdemand.set(const_p1demandstart, const_p2demandstart)
	var demandchangeflag bool
	//uncomment code below to output logs to file, but there's nothing clever in here to limit file size
	//f, err := os.OpenFile("testlogfile", os.O_RDWR | os.O_CREATE | os.O_APPEND, 0666)
	//if err != nil {
	//   panic(err)
	//}
	//defer f.Close()

	//log.SetOutput(f)
	log.Println("This is a test log entry")

	// Initialise container types
	err := currentdemand.sched.InitScheduler(os.Getenv("CLIENT_TASK"))
	if err != nil {
		log.Printf("Failed to start P1 task. %v", err)
		return
	}
	err = currentdemand.sched.InitScheduler(os.Getenv("SERVER_TASK"))
	if err != nil {
		log.Printf("Failed to start P2 task. %v", err)
		return
	}

	var demandchangeflag bool
	demandchangeflag = currentdemand.update()
	demandchangeflag = true

	var sleepcount int = 0
	var sleep time.Duration
	sleep = const_sleep * time.Millisecond

	for {
		//Update currentdemand with latest client and server demand, if changed, set flag
		demandchangeflag = currentdemand.update()
		if demandchangeflag {
			//make any changes dictated by this new demand level
			currentdemand.p1requested, currentdemand.p2requested, err = currentdemand.sched.CountAllTasks()
			if err != nil {
				log.Printf("Failed to count tasks. %v", err)
			}
			err = currentdemand.handle()
			if err != nil {
				log.Printf("Failed to handle demand change. %v", err)
			}
		}

		time.Sleep(sleep)
		sleepcount++
		if sleepcount == const_sendstate_sleeps {
			sleepcount = 0

			//Periodically send state to the API if required
			if os.Getenv("SENDSTATETO_API") == "true" {
				err = sendStateToAPI(&currentdemand)
				if err != nil {
					log.Printf("Failed to send state. %v", err)
				}
			}
		}

	}
}
<commit_msg>First pass at using the new demand and scheduler models.  THIS WON'T WORK YET!<commit_after>// Force12.io is a package that monitors demand for resource in a system and then scales and repurposes
// containers, based on agreed "quality of service" contracts, to best handle that demand within the constraints of your existing VM
// or physical infrastructure (for v1).
//
// Force12 is defined to optimize the use of existing physical and VM resources instantly. VMs cannot be scaled in real time (it takes
// several minutes) and new physical machines take even longer. However, containers can be started or stopped at sub second speeds,
// allowing your infrastructure to adapt itself in real time to meet system demands.
//
// Force12 is aimed at effectively using the resources you have right now - your existing VMs or physical servers - by using them as
// optimally as possible.
//
// The Force12 approach is analogous to the way that a router dynamically optimises the use of a physical network. A router is limited
// by the capacity of the lines physically connected to it. Adding additional capacity is a physical process and takes time. Routers
// therefore make decisions in real time about which packets will be prioritized on a particular line based on the packet's priority
// (defined by a "quality of service" contract).
//
// For example, at times of high bandwidth usage a router might prioritize VOIP traffic over web browsing in real time.
//
// Containers allow Force12 to make similar "instant" judgements on service prioritisation within your existing infrastructure. Routers
// make very simplistic judgments because they have limited time and cpu and they act at a per packet level. Force12 has the capability
// of making far more sophisticated judgements, although even fairly simple ones will still provide a significant new service.
//
// This prototype is a bare bones implementation of Force12.io that recognises only 1 demand type:
// randomised demand for a priority 1 service. Resources are allocated to meet this demand for priority 1, and spare resource can
// be used for a priority 2 service.
//
// These demand type examples have been chosen purely for simplicity of demonstration. In the future more demand types
// will be offered
//
// V1 - Force12.io reacts to increased demand by starting/stopping containers on the slaves already in play.
//
// This version of Force12 starts and stops containers on a Mesos cluser using Marathon as the scheduler
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"log"
	"math"
	"net/http"
	"os"
	"time"

	"bitbucket.org/force12io/force12-scheduler/compose"
	"bitbucket.org/force12io/force12-scheduler/consul"
	"bitbucket.org/force12io/force12-scheduler/demand"
	"bitbucket.org/force12io/force12-scheduler/marathon"
	"bitbucket.org/force12io/force12-scheduler/rng"
	"bitbucket.org/force12io/force12-scheduler/scheduler"
)

type sendStatePayload struct {
	CreatedAt          int64 `json:"createdAt"`
	Priority1Requested int   `json:"priority1Requested"`
	Priority1Running   int   `json:"priority1Running"`
	Priority2Running   int   `json:"priority2Running"`
}

const const_sleep = 100          //milliseconds
const const_sendstate_sleeps = 5 // number of sleeps before we send state on the API
const const_stopsleep = 250      //milliseconds pause between stopping and restarting containers
const const_p1demandstart int = 5
const const_p2demandstart int = 4
const const_maxcontainers int = 9

type Demand struct {
	sched scheduler.Scheduler

	p1demand    int // number of Priority 1 tasks demanded
	p2demand    int
	p1requested int // indicates how many P1 tasks we've tried to kick off.
	p2requested int
}

// set returns values that were there (p1, p2)
// if provided value is -1 don't update, demand will always be between 0 and const_maxcontainers
func (d *Demand) set(p1, p2 int) (int, int) {
	//d.mu.Lock()
	p1old := d.p1demand
	p2old := d.p2demand
	if p2 != -1 {
		d.p2demand = p2
	}
	if p1 != -1 {
		d.p1demand = p1
	}
	//d.mu.Unlock()
	return p1old, p2old
}

// get returns client, server AEC - Combine this with the set to reduce code
func (d *Demand) get() (int, int) {
	return d.p1demand, d.p2demand
}

// handle processes a change in demand
// Note that handle will make any judgment on what to do with a demand
// change, including potentially nothing.
func (d *Demand) handle() error {
	var err error
	// AEC NOTE THIS FUNCTION NEEDS TO BE HEAVILY REWRITTEN TO HANDLE ECS
	// WHEN WE PORT THAT OVER TO THE SAME STRUCTURE.
	// THe reason is that all the code we wrote to handle stopping before
	// starting etc.. is handled directly by Marathon so that code
	// from the old scheduler needs to go behind the scheduler interface
	err = d.sched.StopStartNTasks(os.Getenv("CLIENT_TASK"), os.Getenv("CLIENT_FAMILY"), d.p1demand, d.p1requested)
	if err != nil {
		log.Printf("Failed to start Priority1 tasks. %v", err)
	}
	d.sched.StopStartNTasks(os.Getenv("SERVER_TASK"), os.Getenv("SERVER_FAMILY"), d.p2demand, d.p2requested)
	if err != nil {
		log.Printf("Failed to start Priority2 tasks. %v", err)
	}

	return err
}

// update checks for changes in demand, returning true if demand changed
// Note that this function makes no judgement on whether a demand change is
// significant. handle() will determine that.
func (d *Demand) update() bool {
	//log.Println("demand update check.")
	var demandchange bool = false

	container_count, err := d.sched.GetContainerCount("priority1-demand")
	if err != nil {
		log.Printf("Failed to get container count. %v", err)
		return false
	}
	//log.Printf("container count %v\n", container_count)

	//Update our saved p1 demand
	oldP1, _ := d.set(container_count, const_maxcontainers-container_count)

	//Has the demand changed?
	demandchange = (container_count != oldP1)

	if demandchange {
		log.Println("demandchange from, to ", oldP1, container_count)
	}

	return demandchange
}

// sendStateToAPI checks the current state of cluster (or single node) and sends that
// state to the f12 API
func sendStateToAPI(currentdemand *Demand) error {
	count1, count2, err := currentdemand.sched.CountAllTasks()
	if err != nil {
		return fmt.Errorf("Failed to get state err %v", err)
	}

	// Submit a PUT request to the API
	// Note the magic hardcoded string is the user ID, we need to pass this in in some way. ENV VAR?
	url := getBaseF12APIUrl() + "/metrics/" + "5k5gk"
	log.Printf("API PUT: %s", url)

	payload := sendStatePayload{
		CreatedAt:          time.Now().Unix(),
		Priority1Requested: currentdemand.p1demand,
		Priority1Running:   count1,
		Priority2Running:   count2,
	}

	w := &bytes.Buffer{}
	encoder := json.NewEncoder(w)
	err = encoder.Encode(&payload)
	if err != nil {
		return fmt.Errorf("Failed to encode API json. %v", err)
	}

	req, err := http.NewRequest("PUT", url, w)

	if err != nil {
		return fmt.Errorf("Failed to build API PUT request err %v", err)
	}
	//req.Header.Set("X-Custom-Header", "myvalue")
	req.Header.Set("Content-Type", "application/json")

	resp, err := http.DefaultClient.Do(req)
	defer resp.Body.Close()

	if err != nil {
		// handle error
		return fmt.Errorf("API state err %v", err)
	}

	if resp.StatusCode > 204 {
		return fmt.Errorf("error response from API. %s", resp.Status)
	}
	return err
}

func getBaseF12APIUrl() string {
	baseUrl := os.Getenv("API_ADDRESS")
	if baseUrl == "" {
		baseUrl = "https://force12-windtunnel.herokuapp.com"
	}
	return baseUrl
}

func getEnvOrDefault(name string, defaultValue string) string {
	v := os.Getenv(name)
	if v == "" {
		v = defaultValue
	}

	return v
}

// For the simple prototype, Force12.io sits in a loop checking for demand changes every X milliseconds
// In phase 2 we'll add a reactive mode where appropriate.
//
// Note - we don't route messages from demandcheckers to demandhandlers using channels because we want new values
// to override old values. Queued history is of no importance here.
//
// Also for simplicity this first release is concurrency free (single threaded)
func main() {
	var err error
	// TODO! Make it so you can send in settings on the command line
	var demandModelType string = getEnvOrDefault("F12_DEMAND_MODEL", "RNG")
	var schedulerType string = getEnvOrDefault("F12_SCHEDULER", "COMPOSE")

	var di demand.Input
	var s scheduler.Scheduler

	switch demandModelType {
	case "CONSUL":
		log.Println("Getting demand metric from Consul")
		di = consul.NewDemandModel()
	case "RNG":
		log.Println("Random demand generation")
		di = rng.NewDemandModel()
	default:
		err = fmt.Errorf("Bad value for F12_DEMAND_MODEL: %s", demandModelType)
		return err
	}

	switch schedulerType {
	case "MESOS":
		log.Println("Scheduling with Mesos / Marathon")
		s = marathon.NewScheduler()
	case "COMPOSE":
		log.Println("Scheduling with Docker compose")
		s = compose.NewScheduler()
	default:
		err = fmt.Errorf("Bad value for F12_SCHEDULER: %s", schedulerType)
		return err
	}

	// TODO!! Remove this - it's just to get things compiling
	log.Println(di)

	currentdemand := Demand{
		sched: s,
	}
	currentdemand.set(const_p1demandstart, const_p2demandstart)
	var demandchangeflag bool
	//uncomment code below to output logs to file, but there's nothing clever in here to limit file size
	//f, err := os.OpenFile("testlogfile", os.O_RDWR | os.O_CREATE | os.O_APPEND, 0666)
	//if err != nil {
	//   panic(err)
	//}
	//defer f.Close()

	//log.SetOutput(f)
	log.Println("This is a test log entry")

	// Initialise container types
	err := currentdemand.sched.InitScheduler(os.Getenv("CLIENT_TASK"))
	if err != nil {
		log.Printf("Failed to start P1 task. %v", err)
		return
	}
	err = currentdemand.sched.InitScheduler(os.Getenv("SERVER_TASK"))
	if err != nil {
		log.Printf("Failed to start P2 task. %v", err)
		return
	}

	var demandchangeflag bool
	demandchangeflag = currentdemand.update()
	demandchangeflag = true

	var sleepcount int = 0
	var sleep time.Duration
	sleep = const_sleep * time.Millisecond

	for {
		//Update currentdemand with latest client and server demand, if changed, set flag
		demandchangeflag = currentdemand.update()
		if demandchangeflag {
			//make any changes dictated by this new demand level
			currentdemand.p1requested, currentdemand.p2requested, err = currentdemand.sched.CountAllTasks()
			if err != nil {
				log.Printf("Failed to count tasks. %v", err)
			}
			err = currentdemand.handle()
			if err != nil {
				log.Printf("Failed to handle demand change. %v", err)
			}
		}

		time.Sleep(sleep)
		sleepcount++
		if sleepcount == const_sendstate_sleeps {
			sleepcount = 0

			//Periodically send state to the API if required
			if os.Getenv("SENDSTATETO_API") == "true" {
				err = sendStateToAPI(&currentdemand)
				if err != nil {
					log.Printf("Failed to send state. %v", err)
				}
			}
		}

	}
}
<|endoftext|>


<commit_before>package main

import (
	"database/sql"
	"errors"
	"flag"
	"fmt"
	"log"
	"os"
	"regexp"

	"github.com/Chiliec/golos-go/client"
	"gopkg.in/telegram-bot-api.v4"

	"github.com/Chiliec/golos-vote-bot/db"
	"github.com/Chiliec/golos-vote-bot/models"
)

var (
	postingKey string
	database   *sql.DB
)

const (
	rpc   = "wss://ws.golos.io"
	chain = "golos"
)

func init() {
	flag.StringVar(&postingKey, "postingKey", "", "posting key")
	flag.Parse()

	database = db.InitDB("./db/database.db")
}

func main() {
	token := os.Getenv("TELEGRAM_TOKEN")
	if token == "" {
		log.Panic(errors.New("Нет токена"))
	}
	bot, err := tgbotapi.NewBotAPI(token)
	if err != nil {
		log.Panic(err)
	}

	bot.Debug = true

	log.Printf("Authorized on account %s", bot.Self.UserName)

	u := tgbotapi.NewUpdate(0)
	u.Timeout = 60

	updates, err := bot.GetUpdatesChan(u)
	if err != nil {
		log.Panic(err)
	}
	for update := range updates {
		err := processMessage(bot, update)
		if err != nil {
			log.Println(err)
		}
	}
}

func processMessage(bot *tgbotapi.BotAPI, update tgbotapi.Update) error {
	log.Printf("[%s] %s", update.Message.From.UserName, update.Message.Text)
	if update.Message != nil {
		userMessageText := update.Message.Text
		msg := tgbotapi.NewMessage(update.Message.Chat.ID, update.Message.Text)
		if update.Message.IsCommand() {
			switch update.Message.Command() {
			case "start":
				keyButton := tgbotapi.NewKeyboardButton("🔑 Ключница")
				aboutButton := tgbotapi.NewKeyboardButton("🐞 О боте")
				buttons := []tgbotapi.KeyboardButton{keyButton, aboutButton}
				keyboard := tgbotapi.NewReplyKeyboard(buttons)
				msg.ReplyMarkup = keyboard
			}
		}

		regexp, err := regexp.Compile("https://golos.io/([-a-zA-Z0-9@:%_+.~#?&//=]{2,256})/@([-a-zA-Z0-9]{2,256})/([-a-zA-Z0-9@:%_+.~#?&=]{2,256})")
		if err != nil {
			return err
		}
		if regexp.MatchString(userMessageText) {
			matched := regexp.FindStringSubmatch(userMessageText)
			log.Println(matched)
			author, permalink := matched[2], matched[3]
			voter := "chiliec"
			percent := 65
			voteModel := models.Vote{
				UserID:    update.Message.From.ID,
				Voter:     voter,
				Author:    author,
				Permalink: permalink,
				Percent:   percent,
			}
			msg.ReplyToMessageID = update.Message.MessageID
			err := vote(voteModel)
			if err != nil {
				switch err.(type) {
				case *ErrorAlreadyVoted:
					msg.Text = "Уже голосовал за этот пост!"
				default:
					msg.Text = "Не смог прогосовать, попробуйте ещё раз"
				}
			} else {
				msg.Text = fmt.Sprintf("Проголосовал с силой %d%%", percent)
			}
		}
		bot.Send(msg)
	}
	return nil
}

func vote(model models.Vote) error {
	exists := model.Exists(database)
	if exists {
		return NewErrorAlreadyVoted("Уже проголосовали!")
	}
	weight := model.Percent * 100
	client.Key_List = map[string]client.Keys{model.Voter: client.Keys{postingKey, "", "", ""}}
	api := client.NewApi(rpc, chain)
	err := api.Vote(model.Voter, model.Author, model.Permalink, weight)
	if err != nil {
		return err
	}
	_, err = model.Save(database)
	if err != nil {
		return err
	}
	return nil
}

type ErrorAlreadyVoted struct {
	message string
}

func NewErrorAlreadyVoted(message string) *ErrorAlreadyVoted {
	return &ErrorAlreadyVoted{
		message: message,
	}
}
func (e *ErrorAlreadyVoted) Error() string {
	return e.message
}
<commit_msg>Processing commands and save login and private key to memory<commit_after>package main

import (
	"database/sql"
	"errors"
	"flag"
	"fmt"
	"log"
	"os"
	"regexp"

	"github.com/Chiliec/golos-go/client"
	"gopkg.in/telegram-bot-api.v4"

	"github.com/Chiliec/golos-vote-bot/db"
	"github.com/Chiliec/golos-vote-bot/models"
)

var (
	postingKey string
	database   *sql.DB
	logins     map[int]string
)

const (
	rpc   = "wss://ws.golos.io"
	chain = "golos"

	keyButtonText   = "🔑 Ключница"
	aboutButtonText = "🐞 О боте"
)

func init() {
	flag.StringVar(&postingKey, "postingKey", "", "posting key")
	flag.Parse()

	database = db.InitDB("./db/database.db")
	logins = map[int]string{}
}

func main() {
	token := os.Getenv("TELEGRAM_TOKEN")
	if token == "" {
		log.Panic(errors.New("Нет токена"))
	}
	bot, err := tgbotapi.NewBotAPI(token)
	if err != nil {
		log.Panic(err)
	}

	bot.Debug = true

	log.Printf("Authorized on account %s", bot.Self.UserName)

	u := tgbotapi.NewUpdate(0)
	u.Timeout = 60

	updates, err := bot.GetUpdatesChan(u)
	if err != nil {
		log.Panic(err)
	}
	for update := range updates {
		err := processMessage(bot, update)
		if err != nil {
			log.Println(err)
		}
	}
}

func processMessage(bot *tgbotapi.BotAPI, update tgbotapi.Update) error {
	log.Printf("[%s] %s", update.Message.From.UserName, "")
	if update.Message != nil {
		regexp, err := regexp.Compile("https://golos.io/([-a-zA-Z0-9@:%_+.~#?&//=]{2,256})/@([-a-zA-Z0-9]{2,256})/([-a-zA-Z0-9@:%_+.~#?&=]{2,256})")
		if err != nil {
			return err
		}
		msg := tgbotapi.NewMessage(update.Message.Chat.ID, update.Message.Text)
		if update.Message.IsCommand() {
			switch update.Message.Command() {
			case "start":
				keyButton := tgbotapi.NewKeyboardButton(keyButtonText)
				aboutButton := tgbotapi.NewKeyboardButton(aboutButtonText)
				buttons := []tgbotapi.KeyboardButton{keyButton, aboutButton}
				keyboard := tgbotapi.NewReplyKeyboard(buttons)
				msg.ReplyMarkup = keyboard
			}
		} else if update.Message.Text == keyButtonText {
			msg.Text = "Введите логин на Голосе"
			setWaitLogin(update.Message.From.ID)
		} else if update.Message.Text == aboutButtonText {
			msg.Text = "Бот для блого-социальной сети на блокчейне \"Голос\"\n" +
				"Нет времени голосовать, но хочется зарабатывать? Добавьте приватный постинг ключ и мы распорядимся вашей Силой голоса наилучшим образом!\n" +
				"Автор: @babin"
		} else if regexp.MatchString(update.Message.Text) {
			matched := regexp.FindStringSubmatch(update.Message.Text)
			log.Println(matched)
			author, permalink := matched[2], matched[3]
			voter := "chiliec"
			percent := 65
			voteModel := models.Vote{
				UserID:    update.Message.From.ID,
				Voter:     voter,
				Author:    author,
				Permalink: permalink,
				Percent:   percent,
			}
			msg.ReplyToMessageID = update.Message.MessageID
			err := vote(voteModel)
			if err != nil {
				switch err.(type) {
				case *ErrorAlreadyVoted:
					msg.Text = "Уже голосовал за этот пост!"
				default:
					msg.Text = "Не смог прогосовать, попробуйте ещё раз"
				}
			} else {
				msg.Text = fmt.Sprintf("Проголосовал с силой %d%%", percent)
			}
		} else if wait, login := isWaitingKey(update.Message.From.ID); wait && login == "" {
			msg.Text = "Введите приватный ключ"
			setWaitKey(update.Message.From.ID, update.Message.Text)
		} else if wait, login := isWaitingKey(update.Message.From.ID); wait && login != "" {
			log.Println("Сейчас нужно сохранить логин и приватный ключ!")
			forgetLogin(update.Message.From.ID)
		} else {
			msg.Text = "Команда не распознана"
		}
		bot.Send(msg)
	}
	return nil
}

func forgetLogin(userID int) {
	delete(logins, userID)
}

func setWaitLogin(userID int) {
	logins[userID] = ""
}

func setWaitKey(userID int, login string) {
	logins[userID] = login
}

func isWaitingKey(userID int) (bool, string) {
	for id, login := range logins {
		if userID == id {
			return true, login
		}
	}
	return false, ""
}

func vote(model models.Vote) error {
	exists := model.Exists(database)
	if exists {
		return NewErrorAlreadyVoted("Уже проголосовали!")
	}
	weight := model.Percent * 100
	client.Key_List = map[string]client.Keys{model.Voter: client.Keys{postingKey, "", "", ""}}
	api := client.NewApi(rpc, chain)
	err := api.Vote(model.Voter, model.Author, model.Permalink, weight)
	if err != nil {
		return err
	}
	_, err = model.Save(database)
	if err != nil {
		return err
	}
	return nil
}

type ErrorAlreadyVoted struct {
	message string
}

func NewErrorAlreadyVoted(message string) *ErrorAlreadyVoted {
	return &ErrorAlreadyVoted{
		message: message,
	}
}
func (e *ErrorAlreadyVoted) Error() string {
	return e.message
}
<|endoftext|>


<commit_before>package main

import (
	"fmt"
	"io/ioutil"
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
)

const space = "  " // Used for pretty printing
const br = "\n\n"

func main() {
	var command string

	usage := "binpath command [arguments]"
	options := "-list, -ls"
	optionsDesc := "list directory contents of bin path"

	// Create help message with usage messaging
	helpMessage := fmt.Sprintf(bold("USAGE:")+"\n%s%v", space, usage)
	// Break between messages
	helpMessage += br
	// Add options messaging
	helpMessage += fmt.Sprintf(bold("OPTIONS:")+"\n%v\n%s%v", options, space, optionsDesc)

	// Check arg for appname to load
	if len(os.Args) > 1 {
		command = os.Args[1]
	} else {
		fmt.Println(helpMessage)
		os.Exit(1)
	}

	// Look for help flag before executing command
	switch command {
	case "--help":
		fallthrough
	case "-help":
		fallthrough
	case "--h":
		fallthrough
	case "-h":
		fmt.Println(helpMessage)
		os.Exit(1)
	case "--show-bash-completion":
		checkForBin(command, true)
	default:
		checkForBin(command, false)
	}

}

func checkForBin(command string, showBashCompletion bool) {
	dirname := "." + string(filepath.Separator)

	d, err := os.Open(dirname)
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
	defer d.Close()

	files, err := d.Readdir(-1)
	if err != nil {
		fmt.Println(err)
		log.Fatal(err)
	}

	for _, file := range files {
		if file.Mode().IsDir() {
			if file.Name() == "bin" {
				dir, err := filepath.Abs(filepath.Dir(os.Args[0]))
				if err != nil {
					log.Fatal(err)
				}

				// Set bin path
				binPath := filepath.Join(dir, file.Name())

				// If --show-bash-completion flag was used then list out files in binPath
				if showBashCompletion == true {
					// List files in bin folder
					listBinPathFiles(binPath)
				} else {
					// Execute command in binPath
					executeCommand(binPath, command)
				}

				os.Exit(0)
			}
		}
	}

	// If it doesn't move up a directory and test
	os.Chdir(".." + string(filepath.Separator))

	checkForBin(command, showBashCompletion)
}

// Print out files found in binpath
func listBinPathFiles(binPath string) {
	files, err := ioutil.ReadDir(binPath)
	if err != nil {
		fmt.Printf("-binpath: error reading: %v \n", binPath)
		os.Exit(1)
	}

	for _, file := range files {
		name := file.Name()

		// If it's a hidden file don't print it
		if !strings.HasPrefix(name, ".") {
			fmt.Println(name)
		}
	}
}

// Try to execute the command if a binpath is found
func executeCommand(binPath string, command string) {
	// Set path to command
	commandPath := filepath.Join(binPath, command)

	// Make to see if command exist in bin path
	_, err := os.Stat(commandPath)
	if err != nil {
		fmt.Printf("-binpath: %v: command not found in: %v \n", command, binPath)
		os.Exit(1)
	}

	// Create separator for easier reading
	separator := "-----------------------"
	fmt.Println(separator)

	// Notify if a bin is found
	fmt.Printf("Exec command: %v\n", commandPath)

	// Print separator for separation of binpath and command
	fmt.Println(separator)

	// And binPath to $PATH env
	v := fmt.Sprintf("$PATH:%v", binPath)
	os.Setenv("PATH", v)

	// Make sure to pass any arguments to the app
	args := os.Args[2:len(os.Args)]

	// Call command
	cmd := exec.Command(command, args...)
	cmd.Stdin = os.Stdin
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	err = cmd.Run()
	if _, ok := err.(*exec.ExitError); ok {
		log.Fatal(err)
	}
}

func bold(s string) string {
	return "\033[1m" + s + "\033[0m"
}
<commit_msg>Updating --show-bash-completion flag to be -ls, --ls, -list, --list for easier usage. Updating comments.<commit_after>package main

import (
	"fmt"
	"io/ioutil"
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
)

const space = "  " // Used for pretty printing
const br = "\n\n"

func main() {
	var command string

	usage := "binpath command [arguments]"
	options := "-list, -ls"
	optionsDesc := "list directory contents of bin path"

	// Create help message with usage messaging
	helpMessage := fmt.Sprintf(bold("USAGE:")+"\n%s%v", space, usage)
	// Break between messages
	helpMessage += br
	// Add options messaging
	helpMessage += fmt.Sprintf(bold("OPTIONS:")+"\n%v\n%s%v", options, space, optionsDesc)

	// Check arg for appname to load
	if len(os.Args) > 1 {
		command = os.Args[1]
	} else {
		fmt.Println(helpMessage)
		os.Exit(1)
	}

	// Look for help flag before executing command
	switch command {
	case "--help":
		fallthrough
	case "-help":
		fallthrough
	case "--h":
		fallthrough
	case "-h":
		fmt.Println(helpMessage)
		os.Exit(1)
	case "-ls":
		fallthrough
	case "--ls":
		fallthrough
	case "-list":
		fallthrough
	case "--list":
		checkForBin(command, true)
	default:
		checkForBin(command, false)
	}

}

// Searches current folder then searches up until it finds a /bin folder
// If a /bin is found it tries to execute a command or list the contents
func checkForBin(command string, list bool) {
	dirname := "." + string(filepath.Separator)

	d, err := os.Open(dirname)
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
	defer d.Close()

	files, err := d.Readdir(-1)
	if err != nil {
		fmt.Println(err)
		log.Fatal(err)
	}

	for _, file := range files {
		if file.Mode().IsDir() {
			if file.Name() == "bin" {
				dir, err := filepath.Abs(filepath.Dir(os.Args[0]))
				if err != nil {
					log.Fatal(err)
				}

				// Set bin path
				binPath := filepath.Join(dir, file.Name())

				// If --list flag was used then list out files in binPath
				if list == true {
					// List files in bin folder
					listBinPathFiles(binPath)
				} else {
					// Execute command in binPath
					executeCommand(binPath, command)
				}

				os.Exit(0)
			}
		}
	}

	// If it doesn't move up a directory and test
	os.Chdir(".." + string(filepath.Separator))

	// bin folder not found. Keep checking
	checkForBin(command, list)
}

// Print out files found in binpath
func listBinPathFiles(binPath string) {
	files, err := ioutil.ReadDir(binPath)
	if err != nil {
		fmt.Printf("-binpath: error reading: %v \n", binPath)
		os.Exit(1)
	}

	for _, file := range files {
		name := file.Name()

		// If it's a hidden file don't print it
		if !strings.HasPrefix(name, ".") {
			fmt.Println(name)
		}
	}
}

// Try to execute the command if a binpath is found
func executeCommand(binPath string, command string) {
	// Set path to command
	commandPath := filepath.Join(binPath, command)

	// Make to see if command exist in bin path
	_, err := os.Stat(commandPath)
	if err != nil {
		fmt.Printf("-binpath: %v: command not found in: %v \n", command, binPath)
		os.Exit(1)
	}

	// Create separator for easier reading
	separator := "-----------------------"
	fmt.Println(separator)

	// Notify if a bin is found
	fmt.Printf("Exec command: %v\n", commandPath)

	// Print separator for separation of binpath and command
	fmt.Println(separator)

	// And binPath to $PATH env
	v := fmt.Sprintf("$PATH:%v", binPath)
	os.Setenv("PATH", v)

	// Make sure to pass any arguments to the app
	args := os.Args[2:len(os.Args)]

	// Call command
	cmd := exec.Command(command, args...)
	cmd.Stdin = os.Stdin
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	err = cmd.Run()
	if _, ok := err.(*exec.ExitError); ok {
		log.Fatal(err)
	}
}

func bold(s string) string {
	return "\033[1m" + s + "\033[0m"
}
<|endoftext|>


<commit_before>package main

import (
	"flag"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"strconv"
	"strings"

	adrianConfig "github.com/daveross/adrian/config"
	adrianFonts "github.com/daveross/adrian/fonts"

	"github.com/labstack/echo"
)

func main() {

	versionParam := flag.Bool("version", false, "display the version number and exit")
	configParam := flag.String("config", "", "specify a config file")
	flag.Parse()

	// Handle the --version parameter
	if *versionParam {
		fmt.Printf("%s\n", "2.2.0")
		os.Exit(0)
	}

	log.Println("Starting Adrian 2.1.1")
	log.Println("Loading adrian.yaml")
	var config adrianConfig.Config
	if *configParam != "" {
		config = adrianConfig.LoadConfig(*configParam)
	} else {
		config = adrianConfig.LoadConfig("./adrian.yaml")
	}
	log.Println("Initializing web server")
	e := Instantiate(config)
	log.Println("Loading fonts and starting watchers")
	for _, folder := range config.Global.Directories {
		adrianFonts.FindFonts(folder, config)
		adrianFonts.InstantiateWatcher(folder, config)
	}
	log.Println("Defining paths")

	e.GET("/css/", func(c echo.Context) error {
		c.Response().Header().Set(echo.HeaderContentType, "text/css")
		fontRequests := strings.Split(c.QueryParam("family"), "|")
		display := c.QueryParam("display")
		var fontsCSS string
		for _, fontRequest := range fontRequests {
			fontRequestData := strings.SplitN(fontRequest, ":", 2)
			fontFamilyName := fontRequestData[0]
			var fontWeights []int
			if len(fontRequestData) > 1 {
				for _, weight := range strings.Split(fontRequestData[1], ",") {
					numericWeight, err := strconv.Atoi(weight)
					if err == nil {
						fontWeights = append(fontWeights, numericWeight)
					}
				}
				fontWeights = uniqueInts(fontWeights)
			}
			fontData, err := adrianFonts.GetFont(fontFamilyName)
			if err != nil {
				return return404(c)
			}
			fontsCSS = fontsCSS + adrianFonts.FontFaceCSS(fontData, fontWeights, display)
		}
		writeToCache(c, fontsCSS)
		return c.String(http.StatusOK, fontsCSS)
	})

	e.GET("/font/:filename/", func(c echo.Context) error {
		filename, error := url.QueryUnescape(c.Param("filename"))
		if error != nil {
			return return404(c)
		}

		switch filepath.Ext(filename) {
		case ".ttf":
			return outputFont(c, "font/truetype")
		case ".woff":
			return outputFont(c, "font/woff")
		case ".woff2":
			return outputFont(c, "font/woff2")
		case ".otf":
			return outputFont(c, "font/opentype")
		}

		return return404(c)
	})

	log.Printf("Listening on port %d", config.Global.Port)
	e.Logger.Fatal(e.Start(fmt.Sprintf(":%d", config.Global.Port)))
}

// Basename gets the base filename (minus the last extension)
func basename(s string) string {
	n := strings.LastIndexByte(s, '.')
	if n >= 0 {
		return s[:n]
	}
	return s
}

func outputFont(c echo.Context, mimeType string) error {
	filename, error := url.QueryUnescape(c.Param("filename"))
	if error != nil {
		return return404(c)
	}
	fontVariant, err := adrianFonts.GetFontVariantByUniqueID(basename(filename))
	if err != nil {
		return return404(c)
	}

	fontFileData, ok := fontVariant.Files[adrianFonts.GetCanonicalExtension(filename)]
	if !ok {
		log.Fatal("Invalid font format" + adrianFonts.GetCanonicalExtension(filename))
	}

	for i := range c.Request().Header["If-None-Match"] {
		individualHashes := strings.Split(c.Request().Header["If-None-Match"][i], (", "))
		for j := range individualHashes {
			if individualHashes[j] == fontFileData.MD5 {
				status := make(map[string]string)
				status["message"] = "Not Modified"
				return c.JSON(http.StatusNotModified, status)
			}
		}
	}

	fontBinary, err := ioutil.ReadFile(fontFileData.Path) // just pass the file name
	if err != nil {
		log.Fatal("Can't read font file " + fontFileData.FileName)
	}

	c.Response().Header().Set("Content-Transfer-Encoding", "binary")
	c.Response().Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
	c.Response().Header().Set("ETag", fontFileData.MD5)
	return c.Blob(http.StatusOK, mimeType, fontBinary)

}

// uniqueInts returns a unique subset of the int slice provided.
func uniqueInts(input []int) []int {
	u := make([]int, 0, len(input))
	m := make(map[int]bool)
	for _, val := range input {
		if _, ok := m[val]; !ok {
			m[val] = true
			u = append(u, val)
		}
	}
	return u
}
<commit_msg>Updated version number<commit_after>package main

import (
	"flag"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"strconv"
	"strings"

	adrianConfig "github.com/daveross/adrian/config"
	adrianFonts "github.com/daveross/adrian/fonts"

	"github.com/labstack/echo"
)

func main() {

	versionParam := flag.Bool("version", false, "display the version number and exit")
	configParam := flag.String("config", "", "specify a config file")
	flag.Parse()

	// Handle the --version parameter
	if *versionParam {
		fmt.Printf("%s\n", "2.2.1")
		os.Exit(0)
	}

	log.Println("Starting Adrian 2.1.1")
	log.Println("Loading adrian.yaml")
	var config adrianConfig.Config
	if *configParam != "" {
		config = adrianConfig.LoadConfig(*configParam)
	} else {
		config = adrianConfig.LoadConfig("./adrian.yaml")
	}
	log.Println("Initializing web server")
	e := Instantiate(config)
	log.Println("Loading fonts and starting watchers")
	for _, folder := range config.Global.Directories {
		adrianFonts.FindFonts(folder, config)
		adrianFonts.InstantiateWatcher(folder, config)
	}
	log.Println("Defining paths")

	e.GET("/css/", func(c echo.Context) error {
		c.Response().Header().Set(echo.HeaderContentType, "text/css")
		fontRequests := strings.Split(c.QueryParam("family"), "|")
		display := c.QueryParam("display")
		var fontsCSS string
		for _, fontRequest := range fontRequests {
			fontRequestData := strings.SplitN(fontRequest, ":", 2)
			fontFamilyName := fontRequestData[0]
			var fontWeights []int
			if len(fontRequestData) > 1 {
				for _, weight := range strings.Split(fontRequestData[1], ",") {
					numericWeight, err := strconv.Atoi(weight)
					if err == nil {
						fontWeights = append(fontWeights, numericWeight)
					}
				}
				fontWeights = uniqueInts(fontWeights)
			}
			fontData, err := adrianFonts.GetFont(fontFamilyName)
			if err != nil {
				return return404(c)
			}
			fontsCSS = fontsCSS + adrianFonts.FontFaceCSS(fontData, fontWeights, display)
		}
		writeToCache(c, fontsCSS)
		return c.String(http.StatusOK, fontsCSS)
	})

	e.GET("/font/:filename/", func(c echo.Context) error {
		filename, error := url.QueryUnescape(c.Param("filename"))
		if error != nil {
			return return404(c)
		}

		switch filepath.Ext(filename) {
		case ".ttf":
			return outputFont(c, "font/truetype")
		case ".woff":
			return outputFont(c, "font/woff")
		case ".woff2":
			return outputFont(c, "font/woff2")
		case ".otf":
			return outputFont(c, "font/opentype")
		}

		return return404(c)
	})

	log.Printf("Listening on port %d", config.Global.Port)
	e.Logger.Fatal(e.Start(fmt.Sprintf(":%d", config.Global.Port)))
}

// Basename gets the base filename (minus the last extension)
func basename(s string) string {
	n := strings.LastIndexByte(s, '.')
	if n >= 0 {
		return s[:n]
	}
	return s
}

func outputFont(c echo.Context, mimeType string) error {
	filename, error := url.QueryUnescape(c.Param("filename"))
	if error != nil {
		return return404(c)
	}
	fontVariant, err := adrianFonts.GetFontVariantByUniqueID(basename(filename))
	if err != nil {
		return return404(c)
	}

	fontFileData, ok := fontVariant.Files[adrianFonts.GetCanonicalExtension(filename)]
	if !ok {
		log.Fatal("Invalid font format" + adrianFonts.GetCanonicalExtension(filename))
	}

	for i := range c.Request().Header["If-None-Match"] {
		individualHashes := strings.Split(c.Request().Header["If-None-Match"][i], (", "))
		for j := range individualHashes {
			if individualHashes[j] == fontFileData.MD5 {
				status := make(map[string]string)
				status["message"] = "Not Modified"
				return c.JSON(http.StatusNotModified, status)
			}
		}
	}

	fontBinary, err := ioutil.ReadFile(fontFileData.Path) // just pass the file name
	if err != nil {
		log.Fatal("Can't read font file " + fontFileData.FileName)
	}

	c.Response().Header().Set("Content-Transfer-Encoding", "binary")
	c.Response().Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
	c.Response().Header().Set("ETag", fontFileData.MD5)
	return c.Blob(http.StatusOK, mimeType, fontBinary)

}

// uniqueInts returns a unique subset of the int slice provided.
func uniqueInts(input []int) []int {
	u := make([]int, 0, len(input))
	m := make(map[int]bool)
	for _, val := range input {
		if _, ok := m[val]; !ok {
			m[val] = true
			u = append(u, val)
		}
	}
	return u
}
<|endoftext|>