<commit_before>package bitbucket

import (
	"errors"

	"github.com/mitchellh/mapstructure"
)

type Workspace struct {
	c *Client

	Repositories *Repositories
	Permissions  *Permission

	UUID       string
	Type       string
	Slug       string
	Is_Private bool
	Name       string
	workspace
}

type WorkspaceList struct {
	Page       int
	Pagelen    int
	MaxDepth   int
	Size       int
	Next       string
	Workspaces []Workspace
}

type Permission struct {
	c *Client

	Type string
}

type ProjectsRes struct {
	Page     int32
	Pagelen  int32
	MaxDepth int32
	Size     int32
	Items    []Project
}

type WorkspaceMembers struct {
	Page    int
	Pagelen int
	Size    int
	Members []User
}

func (t *Permission) GetUserPermissions(organization, member string) (*Permission, error) {
	urlStr := t.c.requestUrl("/workspaces/%s/permissions?q=user.nickname=\"%s\"", organization, member)
	response, err := t.c.executePaginated("GET", urlStr, "")
	if err != nil {
		return nil, err
	}

	return decodePermission(response), err
}

func (t *Permission) GetUserPermissionsByUuid(organization, member string) (*Permission, error) {
	urlStr := t.c.requestUrl("/workspaces/%s/permissions?q=user.uuid=\"%s\"", organization, member)
	response, err := t.c.executePaginated("GET", urlStr, "")
	if err != nil {
		return nil, err
	}

	return decodePermission(response), err
}

func (t *Workspace) List() (*WorkspaceList, error) {
	urlStr := t.c.requestUrl("/workspaces")
	response, err := t.c.executePaginated("GET", urlStr, "")
	if err != nil {
		return nil, err
	}

	return decodeWorkspaceList(response)
}

func (t *Workspace) Get(workspace string) (*Workspace, error) {
	urlStr := t.c.requestUrl("/workspaces/%s", workspace)
	response, err := t.c.execute("GET", urlStr, "")
	if err != nil {
		return nil, err
	}

	return decodeWorkspace(response)
}

func (w *Workspace) Members(teamname string) (*WorkspaceMembers, error) {
	urlStr := w.c.requestUrl("/workspaces/%s/members", teamname)
	response, err := w.c.execute("GET", urlStr, "")
	if err != nil {
		return nil, err
	}

	return decodeMembers(response)
}

func (w *Workspace) Projects(teamname string) (*ProjectsRes, error) {
	urlStr := w.c.requestUrl("/workspaces/%s/projects/", teamname)
	response, err := w.c.executePaginated("GET", urlStr, "")
	if err != nil {
		return nil, err
	}

	return decodeProjects(response)
}

func decodePermission(permission interface{}) *Permission {
	permissionResponseMap := permission.(map[string]interface{})
	if permissionResponseMap["size"].(float64) == 0 {
		return nil
	}

	permissionValues := permissionResponseMap["values"].([]interface{})
	if len(permissionValues) == 0 {
		return nil
	}

	permissionValue := permissionValues[0].(map[string]interface{})
	return &Permission{
		Type: permissionValue["permission"].(string),
	}
}

func decodeWorkspace(workspace interface{}) (*Workspace, error) {
	var workspaceEntry Workspace
	workspaceResponseMap := workspace.(map[string]interface{})

	if workspaceResponseMap["type"] != nil && workspaceResponseMap["type"] == "error" {
		return nil, DecodeError(workspaceResponseMap)
	}

	err := mapstructure.Decode(workspace, &workspaceEntry)
	return &workspaceEntry, err
}

func decodeWorkspaceList(workspaceResponse interface{}) (*WorkspaceList, error) {
	workspaceResponseMap := workspaceResponse.(map[string]interface{})
	workspaceMapList := workspaceResponseMap["values"].([]interface{})

	var workspaces []Workspace
	for _, workspaceMap := range workspaceMapList {
		workspaceEntry, err := decodeWorkspace(workspaceMap)
		if err != nil {
			return nil, err
		}
		workspaces = append(workspaces, *workspaceEntry)
	}

	page, ok := workspaceResponseMap["page"].(float64)
	if !ok {
		page = 0
	}

	pagelen, ok := workspaceResponseMap["pagelen"].(float64)
	if !ok {
		pagelen = 0
	}
	max_depth, ok := workspaceResponseMap["max_depth"].(float64)
	if !ok {
		max_depth = 0
	}
	size, ok := workspaceResponseMap["size"].(float64)
	if !ok {
		size = 0
	}

	next, ok := workspaceResponseMap["next"].(string)
	if !ok {
		next = ""
	}

	workspacesList := WorkspaceList{
		Page:       int(page),
		Pagelen:    int(pagelen),
		MaxDepth:   int(max_depth),
		Size:       int(size),
		Next:       next,
		Workspaces: workspaces,
	}

	return &workspacesList, nil
}

func decodeProjects(projectResponse interface{}) (*ProjectsRes, error) {
	projectsResponseMap, ok := projectResponse.(map[string]interface{})
	if !ok {
		return nil, errors.New("Not a valid format")
	}

	var projects []Project
	projectArray := projectsResponseMap["values"].([]interface{})
	for _, projectEntry := range projectArray {
		var project Project
		if err := mapstructure.Decode(projectEntry, &project); err == nil {
			projects = append(projects, project)
		}
	}

	page, ok := projectsResponseMap["page"].(float64)
	if !ok {
		page = 0
	}

	pagelen, ok := projectsResponseMap["pagelen"].(float64)
	if !ok {
		pagelen = 0
	}
	max_depth, ok := projectsResponseMap["max_width"].(float64)
	if !ok {
		max_depth = 0
	}
	size, ok := projectsResponseMap["size"].(float64)
	if !ok {
		size = 0
	}

	res := ProjectsRes{
		Page:     int32(page),
		Pagelen:  int32(pagelen),
		MaxDepth: int32(max_depth),
		Size:     int32(size),
		Items:    projects,
	}
	return &res, nil
}

func decodeMembers(membersResponse interface{}) (*WorkspaceMembers, error) {
	responseMap, ok := membersResponse.(map[string]interface{})
	if !ok {
		return nil, errors.New("not a valid format")
	}

	var members []User
	userArray := responseMap["values"].([]interface{})
	for _, userEntry := range userArray {
		userEntryMap := userEntry.(map[string]interface{})

		member, err := decodeUser(userEntryMap["user"])
		if err != nil {
			return nil, err
		}
		members = append(members, *member)
	}

	page, ok := responseMap["page"].(int)
	if !ok {
		page = 0
	}
	pagelen, ok := responseMap["pagelen"].(int)
	if !ok {
		pagelen = 0
	}
	size, ok := responseMap["size"].(int)
	if !ok {
		size = 0
	}

	workspaceMembers := WorkspaceMembers{
		Page:    page,
		Pagelen: pagelen,
		Size:    size,
		Members: members,
	}
	return &workspaceMembers, nil
}
<commit_msg>Update `Workspace.Members` to use pagination (#197)<commit_after>package bitbucket

import (
	"errors"

	"github.com/mitchellh/mapstructure"
)

type Workspace struct {
	c *Client

	Repositories *Repositories
	Permissions  *Permission

	UUID       string
	Type       string
	Slug       string
	Is_Private bool
	Name       string
	workspace
}

type WorkspaceList struct {
	Page       int
	Pagelen    int
	MaxDepth   int
	Size       int
	Next       string
	Workspaces []Workspace
}

type Permission struct {
	c *Client

	Type string
}

type ProjectsRes struct {
	Page     int32
	Pagelen  int32
	MaxDepth int32
	Size     int32
	Items    []Project
}

type WorkspaceMembers struct {
	Page    int
	Pagelen int
	Size    int
	Members []User
}

func (t *Permission) GetUserPermissions(organization, member string) (*Permission, error) {
	urlStr := t.c.requestUrl("/workspaces/%s/permissions?q=user.nickname=\"%s\"", organization, member)
	response, err := t.c.executePaginated("GET", urlStr, "")
	if err != nil {
		return nil, err
	}

	return decodePermission(response), err
}

func (t *Permission) GetUserPermissionsByUuid(organization, member string) (*Permission, error) {
	urlStr := t.c.requestUrl("/workspaces/%s/permissions?q=user.uuid=\"%s\"", organization, member)
	response, err := t.c.executePaginated("GET", urlStr, "")
	if err != nil {
		return nil, err
	}

	return decodePermission(response), err
}

func (t *Workspace) List() (*WorkspaceList, error) {
	urlStr := t.c.requestUrl("/workspaces")
	response, err := t.c.executePaginated("GET", urlStr, "")
	if err != nil {
		return nil, err
	}

	return decodeWorkspaceList(response)
}

func (t *Workspace) Get(workspace string) (*Workspace, error) {
	urlStr := t.c.requestUrl("/workspaces/%s", workspace)
	response, err := t.c.execute("GET", urlStr, "")
	if err != nil {
		return nil, err
	}

	return decodeWorkspace(response)
}

func (w *Workspace) Members(teamname string) (*WorkspaceMembers, error) {
	urlStr := w.c.requestUrl("/workspaces/%s/members", teamname)
	response, err := w.c.executePaginated("GET", urlStr, "")
	if err != nil {
		return nil, err
	}

	return decodeMembers(response)
}

func (w *Workspace) Projects(teamname string) (*ProjectsRes, error) {
	urlStr := w.c.requestUrl("/workspaces/%s/projects/", teamname)
	response, err := w.c.executePaginated("GET", urlStr, "")
	if err != nil {
		return nil, err
	}

	return decodeProjects(response)
}

func decodePermission(permission interface{}) *Permission {
	permissionResponseMap := permission.(map[string]interface{})
	if permissionResponseMap["size"].(float64) == 0 {
		return nil
	}

	permissionValues := permissionResponseMap["values"].([]interface{})
	if len(permissionValues) == 0 {
		return nil
	}

	permissionValue := permissionValues[0].(map[string]interface{})
	return &Permission{
		Type: permissionValue["permission"].(string),
	}
}

func decodeWorkspace(workspace interface{}) (*Workspace, error) {
	var workspaceEntry Workspace
	workspaceResponseMap := workspace.(map[string]interface{})

	if workspaceResponseMap["type"] != nil && workspaceResponseMap["type"] == "error" {
		return nil, DecodeError(workspaceResponseMap)
	}

	err := mapstructure.Decode(workspace, &workspaceEntry)
	return &workspaceEntry, err
}

func decodeWorkspaceList(workspaceResponse interface{}) (*WorkspaceList, error) {
	workspaceResponseMap := workspaceResponse.(map[string]interface{})
	workspaceMapList := workspaceResponseMap["values"].([]interface{})

	var workspaces []Workspace
	for _, workspaceMap := range workspaceMapList {
		workspaceEntry, err := decodeWorkspace(workspaceMap)
		if err != nil {
			return nil, err
		}
		workspaces = append(workspaces, *workspaceEntry)
	}

	page, ok := workspaceResponseMap["page"].(float64)
	if !ok {
		page = 0
	}

	pagelen, ok := workspaceResponseMap["pagelen"].(float64)
	if !ok {
		pagelen = 0
	}
	max_depth, ok := workspaceResponseMap["max_depth"].(float64)
	if !ok {
		max_depth = 0
	}
	size, ok := workspaceResponseMap["size"].(float64)
	if !ok {
		size = 0
	}

	next, ok := workspaceResponseMap["next"].(string)
	if !ok {
		next = ""
	}

	workspacesList := WorkspaceList{
		Page:       int(page),
		Pagelen:    int(pagelen),
		MaxDepth:   int(max_depth),
		Size:       int(size),
		Next:       next,
		Workspaces: workspaces,
	}

	return &workspacesList, nil
}

func decodeProjects(projectResponse interface{}) (*ProjectsRes, error) {
	projectsResponseMap, ok := projectResponse.(map[string]interface{})
	if !ok {
		return nil, errors.New("Not a valid format")
	}

	var projects []Project
	projectArray := projectsResponseMap["values"].([]interface{})
	for _, projectEntry := range projectArray {
		var project Project
		if err := mapstructure.Decode(projectEntry, &project); err == nil {
			projects = append(projects, project)
		}
	}

	page, ok := projectsResponseMap["page"].(float64)
	if !ok {
		page = 0
	}

	pagelen, ok := projectsResponseMap["pagelen"].(float64)
	if !ok {
		pagelen = 0
	}
	max_depth, ok := projectsResponseMap["max_width"].(float64)
	if !ok {
		max_depth = 0
	}
	size, ok := projectsResponseMap["size"].(float64)
	if !ok {
		size = 0
	}

	res := ProjectsRes{
		Page:     int32(page),
		Pagelen:  int32(pagelen),
		MaxDepth: int32(max_depth),
		Size:     int32(size),
		Items:    projects,
	}
	return &res, nil
}

func decodeMembers(membersResponse interface{}) (*WorkspaceMembers, error) {
	responseMap, ok := membersResponse.(map[string]interface{})
	if !ok {
		return nil, errors.New("not a valid format")
	}

	var members []User
	userArray := responseMap["values"].([]interface{})
	for _, userEntry := range userArray {
		userEntryMap := userEntry.(map[string]interface{})

		member, err := decodeUser(userEntryMap["user"])
		if err != nil {
			return nil, err
		}
		members = append(members, *member)
	}

	page, ok := responseMap["page"].(int)
	if !ok {
		page = 0
	}
	pagelen, ok := responseMap["pagelen"].(int)
	if !ok {
		pagelen = 0
	}
	size, ok := responseMap["size"].(int)
	if !ok {
		size = 0
	}

	workspaceMembers := WorkspaceMembers{
		Page:    page,
		Pagelen: pagelen,
		Size:    size,
		Members: members,
	}
	return &workspaceMembers, nil
}
<|endoftext|>


<commit_before>/*
 * provide aspects of functionality relating to my song_tracker project.
 * I intend to move some/all of the functionality from PHP to Go.
 *
 * TODO:
 * - all 'api' type requests should always respond with json - even errors.
 */

package main

import (
	"database/sql"
	"errors"
	"encoding/json"
	"flag"
	"fmt"
	_ "github.com/lib/pq"
	"log"
	"net"
	"net/http"
	"net/http/fcgi"
	"os"
	"regexp"
	"strconv"
	"summercat.com/config"
)

// Config holds our config keys.
type Config struct {
	ListenHost string
	ListenPort uint64
	DbUser     string
	DbPass     string
	DbName     string
	DbHost     string
	DbPort     uint64
	UriPrefix  string
}

// HttpHandler is an object implementing the http.Handler interface
// for serving requests.
type HttpHandler struct {
	settings *Config
}

// RequestHandlerFunc is a function that services a specific request.
type RequestHandlerFunc func(http.ResponseWriter, *http.Request,
	*Config)

// RequestHandler defines requests we service.
type RequestHandler struct {
	Method string
	// regex patter on the path to match.
	PathPattern string
	// handler function.
	Func RequestHandlerFunc
}

// TopResult holds row data for a 'top artist' or 'top song' request.
type TopResult struct {
	Count int64
	Label string
}

// global db connection.
// this is so we try to share a single connection for multiple requests.
// NOTE: according to the database/sql documentation, the DB type
//   is indeed safe for concurrent use by multiple goroutines.
var Db *sql.DB

// TopLimitMax defines the maximum number of 'top' results we respond to.
var TopLimitMax = 100

// connectToDb opens a new connection to the database.
func connectToDb(settings *Config) (*sql.DB, error) {
	// connect to the database.
	dsn := fmt.Sprintf("user=%s password=%s dbname=%s host=%s port=%d",
		settings.DbUser, settings.DbPass, settings.DbName, settings.DbHost,
		settings.DbPort)
	db, err := sql.Open("postgres", dsn)
	if err != nil {
		log.Print("Failed to connect to the database: " + err.Error())
		return nil, err
	}
	log.Print("Opened new connection to the database.")
	return db, nil
}

// getDb connects us to the database if necessary, and returns an active
// database connection.
// we use the global Db variable to try to ensure we use a single connection.
func getDb(settings *Config) (*sql.DB, error) {
	// if we have a db connection, ensure that it is still available
	// so that we reconnect if it is not.
	if Db != nil {
		err := Db.Ping()
		if err != nil {
			log.Printf("Database ping failed: %s", err.Error())
			// continue on, but set us so that we attempt to reconnect.
			Db.Close()
			Db = nil
		}
	}
	// connect to the database if necessary.
	if Db == nil {
		db, err := connectToDb(settings)
		if err != nil {
			log.Printf("Failed to connect to the database: %s", err.Error())
			return nil, err
		}
		Db = db
	}
	return Db, nil
}

// send500Error sends an internal server error with the given message in the
// body.
func send500Error(rw http.ResponseWriter, message string) {
	rw.WriteHeader(http.StatusInternalServerError)
	rw.Write([]byte(message))
}

// getParametersTopRequest retrieves and validates parameters to a
// top artists/songs request.
// we return: user_id, limit (limit of top count), days back to build
//   the top artists count for. if days back is -1, we find the count
//   for all time.
func getParametersTopRequest(request *http.Request) (int64, int64, int64, error) {
	// pull the parameters out and convert and validate them.
	err := request.ParseForm()
	if err != nil {
		return 0, 0, 0, err
	}

	// user_id. required.
	userIdStr, exists := request.Form["user_id"]
	if !exists || len(userIdStr) != 1 {
		return 0, 0, 0, errors.New("No user ID given")
	}
	userId, err := strconv.ParseInt(userIdStr[0], 10, 64)
	if err != nil {
		return 0, 0, 0, err
	}
	if userId < 0 {
		return 0, 0, 0, errors.New("Invalid user ID")
	}

	// limit. required.
	limitStr, exists := request.Form["limit"]
	if !exists || len(limitStr) != 1 {
		return 0, 0, 0, errors.New("No limit given")
	}
	limit, err := strconv.ParseInt(limitStr[0], 10, 64)
	if err != nil {
		return 0, 0, 0, err
	}
	if limit < 1 || int(limit) > TopLimitMax {
		return 0, 0, 0, errors.New("Invalid limit")
	}

	// days_back. optional.
	daysBackStr, exists := request.Form["days_back"]
	var daysBack int64 = -1
	if exists && len(daysBackStr) == 1 {
		daysBack, err = strconv.ParseInt(daysBackStr[0], 10, 64)
		if err != nil {
			return 0, 0, 0, err
		}
		if daysBack < 1 {
			return 0, 0, 0, errors.New("Invalid days back")
		}
	}
	log.Printf("Parameters: user_id [%d] limit [%d] days_back [%d]",
		userId, limit, daysBack)
	return userId, limit, daysBack, nil
}

// retrieveTopArtists retrieves the top artist counts.
// we find the top 'limit' artists for the given user.
// we do this for the specified number of days back. if the given
// days back is set as -1, we find the top artists of all time.
func retrieveTopArtists(settings *Config, userId int64, limit int64,
	daysBack int64) ([]TopResult, error) {
	// we need a database connection.
	// TODO: we could try a cache first.
	db, err := getDb(settings)
	if err != nil {
		return nil, err
	}

	query := `
SELECT
COUNT(s.id) AS count,
s.artist AS label
FROM play p
LEFT JOIN song s
ON p.song_id = s.id
WHERE
p.user_id = $1
AND s.artist != 'N/A'
AND p.create_time > current_timestamp - CAST($2 AS INTERVAL)
GROUP BY s.artist
ORDER BY count DESC
LIMIT $3
`
	interval := fmt.Sprintf("%d days", daysBack)
	if daysBack == -1 {
		// arbitrary. another alternative is to take out the create_time
		// comparison, but that means having a separate query (or messing
		// around with parameters more than I want)
		interval = "1000 years"
	}
	log.Printf("Using interval [%s]", interval)

	rows, err := db.Query(query, userId, interval, limit)
	if err != nil {
		return nil, err
	}

	var results []TopResult
	for rows.Next() {
		var result TopResult
		err := rows.Scan(&result.Count, &result.Label)
		if err != nil {
			return nil, err
		}
		results = append(results, result)
	}
	return results, nil
}

// retrieveTopSongs retrieves the top song counts.
// we find the top 'limit' artists for the given user.
// we do this for the specified number of days back. if the given
// days back is set as -1, we find the top songs of all time.
func retrieveTopSongs(settings *Config, userId int64, limit int64,
	daysBack int64) ([]TopResult, error) {
	// we need a database connection.
	// TODO: we could try a cache first.
	db, err := getDb(settings)
	if err != nil {
		return nil, err
	}

	query := `
SELECT
COUNT(1) AS count,
CONCAT(s.artist, ' - ', s.title) AS label
FROM play p
LEFT JOIN song s
ON p.song_id = s.id
WHERE
p.user_id = $1
AND p.create_time > current_timestamp - CAST($2 AS INTERVAL)
GROUP BY label
ORDER BY count DESC
LIMIT $3
`
	interval := fmt.Sprintf("%d days", daysBack)
	if daysBack == -1 {
		// arbitrary. another alternative is to take out the create_time
		// comparison, but that means having a separate query (or messing
		// around with parameters more than I want)
		interval = "1000 years"
	}
	log.Printf("Using interval [%s]", interval)

	rows, err := db.Query(query, userId, interval, limit)
	if err != nil {
		return nil, err
	}

	var results []TopResult
	for rows.Next() {
		var result TopResult
		err := rows.Scan(&result.Count, &result.Label)
		if err != nil {
			return nil, err
		}
		results = append(results, result)
	}
	return results, nil
}

// responseTopCount sends the response to a top artists or songs request.
func responseTopCount(rw http.ResponseWriter, counts []TopResult) error {
	type TopResponse struct {
		Counts []TopResult
	}
	topResponse := TopResponse{Counts: counts}
	b, err := json.Marshal(topResponse)
	if err != nil {
		return err
	}
	rw.Header().Set("Content-Type", "application/json; charset=utf8")
	rw.Write(b)
	return nil
}

// handlerTopArtists looks up the top artists for a user.
func handlerTopArtists(rw http.ResponseWriter, request *http.Request,
	settings *Config) {
	// find our parameters.
	userId, limit, daysBack, err := getParametersTopRequest(request)
	if err != nil {
		msg := fmt.Sprintf("Failed to retrieve parameters: %s", err.Error())
		log.Printf(msg)
		send500Error(rw, msg)
		return
	}

	// find the counts.
	counts, err := retrieveTopArtists(settings, userId, limit, daysBack)
	if err != nil {
		msg := fmt.Sprintf("Failed to retrieve top artists: %s", err.Error())
		log.Printf(msg)
		send500Error(rw, msg)
		return
	}

	// build and send the response.
	err = responseTopCount(rw, counts)
	if err != nil {
		msg := fmt.Sprintf("Failed to generate response: %s", err.Error())
		log.Printf(msg)
		send500Error(rw, msg)
		return
	}
}

// handlerTopSongs looks up the top songs for a user.
func handlerTopSongs(rw http.ResponseWriter, request *http.Request,
	settings *Config) {
	// find our parameters.
	userId, limit, daysBack, err := getParametersTopRequest(request)
	if err != nil {
		msg := fmt.Sprintf("Failed to retrieve parameters: %s", err.Error())
		log.Printf(msg)
		send500Error(rw, msg)
		return
	}

	// find the counts.
	counts, err := retrieveTopSongs(settings, userId, limit, daysBack)
	if err != nil {
		msg := fmt.Sprintf("Failed to retrieve top artists: %s", err.Error())
		log.Printf(msg)
		send500Error(rw, msg)
		return
	}

	// build and send the response.
	err = responseTopCount(rw, counts)
	if err != nil {
		msg := fmt.Sprintf("Failed to generate response: %s", err.Error())
		log.Printf(msg)
		send500Error(rw, msg)
		return
	}
}

// ServeHTTP is a function to implement the http.Handler interface.
// we service http requests.
func (handler HttpHandler) ServeHTTP(rw http.ResponseWriter,
	request *http.Request) {
	log.Printf("Serving new [%s] request from [%s] to path [%s]",
		request.Method, request.RemoteAddr, request.URL.Path)

	// define our handlers.
	var handlers = []RequestHandler{
		RequestHandler{
			Method: "GET",
			PathPattern: "^" + handler.settings.UriPrefix + "/top/artists",
			Func: handlerTopArtists,
		},
		RequestHandler{
			Method: "GET",
			PathPattern: "^" + handler.settings.UriPrefix + "/top/songs",
			Func: handlerTopSongs,
		},
	}

	// find a matching handler.
	for _, actionHandler := range handlers {
		if actionHandler.Method != request.Method {
			continue
		}
		matched, err := regexp.MatchString(actionHandler.PathPattern,
			request.URL.Path)
		if err != nil {
			log.Printf("Error matching regex: %s", err.Error())
			continue
		}
		if matched {
			actionHandler.Func(rw, request, handler.settings)
			return
		}
	}

	// there was no matching handler - send a 404.
	log.Printf("No handler for this request.")
	rw.WriteHeader(http.StatusNotFound)
	rw.Write([]byte("404 Not Found"))
}

// main is the entry point of the program.
func main() {
	log.SetFlags(log.Ltime)

	// command line arguments.
	configPath := flag.String("config-file", "",
		"Path to a configuration file.")
	flag.Parse()
	// config file is required.
	if len(*configPath) == 0 {
		log.Print("You must specify a configuration file.")
		flag.PrintDefaults()
		os.Exit(1)
	}

	// load up our settings.
	var settings Config
	err := config.GetConfig(*configPath, &settings)
	if err != nil {
		log.Fatalf("Failed to retrieve config: %s", err.Error())
	}

	// start listening.
	var listenHostPort = fmt.Sprintf("%s:%d", settings.ListenHost,
		settings.ListenPort)
	listener, err := net.Listen("tcp", listenHostPort)
	if err != nil {
		log.Fatal("Failed to open port: " + err.Error())
	}

	httpHandler := HttpHandler{settings: &settings}

	// XXX: this will serve requests forever - should we have a signal
	//   or a method to cause this to gracefully stop?
	log.Print("Starting to serve requests.")
	err = fcgi.Serve(listener, httpHandler)
	if err != nil {
		log.Fatal("Failed to start serving HTTP: " + err.Error())
	}
}
<commit_msg>run go fmt on song_tracker2.go<commit_after>/*
 * provide aspects of functionality relating to my song_tracker project.
 * I intend to move some/all of the functionality from PHP to Go.
 *
 * TODO:
 * - all 'api' type requests should always respond with json - even errors.
 */

package main

import (
	"database/sql"
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	_ "github.com/lib/pq"
	"log"
	"net"
	"net/http"
	"net/http/fcgi"
	"os"
	"regexp"
	"strconv"
	"summercat.com/config"
)

// Config holds our config keys.
type Config struct {
	ListenHost string
	ListenPort uint64
	DbUser     string
	DbPass     string
	DbName     string
	DbHost     string
	DbPort     uint64
	UriPrefix  string
}

// HttpHandler is an object implementing the http.Handler interface
// for serving requests.
type HttpHandler struct {
	settings *Config
}

// RequestHandlerFunc is a function that services a specific request.
type RequestHandlerFunc func(http.ResponseWriter, *http.Request,
	*Config)

// RequestHandler defines requests we service.
type RequestHandler struct {
	Method string
	// regex patter on the path to match.
	PathPattern string
	// handler function.
	Func RequestHandlerFunc
}

// TopResult holds row data for a 'top artist' or 'top song' request.
type TopResult struct {
	Count int64
	Label string
}

// global db connection.
// this is so we try to share a single connection for multiple requests.
// NOTE: according to the database/sql documentation, the DB type
//   is indeed safe for concurrent use by multiple goroutines.
var Db *sql.DB

// TopLimitMax defines the maximum number of 'top' results we respond to.
var TopLimitMax = 100

// connectToDb opens a new connection to the database.
func connectToDb(settings *Config) (*sql.DB, error) {
	// connect to the database.
	dsn := fmt.Sprintf("user=%s password=%s dbname=%s host=%s port=%d",
		settings.DbUser, settings.DbPass, settings.DbName, settings.DbHost,
		settings.DbPort)
	db, err := sql.Open("postgres", dsn)
	if err != nil {
		log.Print("Failed to connect to the database: " + err.Error())
		return nil, err
	}
	log.Print("Opened new connection to the database.")
	return db, nil
}

// getDb connects us to the database if necessary, and returns an active
// database connection.
// we use the global Db variable to try to ensure we use a single connection.
func getDb(settings *Config) (*sql.DB, error) {
	// if we have a db connection, ensure that it is still available
	// so that we reconnect if it is not.
	if Db != nil {
		err := Db.Ping()
		if err != nil {
			log.Printf("Database ping failed: %s", err.Error())
			// continue on, but set us so that we attempt to reconnect.
			Db.Close()
			Db = nil
		}
	}
	// connect to the database if necessary.
	if Db == nil {
		db, err := connectToDb(settings)
		if err != nil {
			log.Printf("Failed to connect to the database: %s", err.Error())
			return nil, err
		}
		Db = db
	}
	return Db, nil
}

// send500Error sends an internal server error with the given message in the
// body.
func send500Error(rw http.ResponseWriter, message string) {
	rw.WriteHeader(http.StatusInternalServerError)
	rw.Write([]byte(message))
}

// getParametersTopRequest retrieves and validates parameters to a
// top artists/songs request.
// we return: user_id, limit (limit of top count), days back to build
//   the top artists count for. if days back is -1, we find the count
//   for all time.
func getParametersTopRequest(request *http.Request) (int64, int64, int64, error) {
	// pull the parameters out and convert and validate them.
	err := request.ParseForm()
	if err != nil {
		return 0, 0, 0, err
	}

	// user_id. required.
	userIdStr, exists := request.Form["user_id"]
	if !exists || len(userIdStr) != 1 {
		return 0, 0, 0, errors.New("No user ID given")
	}
	userId, err := strconv.ParseInt(userIdStr[0], 10, 64)
	if err != nil {
		return 0, 0, 0, err
	}
	if userId < 0 {
		return 0, 0, 0, errors.New("Invalid user ID")
	}

	// limit. required.
	limitStr, exists := request.Form["limit"]
	if !exists || len(limitStr) != 1 {
		return 0, 0, 0, errors.New("No limit given")
	}
	limit, err := strconv.ParseInt(limitStr[0], 10, 64)
	if err != nil {
		return 0, 0, 0, err
	}
	if limit < 1 || int(limit) > TopLimitMax {
		return 0, 0, 0, errors.New("Invalid limit")
	}

	// days_back. optional.
	daysBackStr, exists := request.Form["days_back"]
	var daysBack int64 = -1
	if exists && len(daysBackStr) == 1 {
		daysBack, err = strconv.ParseInt(daysBackStr[0], 10, 64)
		if err != nil {
			return 0, 0, 0, err
		}
		if daysBack < 1 {
			return 0, 0, 0, errors.New("Invalid days back")
		}
	}
	log.Printf("Parameters: user_id [%d] limit [%d] days_back [%d]",
		userId, limit, daysBack)
	return userId, limit, daysBack, nil
}

// retrieveTopArtists retrieves the top artist counts.
// we find the top 'limit' artists for the given user.
// we do this for the specified number of days back. if the given
// days back is set as -1, we find the top artists of all time.
func retrieveTopArtists(settings *Config, userId int64, limit int64,
	daysBack int64) ([]TopResult, error) {
	// we need a database connection.
	// TODO: we could try a cache first.
	db, err := getDb(settings)
	if err != nil {
		return nil, err
	}

	query := `
SELECT
COUNT(s.id) AS count,
s.artist AS label
FROM play p
LEFT JOIN song s
ON p.song_id = s.id
WHERE
p.user_id = $1
AND s.artist != 'N/A'
AND p.create_time > current_timestamp - CAST($2 AS INTERVAL)
GROUP BY s.artist
ORDER BY count DESC
LIMIT $3
`
	interval := fmt.Sprintf("%d days", daysBack)
	if daysBack == -1 {
		// arbitrary. another alternative is to take out the create_time
		// comparison, but that means having a separate query (or messing
		// around with parameters more than I want)
		interval = "1000 years"
	}
	log.Printf("Using interval [%s]", interval)

	rows, err := db.Query(query, userId, interval, limit)
	if err != nil {
		return nil, err
	}

	var results []TopResult
	for rows.Next() {
		var result TopResult
		err := rows.Scan(&result.Count, &result.Label)
		if err != nil {
			return nil, err
		}
		results = append(results, result)
	}
	return results, nil
}

// retrieveTopSongs retrieves the top song counts.
// we find the top 'limit' artists for the given user.
// we do this for the specified number of days back. if the given
// days back is set as -1, we find the top songs of all time.
func retrieveTopSongs(settings *Config, userId int64, limit int64,
	daysBack int64) ([]TopResult, error) {
	// we need a database connection.
	// TODO: we could try a cache first.
	db, err := getDb(settings)
	if err != nil {
		return nil, err
	}

	query := `
SELECT
COUNT(1) AS count,
CONCAT(s.artist, ' - ', s.title) AS label
FROM play p
LEFT JOIN song s
ON p.song_id = s.id
WHERE
p.user_id = $1
AND p.create_time > current_timestamp - CAST($2 AS INTERVAL)
GROUP BY label
ORDER BY count DESC
LIMIT $3
`
	interval := fmt.Sprintf("%d days", daysBack)
	if daysBack == -1 {
		// arbitrary. another alternative is to take out the create_time
		// comparison, but that means having a separate query (or messing
		// around with parameters more than I want)
		interval = "1000 years"
	}
	log.Printf("Using interval [%s]", interval)

	rows, err := db.Query(query, userId, interval, limit)
	if err != nil {
		return nil, err
	}

	var results []TopResult
	for rows.Next() {
		var result TopResult
		err := rows.Scan(&result.Count, &result.Label)
		if err != nil {
			return nil, err
		}
		results = append(results, result)
	}
	return results, nil
}

// responseTopCount sends the response to a top artists or songs request.
func responseTopCount(rw http.ResponseWriter, counts []TopResult) error {
	type TopResponse struct {
		Counts []TopResult
	}
	topResponse := TopResponse{Counts: counts}
	b, err := json.Marshal(topResponse)
	if err != nil {
		return err
	}
	rw.Header().Set("Content-Type", "application/json; charset=utf8")
	rw.Write(b)
	return nil
}

// handlerTopArtists looks up the top artists for a user.
func handlerTopArtists(rw http.ResponseWriter, request *http.Request,
	settings *Config) {
	// find our parameters.
	userId, limit, daysBack, err := getParametersTopRequest(request)
	if err != nil {
		msg := fmt.Sprintf("Failed to retrieve parameters: %s", err.Error())
		log.Printf(msg)
		send500Error(rw, msg)
		return
	}

	// find the counts.
	counts, err := retrieveTopArtists(settings, userId, limit, daysBack)
	if err != nil {
		msg := fmt.Sprintf("Failed to retrieve top artists: %s", err.Error())
		log.Printf(msg)
		send500Error(rw, msg)
		return
	}

	// build and send the response.
	err = responseTopCount(rw, counts)
	if err != nil {
		msg := fmt.Sprintf("Failed to generate response: %s", err.Error())
		log.Printf(msg)
		send500Error(rw, msg)
		return
	}
}

// handlerTopSongs looks up the top songs for a user.
func handlerTopSongs(rw http.ResponseWriter, request *http.Request,
	settings *Config) {
	// find our parameters.
	userId, limit, daysBack, err := getParametersTopRequest(request)
	if err != nil {
		msg := fmt.Sprintf("Failed to retrieve parameters: %s", err.Error())
		log.Printf(msg)
		send500Error(rw, msg)
		return
	}

	// find the counts.
	counts, err := retrieveTopSongs(settings, userId, limit, daysBack)
	if err != nil {
		msg := fmt.Sprintf("Failed to retrieve top artists: %s", err.Error())
		log.Printf(msg)
		send500Error(rw, msg)
		return
	}

	// build and send the response.
	err = responseTopCount(rw, counts)
	if err != nil {
		msg := fmt.Sprintf("Failed to generate response: %s", err.Error())
		log.Printf(msg)
		send500Error(rw, msg)
		return
	}
}

// ServeHTTP is a function to implement the http.Handler interface.
// we service http requests.
func (handler HttpHandler) ServeHTTP(rw http.ResponseWriter,
	request *http.Request) {
	log.Printf("Serving new [%s] request from [%s] to path [%s]",
		request.Method, request.RemoteAddr, request.URL.Path)

	// define our handlers.
	var handlers = []RequestHandler{
		RequestHandler{
			Method:      "GET",
			PathPattern: "^" + handler.settings.UriPrefix + "/top/artists",
			Func:        handlerTopArtists,
		},
		RequestHandler{
			Method:      "GET",
			PathPattern: "^" + handler.settings.UriPrefix + "/top/songs",
			Func:        handlerTopSongs,
		},
	}

	// find a matching handler.
	for _, actionHandler := range handlers {
		if actionHandler.Method != request.Method {
			continue
		}
		matched, err := regexp.MatchString(actionHandler.PathPattern,
			request.URL.Path)
		if err != nil {
			log.Printf("Error matching regex: %s", err.Error())
			continue
		}
		if matched {
			actionHandler.Func(rw, request, handler.settings)
			return
		}
	}

	// there was no matching handler - send a 404.
	log.Printf("No handler for this request.")
	rw.WriteHeader(http.StatusNotFound)
	rw.Write([]byte("404 Not Found"))
}

// main is the entry point of the program.
func main() {
	log.SetFlags(log.Ltime)

	// command line arguments.
	configPath := flag.String("config-file", "",
		"Path to a configuration file.")
	flag.Parse()
	// config file is required.
	if len(*configPath) == 0 {
		log.Print("You must specify a configuration file.")
		flag.PrintDefaults()
		os.Exit(1)
	}

	// load up our settings.
	var settings Config
	err := config.GetConfig(*configPath, &settings)
	if err != nil {
		log.Fatalf("Failed to retrieve config: %s", err.Error())
	}

	// start listening.
	var listenHostPort = fmt.Sprintf("%s:%d", settings.ListenHost,
		settings.ListenPort)
	listener, err := net.Listen("tcp", listenHostPort)
	if err != nil {
		log.Fatal("Failed to open port: " + err.Error())
	}

	httpHandler := HttpHandler{settings: &settings}

	// XXX: this will serve requests forever - should we have a signal
	//   or a method to cause this to gracefully stop?
	log.Print("Starting to serve requests.")
	err = fcgi.Serve(listener, httpHandler)
	if err != nil {
		log.Fatal("Failed to start serving HTTP: " + err.Error())
	}
}
<|endoftext|>


<commit_before>package cal

import "time"

// Holidays in France
var (
	FRNouvelAn          = NewYear
	FRLundiDePâques     = EasterMonday
	FRFêteDuTravail     = ECBLabourDay
	FRArmistice1945     = NewHoliday(time.May, 8)
	FRJeudiDeLAscension = NewHolidayFunc(calculateJeuDeLAscension)
	FRLundiDePentecôte  = NewHolidayFunc(calculateLundiDePentecôte)
	FRFêteNationale     = NewHoliday(time.July, 14)
	FRAssomption        = NewHoliday(time.August, 15)
	FRToussaint         = NewHoliday(time.November, 1)
	FRArmistice1918     = NewHoliday(time.November, 11)
	FRNoël              = Christmas
)

// AddFranceHolidays adds all France holidays to the Calendar
func AddFranceHolidays(c *Calendar) {
	c.AddHoliday(
		FRNouvelAn,
		FRLundiDePâques,
		FRFêteDuTravail,
		FRArmistice1945,
		FRJeudiDeLAscension,
		FRLundiDePentecôte,
		FRFêteNationale,
		FRAssomption,
		FRToussaint,
		FRArmistice1918,
		FRNoël,
	)
}

func calculateJeuDeLAscension(year int, loc *time.Location) (time.Month, int) {
	easter := calculateEaster(year, loc)
	// 39 days after Easter Sunday
	t := easter.AddDate(0, 0, +39)
	return t.Month(), t.Day()
}

func calculateLundiDePentecôte(year int, loc *time.Location) (time.Month, int) {
	easter := calculateEaster(year, loc)
	// 50 days after Easter Sunday
	t := easter.AddDate(0, 0, +50)
	return t.Month(), t.Day()
}
<commit_msg>Fix typo for "jeudi de l'ascension"<commit_after>package cal

import "time"

// Holidays in France
var (
	FRNouvelAn          = NewYear
	FRLundiDePâques     = EasterMonday
	FRFêteDuTravail     = ECBLabourDay
	FRArmistice1945     = NewHoliday(time.May, 8)
	FRJeudiDeLAscension = NewHolidayFunc(calculateJeudiDeLAscension)
	FRLundiDePentecôte  = NewHolidayFunc(calculateLundiDePentecôte)
	FRFêteNationale     = NewHoliday(time.July, 14)
	FRAssomption        = NewHoliday(time.August, 15)
	FRToussaint         = NewHoliday(time.November, 1)
	FRArmistice1918     = NewHoliday(time.November, 11)
	FRNoël              = Christmas
)

// AddFranceHolidays adds all France holidays to the Calendar
func AddFranceHolidays(c *Calendar) {
	c.AddHoliday(
		FRNouvelAn,
		FRLundiDePâques,
		FRFêteDuTravail,
		FRArmistice1945,
		FRJeudiDeLAscension,
		FRLundiDePentecôte,
		FRFêteNationale,
		FRAssomption,
		FRToussaint,
		FRArmistice1918,
		FRNoël,
	)
}

func calculateJeudiDeLAscension(year int, loc *time.Location) (time.Month, int) {
	easter := calculateEaster(year, loc)
	// 39 days after Easter Sunday
	t := easter.AddDate(0, 0, +39)
	return t.Month(), t.Day()
}

func calculateLundiDePentecôte(year int, loc *time.Location) (time.Month, int) {
	easter := calculateEaster(year, loc)
	// 50 days after Easter Sunday
	t := easter.AddDate(0, 0, +50)
	return t.Month(), t.Day()
}
<|endoftext|>


<commit_before>// Copyright 2015 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package persistence

import (
	"fmt"
	"time"

	"github.com/juju/errors"
	charmresource "gopkg.in/juju/charm.v6-unstable/resource"
	"gopkg.in/mgo.v2/bson"
	"gopkg.in/mgo.v2/txn"

	"github.com/juju/juju/resource"
)

const (
	resourcesC = "resources"

	stagedIDSuffix = "#staged"
)

// resourceID converts an external resource ID into an internal one.
func (p Persistence) resourceID(id, serviceID string) string {
	return fmt.Sprintf("resource#%s#%s", serviceID, id)
}

// stagedID converts an external resource ID into an internal staged one.
func (p Persistence) stagedID(id, serviceID string) string {
	return p.resourceID(id, serviceID) + stagedIDSuffix
}

func (p Persistence) newStagedResourceOps(id, serviceID string, res resource.Resource) []txn.Op {
	doc := p.newStagedDoc(id, serviceID, res)

	return []txn.Op{{
		C:      resourcesC,
		Id:     doc.DocID,
		Assert: txn.DocMissing,
		Insert: doc,
	}}
}

func (p Persistence) newEnsureStagedSameOps(id, serviceID string, res resource.Resource) []txn.Op {
	doc := p.newStagedDoc(id, serviceID, res)

	// Other than cause the txn to abort, we don't do anything here.
	return []txn.Op{{
		C:      resourcesC,
		Id:     doc.DocID,
		Assert: doc, // TODO(ericsnow) Is this okay?
	}}
}

func (p Persistence) newRemoveStagedOps(id, serviceID string) []txn.Op {
	fullID := p.stagedID(id, serviceID)

	// We don't assert that it exists. We want "missing" to be a noop.
	return []txn.Op{{
		C:      resourcesC,
		Id:     fullID,
		Remove: true,
	}}
}

func (p Persistence) newInsertResourceOps(id, serviceID string, res resource.Resource) []txn.Op {
	doc := p.newResourceDoc(id, serviceID, res)

	return []txn.Op{{
		C:      resourcesC,
		Id:     doc.DocID,
		Assert: txn.DocMissing,
		Insert: doc,
	}}
}

func (p Persistence) newUpdateResourceOps(id, serviceID string, res resource.Resource) []txn.Op {
	doc := p.newResourceDoc(id, serviceID, res)

	return []txn.Op{{
		C:      resourcesC,
		Id:     doc.DocID,
		Assert: txn.DocExists,
		Update: doc,
	}}
}

// newResourceDoc generates a doc that represents the given resource.
func (p Persistence) newResourceDoc(id, serviceID string, res resource.Resource) *resourceDoc {
	fullID := p.resourceID(id, serviceID)
	return resource2doc(fullID, serviceID, res)
}

// newStagedDoc generates a staging doc that represents the given resource.
func (p Persistence) newStagedDoc(id, serviceID string, res resource.Resource) *resourceDoc {
	stagedID := p.stagedID(id, serviceID)
	return resource2doc(stagedID, serviceID, res)
}

// resources returns the resource docs for the given service.
func (p Persistence) resources(serviceID string) ([]resourceDoc, error) {
	var docs []resourceDoc
	query := bson.D{{"service-id", serviceID}}
	if err := p.base.All(resourcesC, query, &docs); err != nil {
		return nil, errors.Trace(err)
	}
	return docs, nil
}

// resourceDoc is the top-level document for resources.
type resourceDoc struct {
	DocID     string `bson:"_id"`
	EnvUUID   string `bson:"env-uuid"`
	ServiceID string `bson:"service-id"`

	Name    string `bson:"name"`
	Type    string `bson:"type"`
	Path    string `bson:"path"`
	Comment string `bson:"comment"`

	Origin      string `bson:"origin"`
	Revision    int    `bson:"revision"`
	Fingerprint []byte `bson:"fingerprint"`
	Size        int64  `bson:"size"`

	Username  string    `bson:"username"`
	Timestamp time.Time `bson:"timestamp-when-added"`
}

// resource2doc converts the resource into a DB doc.
func resource2doc(id, serviceID string, res resource.Resource) *resourceDoc {
	// TODO(ericsnow) We may need to limit the resolution of timestamps
	// in order to avoid some conversion problems from Mongo.
	return &resourceDoc{
		DocID:     id,
		ServiceID: serviceID,

		Name:    res.Name,
		Type:    res.Type.String(),
		Path:    res.Path,
		Comment: res.Comment,

		Origin:      res.Origin.String(),
		Revision:    res.Revision,
		Fingerprint: res.Fingerprint.Bytes(),
		Size:        res.Size,

		Username:  res.Username,
		Timestamp: res.Timestamp,
	}
}

// doc2resource returns the resource.Resource represented by the doc.
func doc2resource(doc resourceDoc) (resource.Resource, error) {
	var res resource.Resource

	resType, err := charmresource.ParseType(doc.Type)
	if err != nil {
		return res, errors.Annotate(err, "got invalid data from DB")
	}

	origin, err := charmresource.ParseOrigin(doc.Origin)
	if err != nil {
		return res, errors.Annotate(err, "got invalid data from DB")
	}

	fp, err := charmresource.NewFingerprint(doc.Fingerprint)
	if err != nil {
		return res, errors.Annotate(err, "got invalid data from DB")
	}

	res = resource.Resource{
		Resource: charmresource.Resource{
			Meta: charmresource.Meta{
				Name:    doc.Name,
				Type:    resType,
				Path:    doc.Path,
				Comment: doc.Comment,
			},
			Origin:      origin,
			Revision:    doc.Revision,
			Fingerprint: fp,
			Size:        doc.Size,
		},
		Username:  doc.Username,
		Timestamp: doc.Timestamp,
	}
	if err := res.Validate(); err != nil {
		return res, errors.Annotate(err, "got invalid data from DB")
	}
	return res, nil
}
<commit_msg>Wrap the doc in a bson.D.<commit_after>// Copyright 2015 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package persistence

import (
	"fmt"
	"time"

	"github.com/juju/errors"
	charmresource "gopkg.in/juju/charm.v6-unstable/resource"
	"gopkg.in/mgo.v2/bson"
	"gopkg.in/mgo.v2/txn"

	"github.com/juju/juju/resource"
)

const (
	resourcesC = "resources"

	stagedIDSuffix = "#staged"
)

// resourceID converts an external resource ID into an internal one.
func (p Persistence) resourceID(id, serviceID string) string {
	return fmt.Sprintf("resource#%s#%s", serviceID, id)
}

// stagedID converts an external resource ID into an internal staged one.
func (p Persistence) stagedID(id, serviceID string) string {
	return p.resourceID(id, serviceID) + stagedIDSuffix
}

func (p Persistence) newStagedResourceOps(id, serviceID string, res resource.Resource) []txn.Op {
	doc := p.newStagedDoc(id, serviceID, res)

	return []txn.Op{{
		C:      resourcesC,
		Id:     doc.DocID,
		Assert: txn.DocMissing,
		Insert: doc,
	}}
}

func (p Persistence) newEnsureStagedSameOps(id, serviceID string, res resource.Resource) []txn.Op {
	doc := p.newStagedDoc(id, serviceID, res)

	// Other than cause the txn to abort, we don't do anything here.
	return []txn.Op{{
		C:      resourcesC,
		Id:     doc.DocID,
		Assert: doc, // TODO(ericsnow) Is this okay?
	}}
}

func (p Persistence) newRemoveStagedOps(id, serviceID string) []txn.Op {
	fullID := p.stagedID(id, serviceID)

	// We don't assert that it exists. We want "missing" to be a noop.
	return []txn.Op{{
		C:      resourcesC,
		Id:     fullID,
		Remove: true,
	}}
}

func (p Persistence) newInsertResourceOps(id, serviceID string, res resource.Resource) []txn.Op {
	doc := p.newResourceDoc(id, serviceID, res)

	return []txn.Op{{
		C:      resourcesC,
		Id:     doc.DocID,
		Assert: txn.DocMissing,
		Insert: doc,
	}}
}

func (p Persistence) newUpdateResourceOps(id, serviceID string, res resource.Resource) []txn.Op {
	doc := p.newResourceDoc(id, serviceID, res)

	return []txn.Op{{
		C:      resourcesC,
		Id:     doc.DocID,
		Assert: txn.DocExists,
		Update: bson.D{{"$set", doc}},
	}}
}

// newResourceDoc generates a doc that represents the given resource.
func (p Persistence) newResourceDoc(id, serviceID string, res resource.Resource) *resourceDoc {
	fullID := p.resourceID(id, serviceID)
	return resource2doc(fullID, serviceID, res)
}

// newStagedDoc generates a staging doc that represents the given resource.
func (p Persistence) newStagedDoc(id, serviceID string, res resource.Resource) *resourceDoc {
	stagedID := p.stagedID(id, serviceID)
	return resource2doc(stagedID, serviceID, res)
}

// resources returns the resource docs for the given service.
func (p Persistence) resources(serviceID string) ([]resourceDoc, error) {
	var docs []resourceDoc
	query := bson.D{{"service-id", serviceID}}
	if err := p.base.All(resourcesC, query, &docs); err != nil {
		return nil, errors.Trace(err)
	}
	return docs, nil
}

// resourceDoc is the top-level document for resources.
type resourceDoc struct {
	DocID     string `bson:"_id"`
	EnvUUID   string `bson:"env-uuid"`
	ServiceID string `bson:"service-id"`

	Name    string `bson:"name"`
	Type    string `bson:"type"`
	Path    string `bson:"path"`
	Comment string `bson:"comment"`

	Origin      string `bson:"origin"`
	Revision    int    `bson:"revision"`
	Fingerprint []byte `bson:"fingerprint"`
	Size        int64  `bson:"size"`

	Username  string    `bson:"username"`
	Timestamp time.Time `bson:"timestamp-when-added"`
}

// resource2doc converts the resource into a DB doc.
func resource2doc(id, serviceID string, res resource.Resource) *resourceDoc {
	// TODO(ericsnow) We may need to limit the resolution of timestamps
	// in order to avoid some conversion problems from Mongo.
	return &resourceDoc{
		DocID:     id,
		ServiceID: serviceID,

		Name:    res.Name,
		Type:    res.Type.String(),
		Path:    res.Path,
		Comment: res.Comment,

		Origin:      res.Origin.String(),
		Revision:    res.Revision,
		Fingerprint: res.Fingerprint.Bytes(),
		Size:        res.Size,

		Username:  res.Username,
		Timestamp: res.Timestamp,
	}
}

// doc2resource returns the resource.Resource represented by the doc.
func doc2resource(doc resourceDoc) (resource.Resource, error) {
	var res resource.Resource

	resType, err := charmresource.ParseType(doc.Type)
	if err != nil {
		return res, errors.Annotate(err, "got invalid data from DB")
	}

	origin, err := charmresource.ParseOrigin(doc.Origin)
	if err != nil {
		return res, errors.Annotate(err, "got invalid data from DB")
	}

	fp, err := charmresource.NewFingerprint(doc.Fingerprint)
	if err != nil {
		return res, errors.Annotate(err, "got invalid data from DB")
	}

	res = resource.Resource{
		Resource: charmresource.Resource{
			Meta: charmresource.Meta{
				Name:    doc.Name,
				Type:    resType,
				Path:    doc.Path,
				Comment: doc.Comment,
			},
			Origin:      origin,
			Revision:    doc.Revision,
			Fingerprint: fp,
			Size:        doc.Size,
		},
		Username:  doc.Username,
		Timestamp: doc.Timestamp,
	}
	if err := res.Validate(); err != nil {
		return res, errors.Annotate(err, "got invalid data from DB")
	}
	return res, nil
}
<|endoftext|>


<commit_before>// Copyright 2012 tsuru authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package app

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"github.com/globocom/config"
	"github.com/globocom/tsuru/api/auth"
	"github.com/globocom/tsuru/api/bind"
	"github.com/globocom/tsuru/api/service"
	"github.com/globocom/tsuru/db"
	"github.com/globocom/tsuru/log"
	"github.com/globocom/tsuru/repository"
	"io"
	"labix.org/v2/mgo/bson"
	"launchpad.net/goyaml"
	"os"
	"os/exec"
	"path"
	"sort"
	"strings"
	"time"
)

const confSep = "========"

type App struct {
	Env       map[string]bind.EnvVar
	Framework string
	Logs      []applog
	Name      string
	State     string
	Units     []Unit
	Teams     []string
}

func (a *App) MarshalJSON() ([]byte, error) {
	result := make(map[string]interface{})
	result["Name"] = a.Name
	result["State"] = a.State
	result["Framework"] = a.Framework
	result["Teams"] = a.Teams
	result["Units"] = a.Units
	result["Repository"] = repository.GetUrl(a.Name)
	return json.Marshal(&result)
}

type applog struct {
	Date    time.Time
	Message string
}

type conf struct {
	PreRestart []string `yaml:"pre-restart"`
	PosRestart []string `yaml:"pos-restart"`
}

func (a *App) Get() error {
	return db.Session.Apps().Find(bson.M{"name": a.Name}).One(a)
}

// createApp creates a new app.
//
// Creating a new app is a process composed of two steps:
//
//       1. Saves the app in the database
//       2. Deploys juju charm
func createApp(a *App) error {
	a.State = "pending"
	err := db.Session.Apps().Insert(a)
	if err != nil {
		return err
	}
	return deploy(a)
}

// Deploys an app.
func deploy(a *App) error {
	a.log(fmt.Sprintf("creating app %s", a.Name))
	cmd := exec.Command("juju", "deploy", "--repository=/home/charms", "local:"+a.Framework, a.Name)
	log.Printf("deploying %s with name %s", a.Framework, a.Name)
	out, err := cmd.CombinedOutput()
	outStr := string(out)
	a.log(outStr)
	log.Printf("executing %s", outStr)
	if err != nil {
		a.log(fmt.Sprintf("juju finished with exit status: %s", err))
		db.Session.Apps().Remove(bson.M{"name": a.Name})
		return errors.New(outStr)
	}
	return nil
}

func (a *App) unbind() error {
	var instances []service.ServiceInstance
	err := db.Session.ServiceInstances().Find(bson.M{"apps": bson.M{"$in": []string{a.Name}}}).All(&instances)
	if err != nil {
		return err
	}
	var msg string
	var addMsg = func(instanceName string, reason error) {
		if msg == "" {
			msg = "Failed to unbind the following instances:\n"
		}
		msg += fmt.Sprintf("- %s (%s)", instanceName, reason.Error())
	}
	for _, instance := range instances {
		err = instance.Unbind(a)
		if err != nil {
			addMsg(instance.Name, err)
		}
	}
	if msg != "" {
		return errors.New(msg)
	}
	return nil
}

func (a *App) destroy() error {
	out, err := a.unit().destroy()
	msg := string(out)
	log.Print(msg)
	if err != nil {
		return errors.New(msg)
	}
	err = a.unbind()
	if err != nil {
		return err
	}
	return db.Session.Apps().Remove(bson.M{"name": a.Name})
}

func (a *App) AddUnit(u *Unit) {
	for i, unt := range a.Units {
		if unt.Machine == u.Machine {
			a.Units[i] = *u
			return
		}
	}
	a.Units = append(a.Units, *u)
}

func (a *App) find(team *auth.Team) (int, bool) {
	pos := sort.Search(len(a.Teams), func(i int) bool {
		return a.Teams[i] >= team.Name
	})
	return pos, pos < len(a.Teams) && a.Teams[pos] == team.Name
}

func (a *App) grant(team *auth.Team) error {
	pos, found := a.find(team)
	if found {
		return errors.New("This team already has access to this app")
	}
	a.Teams = append(a.Teams, "")
	tmp := a.Teams[pos]
	for i := pos; i < len(a.Teams)-1; i++ {
		a.Teams[i+1], tmp = tmp, a.Teams[i]
	}
	a.Teams[pos] = team.Name
	return nil
}

func (a *App) revoke(team *auth.Team) error {
	index, found := a.find(team)
	if !found {
		return errors.New("This team does not have access to this app")
	}
	copy(a.Teams[index:], a.Teams[index+1:])
	a.Teams = a.Teams[:len(a.Teams)-1]
	return nil
}

func (a *App) teams() []auth.Team {
	var teams []auth.Team
	db.Session.Teams().Find(bson.M{"_id": bson.M{"$in": a.Teams}}).All(&teams)
	return teams
}

func (a *App) setTeams(teams []auth.Team) {
	a.Teams = make([]string, len(teams))
	for i, team := range teams {
		a.Teams[i] = team.Name
	}
	sort.Strings(a.Teams)
}

func (a *App) setEnv(env bind.EnvVar) {
	if a.Env == nil {
		a.Env = make(map[string]bind.EnvVar)
	}
	a.Env[env.Name] = env
	a.log(fmt.Sprintf("setting env %s with value %s", env.Name, env.Value))
}

func (a *App) getEnv(name string) (bind.EnvVar, error) {
	var (
		env bind.EnvVar
		err error
		ok  bool
	)
	if env, ok = a.Env[name]; !ok {
		err = errors.New("Environment variable not declared for this app.")
	}
	return env, err
}

func (a *App) InstanceEnv(name string) map[string]bind.EnvVar {
	envs := make(map[string]bind.EnvVar)
	for k, env := range a.Env {
		if env.InstanceName == name {
			envs[k] = bind.EnvVar(env)
		}
	}
	return envs
}

func deployHookAbsPath(p string) (string, error) {
	repoPath, err := config.GetString("git:unit-repo")
	if err != nil {
		return "", nil
	}
	cmdArgs := strings.Fields(p)
	abs := path.Join(repoPath, cmdArgs[0])
	_, err = os.Stat(abs)
	if os.IsNotExist(err) {
		return p, nil
	}
	cmdArgs[0] = abs
	return strings.Join(cmdArgs, " "), nil
}

// Returns app.conf located at app's git repository
func (a *App) conf() (conf, error) {
	var c conf
	uRepo, err := repository.GetPath()
	if err != nil {
		a.log(fmt.Sprintf("Got error while getting repository path: %s", err))
		return c, err
	}
	cPath := path.Join(uRepo, "app.conf")
	cmd := fmt.Sprintf(`echo "%s";cat %s`, confSep, cPath)
	o, err := a.unit().Command(nil, nil, cmd)
	if err != nil {
		a.log(fmt.Sprintf("Got error while executing command: %s... Skipping hooks execution", err))
		return c, nil
	}
	data := strings.Split(string(o), confSep)[1]
	err = goyaml.Unmarshal([]byte(data), &c)
	if err != nil {
		a.log(fmt.Sprintf("Got error while parsing yaml: %s", err))
		return c, err
	}
	return c, nil
}

func (a *App) runHook(cmds []string, kind string) ([]byte, error) {
	var (
		buf bytes.Buffer
		err error
	)
	a.log(fmt.Sprintf("Executing %s hook...", kind))
	for _, cmd := range cmds {
		p, err := deployHookAbsPath(cmd)
		if err != nil {
			a.log(fmt.Sprintf("Error obtaining absolute path to hook: %s.", err))
			continue
		}
		err = a.run(p, &buf)
		if err != nil {
			return nil, err
		}
	}
	a.log(fmt.Sprintf("Output of %s hooks: %s", kind, buf.Bytes()))
	return buf.Bytes(), err
}

// preRestart is responsible for running user's pre-restart script.
//
// The path to this script can be found at the app.conf file, at the root of user's app repository.
func (a *App) preRestart(c conf) ([]byte, error) {
	if !a.hasRestartHooks(c) {
		a.log("app.conf file does not exists or is in the right place. Skipping pre-restart hook...")
		return []byte(nil), nil
	}
	if len(c.PreRestart) == 0 {
		a.log("pre-restart hook section in app conf does not exists... Skipping pre-restart hook...")
		return []byte(nil), nil
	}
	return a.runHook(c.PreRestart, "pre-restart")
}

// posRestart is responsible for running user's pos-restart script.
//
// The path to this script can be found at the app.conf file, at the root of user's app repository.
func (a *App) posRestart(c conf) ([]byte, error) {
	if !a.hasRestartHooks(c) {
		a.log("app.conf file does not exists or is in the right place. Skipping pos-restart hook...")
		return []byte(nil), nil
	}
	if len(c.PosRestart) == 0 {
		a.log("pos-restart hook section in app conf does not exists... Skipping pos-restart hook...")
		return []byte(nil), nil
	}
	return a.runHook(c.PosRestart, "pos-restart")
}

func (a *App) hasRestartHooks(c conf) bool {
	return len(c.PreRestart) > 0 || len(c.PosRestart) > 0
}

// run executes the command in app units
func (a *App) run(cmd string, w io.Writer) error {
	a.log(fmt.Sprintf("running '%s'", cmd))
	cmd = fmt.Sprintf("[ -f /home/application/apprc ] && source /home/application/apprc; [ -d /home/application/current ] && cd /home/application/current; %s", cmd)
	out, err := a.unit().Command(w, w, cmd)
	a.log(string(out))
	return err
}

// restart runs the restart hook for the app
// and returns your output.
func restart(a *App, w io.Writer) ([]byte, error) {
	u := a.unit()
	a.log("executting hook to restarting")
	if w != nil {
		_, err := w.Write([]byte("\n ---> Restarting your app\n"))
		if err != nil {
			return nil, err
		}
	}
	out, err := u.executeHook("restart", w, w)
	if err != nil {
		return out, err
	}
	a.log(string(out))
	return out, nil
}

// installDeps runs the dependencies hook for the app
// and returns your output.
func installDeps(a *App, w io.Writer) ([]byte, error) {
	u := a.unit()
	a.log("executting hook dependencies")
	out, err := u.executeHook("dependencies", w, w)
	a.log(string(out))
	if err != nil {
		return out, err
	}
	return out, nil
}

func (a *App) unit() *Unit {
	if len(a.Units) > 0 {
		unit := a.Units[0]
		unit.app = a
		return &unit
	}
	return &Unit{app: a}
}

func (a *App) GetUnits() []bind.Unit {
	var units []bind.Unit
	for _, u := range a.Units {
		u.app = a
		units = append(units, &u)
	}
	return units
}

func (a *App) GetName() string {
	return a.Name
}

func (a *App) SetEnvs(envs []bind.EnvVar, publicOnly bool) error {
	e := make([]bind.EnvVar, len(envs))
	for i, env := range envs {
		e[i] = bind.EnvVar(env)
	}
	return setEnvsToApp(a, e, publicOnly)
}

func (a *App) UnsetEnvs(envs []string, publicOnly bool) error {
	return unsetEnvFromApp(a, envs, publicOnly)
}

func (a *App) log(message string) error {
	log.Printf(message)
	l := applog{Date: time.Now(), Message: message}
	a.Logs = append(a.Logs, l)
	return db.Session.Apps().Update(bson.M{"name": a.Name}, a)
}
<commit_msg>api/app: prevent short writes on restart<commit_after>// Copyright 2012 tsuru authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package app

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"github.com/globocom/config"
	"github.com/globocom/tsuru/api/auth"
	"github.com/globocom/tsuru/api/bind"
	"github.com/globocom/tsuru/api/service"
	"github.com/globocom/tsuru/db"
	"github.com/globocom/tsuru/log"
	"github.com/globocom/tsuru/repository"
	"io"
	"labix.org/v2/mgo/bson"
	"launchpad.net/goyaml"
	"os"
	"os/exec"
	"path"
	"sort"
	"strings"
	"time"
)

const confSep = "========"

type App struct {
	Env       map[string]bind.EnvVar
	Framework string
	Logs      []applog
	Name      string
	State     string
	Units     []Unit
	Teams     []string
}

func (a *App) MarshalJSON() ([]byte, error) {
	result := make(map[string]interface{})
	result["Name"] = a.Name
	result["State"] = a.State
	result["Framework"] = a.Framework
	result["Teams"] = a.Teams
	result["Units"] = a.Units
	result["Repository"] = repository.GetUrl(a.Name)
	return json.Marshal(&result)
}

type applog struct {
	Date    time.Time
	Message string
}

type conf struct {
	PreRestart []string `yaml:"pre-restart"`
	PosRestart []string `yaml:"pos-restart"`
}

func (a *App) Get() error {
	return db.Session.Apps().Find(bson.M{"name": a.Name}).One(a)
}

// createApp creates a new app.
//
// Creating a new app is a process composed of two steps:
//
//       1. Saves the app in the database
//       2. Deploys juju charm
func createApp(a *App) error {
	a.State = "pending"
	err := db.Session.Apps().Insert(a)
	if err != nil {
		return err
	}
	return deploy(a)
}

// Deploys an app.
func deploy(a *App) error {
	a.log(fmt.Sprintf("creating app %s", a.Name))
	cmd := exec.Command("juju", "deploy", "--repository=/home/charms", "local:"+a.Framework, a.Name)
	log.Printf("deploying %s with name %s", a.Framework, a.Name)
	out, err := cmd.CombinedOutput()
	outStr := string(out)
	a.log(outStr)
	log.Printf("executing %s", outStr)
	if err != nil {
		a.log(fmt.Sprintf("juju finished with exit status: %s", err))
		db.Session.Apps().Remove(bson.M{"name": a.Name})
		return errors.New(outStr)
	}
	return nil
}

func (a *App) unbind() error {
	var instances []service.ServiceInstance
	err := db.Session.ServiceInstances().Find(bson.M{"apps": bson.M{"$in": []string{a.Name}}}).All(&instances)
	if err != nil {
		return err
	}
	var msg string
	var addMsg = func(instanceName string, reason error) {
		if msg == "" {
			msg = "Failed to unbind the following instances:\n"
		}
		msg += fmt.Sprintf("- %s (%s)", instanceName, reason.Error())
	}
	for _, instance := range instances {
		err = instance.Unbind(a)
		if err != nil {
			addMsg(instance.Name, err)
		}
	}
	if msg != "" {
		return errors.New(msg)
	}
	return nil
}

func (a *App) destroy() error {
	out, err := a.unit().destroy()
	msg := string(out)
	log.Print(msg)
	if err != nil {
		return errors.New(msg)
	}
	err = a.unbind()
	if err != nil {
		return err
	}
	return db.Session.Apps().Remove(bson.M{"name": a.Name})
}

func (a *App) AddUnit(u *Unit) {
	for i, unt := range a.Units {
		if unt.Machine == u.Machine {
			a.Units[i] = *u
			return
		}
	}
	a.Units = append(a.Units, *u)
}

func (a *App) find(team *auth.Team) (int, bool) {
	pos := sort.Search(len(a.Teams), func(i int) bool {
		return a.Teams[i] >= team.Name
	})
	return pos, pos < len(a.Teams) && a.Teams[pos] == team.Name
}

func (a *App) grant(team *auth.Team) error {
	pos, found := a.find(team)
	if found {
		return errors.New("This team already has access to this app")
	}
	a.Teams = append(a.Teams, "")
	tmp := a.Teams[pos]
	for i := pos; i < len(a.Teams)-1; i++ {
		a.Teams[i+1], tmp = tmp, a.Teams[i]
	}
	a.Teams[pos] = team.Name
	return nil
}

func (a *App) revoke(team *auth.Team) error {
	index, found := a.find(team)
	if !found {
		return errors.New("This team does not have access to this app")
	}
	copy(a.Teams[index:], a.Teams[index+1:])
	a.Teams = a.Teams[:len(a.Teams)-1]
	return nil
}

func (a *App) teams() []auth.Team {
	var teams []auth.Team
	db.Session.Teams().Find(bson.M{"_id": bson.M{"$in": a.Teams}}).All(&teams)
	return teams
}

func (a *App) setTeams(teams []auth.Team) {
	a.Teams = make([]string, len(teams))
	for i, team := range teams {
		a.Teams[i] = team.Name
	}
	sort.Strings(a.Teams)
}

func (a *App) setEnv(env bind.EnvVar) {
	if a.Env == nil {
		a.Env = make(map[string]bind.EnvVar)
	}
	a.Env[env.Name] = env
	a.log(fmt.Sprintf("setting env %s with value %s", env.Name, env.Value))
}

func (a *App) getEnv(name string) (bind.EnvVar, error) {
	var (
		env bind.EnvVar
		err error
		ok  bool
	)
	if env, ok = a.Env[name]; !ok {
		err = errors.New("Environment variable not declared for this app.")
	}
	return env, err
}

func (a *App) InstanceEnv(name string) map[string]bind.EnvVar {
	envs := make(map[string]bind.EnvVar)
	for k, env := range a.Env {
		if env.InstanceName == name {
			envs[k] = bind.EnvVar(env)
		}
	}
	return envs
}

func deployHookAbsPath(p string) (string, error) {
	repoPath, err := config.GetString("git:unit-repo")
	if err != nil {
		return "", nil
	}
	cmdArgs := strings.Fields(p)
	abs := path.Join(repoPath, cmdArgs[0])
	_, err = os.Stat(abs)
	if os.IsNotExist(err) {
		return p, nil
	}
	cmdArgs[0] = abs
	return strings.Join(cmdArgs, " "), nil
}

// Returns app.conf located at app's git repository
func (a *App) conf() (conf, error) {
	var c conf
	uRepo, err := repository.GetPath()
	if err != nil {
		a.log(fmt.Sprintf("Got error while getting repository path: %s", err))
		return c, err
	}
	cPath := path.Join(uRepo, "app.conf")
	cmd := fmt.Sprintf(`echo "%s";cat %s`, confSep, cPath)
	o, err := a.unit().Command(nil, nil, cmd)
	if err != nil {
		a.log(fmt.Sprintf("Got error while executing command: %s... Skipping hooks execution", err))
		return c, nil
	}
	data := strings.Split(string(o), confSep)[1]
	err = goyaml.Unmarshal([]byte(data), &c)
	if err != nil {
		a.log(fmt.Sprintf("Got error while parsing yaml: %s", err))
		return c, err
	}
	return c, nil
}

func (a *App) runHook(cmds []string, kind string) ([]byte, error) {
	var (
		buf bytes.Buffer
		err error
	)
	a.log(fmt.Sprintf("Executing %s hook...", kind))
	for _, cmd := range cmds {
		p, err := deployHookAbsPath(cmd)
		if err != nil {
			a.log(fmt.Sprintf("Error obtaining absolute path to hook: %s.", err))
			continue
		}
		err = a.run(p, &buf)
		if err != nil {
			return nil, err
		}
	}
	a.log(fmt.Sprintf("Output of %s hooks: %s", kind, buf.Bytes()))
	return buf.Bytes(), err
}

// preRestart is responsible for running user's pre-restart script.
//
// The path to this script can be found at the app.conf file, at the root of user's app repository.
func (a *App) preRestart(c conf) ([]byte, error) {
	if !a.hasRestartHooks(c) {
		a.log("app.conf file does not exists or is in the right place. Skipping pre-restart hook...")
		return []byte(nil), nil
	}
	if len(c.PreRestart) == 0 {
		a.log("pre-restart hook section in app conf does not exists... Skipping pre-restart hook...")
		return []byte(nil), nil
	}
	return a.runHook(c.PreRestart, "pre-restart")
}

// posRestart is responsible for running user's pos-restart script.
//
// The path to this script can be found at the app.conf file, at the root of user's app repository.
func (a *App) posRestart(c conf) ([]byte, error) {
	if !a.hasRestartHooks(c) {
		a.log("app.conf file does not exists or is in the right place. Skipping pos-restart hook...")
		return []byte(nil), nil
	}
	if len(c.PosRestart) == 0 {
		a.log("pos-restart hook section in app conf does not exists... Skipping pos-restart hook...")
		return []byte(nil), nil
	}
	return a.runHook(c.PosRestart, "pos-restart")
}

func (a *App) hasRestartHooks(c conf) bool {
	return len(c.PreRestart) > 0 || len(c.PosRestart) > 0
}

// run executes the command in app units
func (a *App) run(cmd string, w io.Writer) error {
	a.log(fmt.Sprintf("running '%s'", cmd))
	cmd = fmt.Sprintf("[ -f /home/application/apprc ] && source /home/application/apprc; [ -d /home/application/current ] && cd /home/application/current; %s", cmd)
	out, err := a.unit().Command(w, w, cmd)
	a.log(string(out))
	return err
}

// restart runs the restart hook for the app
// and returns your output.
func restart(a *App, w io.Writer) ([]byte, error) {
	u := a.unit()
	a.log("executting hook to restarting")
	if w != nil {
		content := []byte("\n ---> Restarting your app\n")
		n, err := w.Write(content)
		if err != nil {
			return nil, err
		}
		if len(content) != n {
			return nil, io.ErrShortWrite
		}
	}
	out, err := u.executeHook("restart", w, w)
	if err != nil {
		return out, err
	}
	a.log(string(out))
	return out, nil
}

// installDeps runs the dependencies hook for the app
// and returns your output.
func installDeps(a *App, w io.Writer) ([]byte, error) {
	u := a.unit()
	a.log("executting hook dependencies")
	out, err := u.executeHook("dependencies", w, w)
	a.log(string(out))
	if err != nil {
		return out, err
	}
	return out, nil
}

func (a *App) unit() *Unit {
	if len(a.Units) > 0 {
		unit := a.Units[0]
		unit.app = a
		return &unit
	}
	return &Unit{app: a}
}

func (a *App) GetUnits() []bind.Unit {
	var units []bind.Unit
	for _, u := range a.Units {
		u.app = a
		units = append(units, &u)
	}
	return units
}

func (a *App) GetName() string {
	return a.Name
}

func (a *App) SetEnvs(envs []bind.EnvVar, publicOnly bool) error {
	e := make([]bind.EnvVar, len(envs))
	for i, env := range envs {
		e[i] = bind.EnvVar(env)
	}
	return setEnvsToApp(a, e, publicOnly)
}

func (a *App) UnsetEnvs(envs []string, publicOnly bool) error {
	return unsetEnvFromApp(a, envs, publicOnly)
}

func (a *App) log(message string) error {
	log.Printf(message)
	l := applog{Date: time.Now(), Message: message}
	a.Logs = append(a.Logs, l)
	return db.Session.Apps().Update(bson.M{"name": a.Name}, a)
}
<|endoftext|>


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

package e2e

import (
	"strings"
	"testing"
	"time"

	"github.com/coreos/etcd/pkg/fileutil"
	"github.com/coreos/etcd/pkg/testutil"
)

func TestCtlV3SetQuorum(t *testing.T) {
	testCtlV3Set(t, &configNoTLS, 3*time.Second, true)
}

func TestCtlV3SetQuorumZeroTimeout(t *testing.T) {
	testCtlV3Set(t, &configNoTLS, 0, true)
}

func TestCtlV3SetQuorumTimeout(t *testing.T) {
	testCtlV3Set(t, &configNoTLS, time.Nanosecond, true)
}

func TestCtlV3SetPeerTLSQuorum(t *testing.T) {
	testCtlV3Set(t, &configPeerTLS, 3*time.Second, true)
}

func testCtlV3Set(t *testing.T, cfg *etcdProcessClusterConfig, dialTimeout time.Duration, quorum bool) {
	defer testutil.AfterTest(t)

	epc := setupCtlV3Test(t, cfg, quorum)
	defer func() {
		if errC := epc.Close(); errC != nil {
			t.Fatalf("error closing etcd processes (%v)", errC)
		}
	}()

	key, value := "foo", "bar"

	donec := make(chan struct{})
	go func() {
		if err := ctlV3Put(epc, key, value, dialTimeout); err != nil {
			if dialTimeout > 0 && dialTimeout <= time.Nanosecond && isGRPCTimedout(err) { // timeout expected
				donec <- struct{}{}
				return
			}
			t.Fatalf("put error (%v)", err)
		}
		if err := ctlV3Get(epc, key, value, dialTimeout, quorum); err != nil {
			if dialTimeout > 0 && dialTimeout <= time.Nanosecond && isGRPCTimedout(err) { // timeout expected
				donec <- struct{}{}
				return
			}
			t.Fatalf("get error (%v)", err)
		}
		donec <- struct{}{}
	}()

	select {
	case <-time.After(2*dialTimeout + time.Second):
		if dialTimeout > 0 {
			t.Fatalf("test timed out for %v", dialTimeout)
		}
	case <-donec:
	}
}

func ctlV3PrefixArgs(clus *etcdProcessCluster, dialTimeout time.Duration) []string {
	if len(clus.proxies()) > 0 { // TODO: add proxy check as in v2
		panic("v3 proxy not implemented")
	}

	endpoints := ""
	if backends := clus.backends(); len(backends) != 0 {
		es := []string{}
		for _, b := range backends {
			es = append(es, stripSchema(b.cfg.acurl.String()))
		}
		endpoints = strings.Join(es, ",")
	}
	cmdArgs := []string{"../bin/etcdctlv3", "--endpoints", endpoints, "--dial-timeout", dialTimeout.String()}
	if clus.cfg.isClientTLS {
		cmdArgs = append(cmdArgs, "--cacert", caPath, "--cert", certPath, "--key", privateKeyPath)
	}
	return cmdArgs
}

func ctlV3Put(clus *etcdProcessCluster, key, value string, dialTimeout time.Duration) error {
	cmdArgs := append(ctlV3PrefixArgs(clus, dialTimeout), "put", key, value)
	return spawnWithExpectedString(cmdArgs, "OK")
}

func ctlV3Get(clus *etcdProcessCluster, key, value string, dialTimeout time.Duration, quorum bool) error {
	if !quorum { // TODO: add serialized option
		panic("serialized option is not implemented")
	}

	cmdArgs := append(ctlV3PrefixArgs(clus, dialTimeout), "get", key)

	// TODO: match by value. Currently it prints out both key and value in multi-lines.
	return spawnWithExpectedString(cmdArgs, key)
}

func mustCtlV3(t *testing.T) {
	if !fileutil.Exist("../bin/etcdctlv3") {
		t.Fatalf("could not find etcdctlv3 binary")
	}
}

func setupCtlV3Test(t *testing.T, cfg *etcdProcessClusterConfig, quorum bool) *etcdProcessCluster {
	if !quorum { // TODO: add serialized option
		panic("serialized option is not implemented")
	}

	mustCtlV3(t)
	if !quorum {
		cfg = configStandalone(*cfg)
	}
	copied := *cfg
	copied.isV3 = true
	epc, err := newEtcdProcessCluster(&copied)
	if err != nil {
		t.Fatalf("could not start etcd process cluster (%v)", err)
	}
	return epc
}

func isGRPCTimedout(err error) bool {
	return strings.Contains(err.Error(), "grpc: timed out trying to connect")
}

func stripSchema(s string) string {
	if strings.HasPrefix(s, "http://") {
		s = strings.Replace(s, "http://", "", -1)
	}
	if strings.HasPrefix(s, "https://") {
		s = strings.Replace(s, "https://", "", -1)
	}
	return s
}
<commit_msg>e2e: etcdctlv3 with serializable read<commit_after>// Copyright 2016 CoreOS, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package e2e

import (
	"strings"
	"testing"
	"time"

	"github.com/coreos/etcd/pkg/fileutil"
	"github.com/coreos/etcd/pkg/testutil"
)

func TestCtlV3Set(t *testing.T) {
	testCtlV3Set(t, &configNoTLS, 3*time.Second, false)
}

func TestCtlV3SetZeroTimeout(t *testing.T) {
	testCtlV3Set(t, &configNoTLS, 0, false)
}

func TestCtlV3SetTimeout(t *testing.T) {
	testCtlV3Set(t, &configNoTLS, time.Nanosecond, false)
}

func TestCtlV3SetPeerTLS(t *testing.T) {
	testCtlV3Set(t, &configPeerTLS, 3*time.Second, false)
}

func TestCtlV3SetQuorum(t *testing.T) {
	testCtlV3Set(t, &configNoTLS, 3*time.Second, true)
}

func TestCtlV3SetQuorumZeroTimeout(t *testing.T) {
	testCtlV3Set(t, &configNoTLS, 0, true)
}

func TestCtlV3SetQuorumTimeout(t *testing.T) {
	testCtlV3Set(t, &configNoTLS, time.Nanosecond, true)
}

func TestCtlV3SetPeerTLSQuorum(t *testing.T) {
	testCtlV3Set(t, &configPeerTLS, 3*time.Second, true)
}

func testCtlV3Set(t *testing.T, cfg *etcdProcessClusterConfig, dialTimeout time.Duration, quorum bool) {
	defer testutil.AfterTest(t)

	epc := setupCtlV3Test(t, cfg, quorum)
	defer func() {
		if errC := epc.Close(); errC != nil {
			t.Fatalf("error closing etcd processes (%v)", errC)
		}
	}()

	key, value := "foo", "bar"

	donec := make(chan struct{})
	go func() {
		if err := ctlV3Put(epc, key, value, dialTimeout); err != nil {
			if dialTimeout > 0 && dialTimeout <= time.Nanosecond && isGRPCTimedout(err) { // timeout expected
				donec <- struct{}{}
				return
			}
			t.Fatalf("put error (%v)", err)
		}
		if err := ctlV3Get(epc, key, value, dialTimeout, quorum); err != nil {
			if dialTimeout > 0 && dialTimeout <= time.Nanosecond && isGRPCTimedout(err) { // timeout expected
				donec <- struct{}{}
				return
			}
			t.Fatalf("get error (%v)", err)
		}
		donec <- struct{}{}
	}()

	select {
	case <-time.After(2*dialTimeout + time.Second):
		if dialTimeout > 0 {
			t.Fatalf("test timed out for %v", dialTimeout)
		}
	case <-donec:
	}
}

func ctlV3PrefixArgs(clus *etcdProcessCluster, dialTimeout time.Duration) []string {
	if len(clus.proxies()) > 0 { // TODO: add proxy check as in v2
		panic("v3 proxy not implemented")
	}

	endpoints := ""
	if backends := clus.backends(); len(backends) != 0 {
		es := []string{}
		for _, b := range backends {
			es = append(es, stripSchema(b.cfg.acurl.String()))
		}
		endpoints = strings.Join(es, ",")
	}
	cmdArgs := []string{"../bin/etcdctlv3", "--endpoints", endpoints, "--dial-timeout", dialTimeout.String()}
	if clus.cfg.isClientTLS {
		cmdArgs = append(cmdArgs, "--cacert", caPath, "--cert", certPath, "--key", privateKeyPath)
	}
	return cmdArgs
}

func ctlV3Put(clus *etcdProcessCluster, key, value string, dialTimeout time.Duration) error {
	cmdArgs := append(ctlV3PrefixArgs(clus, dialTimeout), "put", key, value)
	return spawnWithExpectedString(cmdArgs, "OK")
}

func ctlV3Get(clus *etcdProcessCluster, key, value string, dialTimeout time.Duration, quorum bool) error {
	cmdArgs := append(ctlV3PrefixArgs(clus, dialTimeout), "get", key)
	if !quorum {
		cmdArgs = append(cmdArgs, "--consistency", "s")
	}
	// TODO: match by value. Currently it prints out both key and value in multi-lines.
	return spawnWithExpectedString(cmdArgs, key)
}

func mustCtlV3(t *testing.T) {
	if !fileutil.Exist("../bin/etcdctlv3") {
		t.Fatalf("could not find etcdctlv3 binary")
	}
}

func setupCtlV3Test(t *testing.T, cfg *etcdProcessClusterConfig, quorum bool) *etcdProcessCluster {
	mustCtlV3(t)
	if !quorum {
		cfg = configStandalone(*cfg)
	}
	copied := *cfg
	copied.isV3 = true
	epc, err := newEtcdProcessCluster(&copied)
	if err != nil {
		t.Fatalf("could not start etcd process cluster (%v)", err)
	}
	return epc
}

func isGRPCTimedout(err error) bool {
	return strings.Contains(err.Error(), "grpc: timed out trying to connect")
}

func stripSchema(s string) string {
	if strings.HasPrefix(s, "http://") {
		s = strings.Replace(s, "http://", "", -1)
	}
	if strings.HasPrefix(s, "https://") {
		s = strings.Replace(s, "https://", "", -1)
	}
	return s
}
<|endoftext|>


<commit_before>package main

import (
	"context"
	"errors"
	"fmt"
	"io"
	"os"
	"strconv"
	"strings"
	"time"

	"github.com/golang/protobuf/ptypes"
	"github.com/google/shlex"
	"github.com/relab/gorums/examples/storage/proto"
	"golang.org/x/crypto/ssh/terminal"
)

var help = `
This interface allows you to run RPCs and quorum calls against the Storage
Servers interactively. Take a look at the files 'client.go' and 'server.go'
for the source code of the RPC handlers and quorum functions.
The following commands can be used:

help 	                        	Show this text
exit 	                        	Exit the program
nodes	                        	Print a list of the available nodes
rpc  	[node index] [operation]	Executes an RPC on the given node.
qc   	[operation]             	Executes a quorum call on all nodes.
cfg  	[config] [opertation]   	Executes a quorum call on a configuration.

The following operations are supported:

read 	[key]        	Read a value
write	[key] [value]	Write a value

Examples:

> rpc 0 write foo bar
The command performs the 'write' RPC on node 0, and sets 'foo' = 'bar'

> qc read foo
The command performs the 'read' quorum call, and returns the value of 'foo'

> cfg 1:3 write foo bar
The command performs the write qourum call on node 1 and 2

> cfg 0,2 write foo 'bar baz'
The command performs the write quorum call on node 0 and 2
`

type repl struct {
	mgr  *proto.Manager
	cfg  *proto.Configuration
	term *terminal.Terminal
}

func newRepl(mgr *proto.Manager, cfg *proto.Configuration) *repl {
	return &repl{
		mgr: mgr,
		cfg: cfg,
		term: terminal.NewTerminal(struct {
			io.Reader
			io.Writer
		}{os.Stdin, os.Stderr}, "> "),
	}
}

// NOTE: x/crypto/ssh/terminal is moving to x/terminal at some point.
// See: https://github.com/golang/go/issues/31044 and https://github.com/golang/term
//
// FIXME: ReadLine currently does not work with arrow keys on windows for some reason
// See: https://stackoverflow.com/questions/58237670/terminal-raw-mode-does-not-support-arrows-on-windows
//
// ReadLine reads a line from the terminal in raw mode
func (r repl) ReadLine() (string, error) {
	fd := int(os.Stdin.Fd())
	oldState, err := terminal.MakeRaw(fd)
	if err != nil {
		panic(err)
	}
	defer func() {
		err := terminal.Restore(fd, oldState)
		if err != nil {
			panic(err)
		}
	}()

	return r.term.ReadLine()
}

func Repl(mgr *proto.Manager, defaultCfg *proto.Configuration) {
	r := newRepl(mgr, defaultCfg)

	fmt.Println(help)
	for {
		l, err := r.ReadLine()
		if errors.Is(err, io.EOF) {
			return
		}
		if err != nil {
			fmt.Fprintf(os.Stderr, "Failed to read line: %v\n", err)
			os.Exit(1)
		}
		args, err := shlex.Split(l)
		if err != nil {
			fmt.Fprintf(os.Stderr, "Failed to split command: %v\n", err)
			os.Exit(1)
		}
		if len(args) < 1 {
			continue
		}

		switch args[0] {
		case "exit":
			fallthrough
		case "quit":
			return
		case "help":
			fmt.Println(help)
		case "rpc":
			r.rpc(args[1:])
		case "qc":
			r.qc(args[1:])
		case "cfg":
			r.qcCfg(args[1:])
		case "nodes":
			fmt.Println("Nodes: ")
			for i, n := range mgr.Nodes() {
				fmt.Printf("%d: %s\n", i, n.Address())
			}
		default:
			fmt.Printf("Unknown command '%s'. Type 'help' to see available commands.\n", args[0])
		}
	}
}

func (r repl) rpc(args []string) {
	if len(args) < 2 {
		fmt.Println("'rpc' requires a node index and an operation.")
		return
	}

	index, err := strconv.Atoi(args[0])
	if err != nil {
		fmt.Printf("Invalid id '%s'. node index must be numeric.\n", args[0])
		return
	}

	if index < 0 || index >= r.cfg.Size() {
		fmt.Printf("Invalid index. Must be between 0 and %d.\n", r.cfg.Size())
		return
	}

	node := r.cfg.Nodes()[index]

	switch args[1] {
	case "read":
		r.readRPC(args[2:], node)
	case "write":
		r.writeRPC(args[2:], node)
	}
}

func (r repl) qc(args []string) {
	if len(args) < 1 {
		fmt.Println("'qc' requires an operation.")
		return
	}

	switch args[0] {
	case "read":
		r.readQC(args[1:], r.cfg)
	case "write":
		r.writeQC(args[1:], r.cfg)
	}
}

func (r repl) qcCfg(args []string) {
	if len(args) < 2 {
		fmt.Println("'cfg' requires a configuration and an operation.")
		return
	}
	cfg := r.parseConfiguration(args[0])
	if cfg == nil {
		return
	}
	switch args[1] {
	case "read":
		r.readQC(args[2:], cfg)
	case "write":
		r.writeQC(args[2:], cfg)
	}
}

func (_ repl) readRPC(args []string, node *proto.Node) {
	if len(args) < 1 {
		fmt.Println("Read requires a key to read.")
		return
	}
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	resp, err := node.ReadRPC(ctx, &proto.ReadRequest{Key: args[0]})
	cancel()
	if err != nil {
		fmt.Printf("Read RPC finished with error: %v\n", err)
		return
	}
	if !resp.GetOK() {
		fmt.Printf("%s was not found\n", args[0])
		return
	}
	fmt.Printf("%s = %s\n", args[0], resp.GetValue())
}

func (_ repl) writeRPC(args []string, node *proto.Node) {
	if len(args) < 2 {
		fmt.Println("Write requires a key and a value to write.")
		return
	}
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	resp, err := node.WriteRPC(ctx, &proto.WriteRequest{Key: args[0], Value: args[1], Time: ptypes.TimestampNow()})
	cancel()
	if err != nil {
		fmt.Printf("Write RPC finished with error: %v\n", err)
		return
	}
	if !resp.GetNew() {
		fmt.Printf("Failed to update %s: timestamp too old.\n", args[0])
		return
	}
	fmt.Println("Write OK")
}

func (_ repl) readQC(args []string, cfg *proto.Configuration) {
	if len(args) < 1 {
		fmt.Println("Read requires a key to read.")
		return
	}
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	resp, err := cfg.ReadQC(ctx, &proto.ReadRequest{Key: args[0]})
	cancel()
	if err != nil {
		fmt.Printf("Read RPC finished with error: %v\n", err)
		return
	}
	if !resp.GetOK() {
		fmt.Printf("%s was not found\n", args[0])
		return
	}
	fmt.Printf("%s = %s\n", args[0], resp.GetValue())
}

func (_ repl) writeQC(args []string, cfg *proto.Configuration) {
	if len(args) < 2 {
		fmt.Println("Write requires a key and a value to write.")
		return
	}
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	resp, err := cfg.WriteQC(ctx, &proto.WriteRequest{Key: args[0], Value: args[1], Time: ptypes.TimestampNow()})
	cancel()
	if err != nil {
		fmt.Printf("Write RPC finished with error: %v\n", err)
		return
	}
	if !resp.GetNew() {
		fmt.Printf("Failed to update %s: timestamp too old.\n", args[0])
		return
	}
	fmt.Println("Write OK")
}

func (r repl) parseConfiguration(cfgStr string) (cfg *proto.Configuration) {
	// configuration using range syntax
	if i := strings.Index(cfgStr, ":"); i > -1 {
		var start, stop int
		var err error
		numNodes, _ := r.mgr.Size()
		if i == 0 {
			start = 0
		} else {
			start, err = strconv.Atoi(cfgStr[:i])
			if err != nil {
				fmt.Printf("Failed to parse configuration: %v\n", err)
				return nil
			}
		}
		if i == len(cfgStr)-1 {
			stop = numNodes
		} else {
			stop, err = strconv.Atoi(cfgStr[i+1:])
			if err != nil {
				fmt.Printf("Failed to parse configuration: %v\n", err)
				return nil
			}
		}
		if start >= stop || start < 0 || stop >= numNodes {
			fmt.Println("Invalid configuration.")
			return nil
		}
		cfg, err = r.mgr.NewConfiguration(r.mgr.NodeIDs()[start:stop], &qspec{cfgSize: stop - start})
		if err != nil {
			fmt.Printf("Failed to create configuration: %v\n", err)
			return nil
		}
		return cfg
	}
	// configuration using list of indices
	if indices := strings.Split(cfgStr, ","); len(indices) > 0 {
		selectedNodes := make([]uint32, 0, len(indices))
		nodeIDs := r.mgr.NodeIDs()
		for _, index := range indices {
			i, err := strconv.Atoi(index)
			if err != nil {
				fmt.Printf("Failed to parse configuration: %v\n", err)
				return nil
			}
			if i < 0 || i >= len(nodeIDs) {
				fmt.Println("Invalid configuration.")
				return nil
			}
			selectedNodes = append(selectedNodes, nodeIDs[i])
		}
		cfg, err := r.mgr.NewConfiguration(selectedNodes, &qspec{cfgSize: len(selectedNodes)})
		if err != nil {
			fmt.Printf("Failed to create configuration: %v\n", err)
			return nil
		}
		return cfg
	}
	fmt.Println("Invalid configuration.")
	return nil
}
<commit_msg>examples/storage: fix bounds check message<commit_after>package main

import (
	"context"
	"errors"
	"fmt"
	"io"
	"os"
	"strconv"
	"strings"
	"time"

	"github.com/golang/protobuf/ptypes"
	"github.com/google/shlex"
	"github.com/relab/gorums/examples/storage/proto"
	"golang.org/x/crypto/ssh/terminal"
)

var help = `
This interface allows you to run RPCs and quorum calls against the Storage
Servers interactively. Take a look at the files 'client.go' and 'server.go'
for the source code of the RPC handlers and quorum functions.
The following commands can be used:

help 	                        	Show this text
exit 	                        	Exit the program
nodes	                        	Print a list of the available nodes
rpc  	[node index] [operation]	Executes an RPC on the given node.
qc   	[operation]             	Executes a quorum call on all nodes.
cfg  	[config] [opertation]   	Executes a quorum call on a configuration.

The following operations are supported:

read 	[key]        	Read a value
write	[key] [value]	Write a value

Examples:

> rpc 0 write foo bar
The command performs the 'write' RPC on node 0, and sets 'foo' = 'bar'

> qc read foo
The command performs the 'read' quorum call, and returns the value of 'foo'

> cfg 1:3 write foo bar
The command performs the write qourum call on node 1 and 2

> cfg 0,2 write foo 'bar baz'
The command performs the write quorum call on node 0 and 2
`

type repl struct {
	mgr  *proto.Manager
	cfg  *proto.Configuration
	term *terminal.Terminal
}

func newRepl(mgr *proto.Manager, cfg *proto.Configuration) *repl {
	return &repl{
		mgr: mgr,
		cfg: cfg,
		term: terminal.NewTerminal(struct {
			io.Reader
			io.Writer
		}{os.Stdin, os.Stderr}, "> "),
	}
}

// NOTE: x/crypto/ssh/terminal is moving to x/terminal at some point.
// See: https://github.com/golang/go/issues/31044 and https://github.com/golang/term
//
// FIXME: ReadLine currently does not work with arrow keys on windows for some reason
// See: https://stackoverflow.com/questions/58237670/terminal-raw-mode-does-not-support-arrows-on-windows
//
// ReadLine reads a line from the terminal in raw mode
func (r repl) ReadLine() (string, error) {
	fd := int(os.Stdin.Fd())
	oldState, err := terminal.MakeRaw(fd)
	if err != nil {
		panic(err)
	}
	defer func() {
		err := terminal.Restore(fd, oldState)
		if err != nil {
			panic(err)
		}
	}()

	return r.term.ReadLine()
}

func Repl(mgr *proto.Manager, defaultCfg *proto.Configuration) {
	r := newRepl(mgr, defaultCfg)

	fmt.Println(help)
	for {
		l, err := r.ReadLine()
		if errors.Is(err, io.EOF) {
			return
		}
		if err != nil {
			fmt.Fprintf(os.Stderr, "Failed to read line: %v\n", err)
			os.Exit(1)
		}
		args, err := shlex.Split(l)
		if err != nil {
			fmt.Fprintf(os.Stderr, "Failed to split command: %v\n", err)
			os.Exit(1)
		}
		if len(args) < 1 {
			continue
		}

		switch args[0] {
		case "exit":
			fallthrough
		case "quit":
			return
		case "help":
			fmt.Println(help)
		case "rpc":
			r.rpc(args[1:])
		case "qc":
			r.qc(args[1:])
		case "cfg":
			r.qcCfg(args[1:])
		case "nodes":
			fmt.Println("Nodes: ")
			for i, n := range mgr.Nodes() {
				fmt.Printf("%d: %s\n", i, n.Address())
			}
		default:
			fmt.Printf("Unknown command '%s'. Type 'help' to see available commands.\n", args[0])
		}
	}
}

func (r repl) rpc(args []string) {
	if len(args) < 2 {
		fmt.Println("'rpc' requires a node index and an operation.")
		return
	}

	index, err := strconv.Atoi(args[0])
	if err != nil {
		fmt.Printf("Invalid id '%s'. node index must be numeric.\n", args[0])
		return
	}

	if index < 0 || index >= r.cfg.Size() {
		fmt.Printf("Invalid index. Must be between 0 and %d.\n", r.cfg.Size()-1)
		return
	}

	node := r.cfg.Nodes()[index]

	switch args[1] {
	case "read":
		r.readRPC(args[2:], node)
	case "write":
		r.writeRPC(args[2:], node)
	}
}

func (r repl) qc(args []string) {
	if len(args) < 1 {
		fmt.Println("'qc' requires an operation.")
		return
	}

	switch args[0] {
	case "read":
		r.readQC(args[1:], r.cfg)
	case "write":
		r.writeQC(args[1:], r.cfg)
	}
}

func (r repl) qcCfg(args []string) {
	if len(args) < 2 {
		fmt.Println("'cfg' requires a configuration and an operation.")
		return
	}
	cfg := r.parseConfiguration(args[0])
	if cfg == nil {
		return
	}
	switch args[1] {
	case "read":
		r.readQC(args[2:], cfg)
	case "write":
		r.writeQC(args[2:], cfg)
	}
}

func (_ repl) readRPC(args []string, node *proto.Node) {
	if len(args) < 1 {
		fmt.Println("Read requires a key to read.")
		return
	}
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	resp, err := node.ReadRPC(ctx, &proto.ReadRequest{Key: args[0]})
	cancel()
	if err != nil {
		fmt.Printf("Read RPC finished with error: %v\n", err)
		return
	}
	if !resp.GetOK() {
		fmt.Printf("%s was not found\n", args[0])
		return
	}
	fmt.Printf("%s = %s\n", args[0], resp.GetValue())
}

func (_ repl) writeRPC(args []string, node *proto.Node) {
	if len(args) < 2 {
		fmt.Println("Write requires a key and a value to write.")
		return
	}
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	resp, err := node.WriteRPC(ctx, &proto.WriteRequest{Key: args[0], Value: args[1], Time: ptypes.TimestampNow()})
	cancel()
	if err != nil {
		fmt.Printf("Write RPC finished with error: %v\n", err)
		return
	}
	if !resp.GetNew() {
		fmt.Printf("Failed to update %s: timestamp too old.\n", args[0])
		return
	}
	fmt.Println("Write OK")
}

func (_ repl) readQC(args []string, cfg *proto.Configuration) {
	if len(args) < 1 {
		fmt.Println("Read requires a key to read.")
		return
	}
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	resp, err := cfg.ReadQC(ctx, &proto.ReadRequest{Key: args[0]})
	cancel()
	if err != nil {
		fmt.Printf("Read RPC finished with error: %v\n", err)
		return
	}
	if !resp.GetOK() {
		fmt.Printf("%s was not found\n", args[0])
		return
	}
	fmt.Printf("%s = %s\n", args[0], resp.GetValue())
}

func (_ repl) writeQC(args []string, cfg *proto.Configuration) {
	if len(args) < 2 {
		fmt.Println("Write requires a key and a value to write.")
		return
	}
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	resp, err := cfg.WriteQC(ctx, &proto.WriteRequest{Key: args[0], Value: args[1], Time: ptypes.TimestampNow()})
	cancel()
	if err != nil {
		fmt.Printf("Write RPC finished with error: %v\n", err)
		return
	}
	if !resp.GetNew() {
		fmt.Printf("Failed to update %s: timestamp too old.\n", args[0])
		return
	}
	fmt.Println("Write OK")
}

func (r repl) parseConfiguration(cfgStr string) (cfg *proto.Configuration) {
	// configuration using range syntax
	if i := strings.Index(cfgStr, ":"); i > -1 {
		var start, stop int
		var err error
		numNodes, _ := r.mgr.Size()
		if i == 0 {
			start = 0
		} else {
			start, err = strconv.Atoi(cfgStr[:i])
			if err != nil {
				fmt.Printf("Failed to parse configuration: %v\n", err)
				return nil
			}
		}
		if i == len(cfgStr)-1 {
			stop = numNodes
		} else {
			stop, err = strconv.Atoi(cfgStr[i+1:])
			if err != nil {
				fmt.Printf("Failed to parse configuration: %v\n", err)
				return nil
			}
		}
		if start >= stop || start < 0 || stop >= numNodes {
			fmt.Println("Invalid configuration.")
			return nil
		}
		cfg, err = r.mgr.NewConfiguration(r.mgr.NodeIDs()[start:stop], &qspec{cfgSize: stop - start})
		if err != nil {
			fmt.Printf("Failed to create configuration: %v\n", err)
			return nil
		}
		return cfg
	}
	// configuration using list of indices
	if indices := strings.Split(cfgStr, ","); len(indices) > 0 {
		selectedNodes := make([]uint32, 0, len(indices))
		nodeIDs := r.mgr.NodeIDs()
		for _, index := range indices {
			i, err := strconv.Atoi(index)
			if err != nil {
				fmt.Printf("Failed to parse configuration: %v\n", err)
				return nil
			}
			if i < 0 || i >= len(nodeIDs) {
				fmt.Println("Invalid configuration.")
				return nil
			}
			selectedNodes = append(selectedNodes, nodeIDs[i])
		}
		cfg, err := r.mgr.NewConfiguration(selectedNodes, &qspec{cfgSize: len(selectedNodes)})
		if err != nil {
			fmt.Printf("Failed to create configuration: %v\n", err)
			return nil
		}
		return cfg
	}
	fmt.Println("Invalid configuration.")
	return nil
}
<|endoftext|>