<commit_before>package main

import (
	"encoding/json"
	"fmt"
	"math/rand"
	"socialapi/models"
	"strconv"
	"testing"
	. "github.com/smartystreets/goconvey/convey"
)

func TestChannelMessage(t *testing.T) {
	Convey("While testing channel messages given a channel", t, func() {
		groupName := "testgroup" + strconv.FormatInt(rand.Int63(), 10)

		Convey("first we should create the account", func() {
			account := models.NewAccount()
			account.OldId = AccountOldId.Hex()
			account, err := createAccount(account)
			So(err, ShouldBeNil)
			So(account, ShouldNotBeNil)

			nonOwnerAccount := models.NewAccount()
			nonOwnerAccount.OldId = AccountOldId2.Hex()
			nonOwnerAccount, err = createAccount(nonOwnerAccount)
			So(err, ShouldBeNil)
			So(nonOwnerAccount, ShouldNotBeNil)

			groupChannel, err := createChannelByGroupNameAndType(account.Id, groupName, models.Channel_TYPE_GROUP)
			So(err, ShouldBeNil)
			So(groupChannel, ShouldNotBeNil)

			Convey("message should be able added to the group channel", func() {
				post, err := createPost(groupChannel.Id, account.Id)
				So(err, ShouldBeNil)
				So(post, ShouldNotBeNil)
				So(post.Id, ShouldNotEqual, 0)
				So(post.Body, ShouldNotEqual, "")
				Convey("message can be edited by owner", func() {

					initialPostBody := post.Body
					post.Body = "edited message"

					editedPost, err := updatePost(post)
					So(err, ShouldBeNil)
					So(editedPost, ShouldNotBeNil)
					// body should not be same
					So(initialPostBody, ShouldNotEqual, editedPost.Body)
				})

				// for now social worker handles this issue
				Convey("message can be edited by an admin", nil)
				Convey("message can not be edited by non-owner", nil)

			})

			Convey("message can be deleted by owner", func() {
				post, err := createPost(groupChannel.Id, account.Id)
				So(err, ShouldBeNil)
				So(post, ShouldNotBeNil)
				err = deletePost(post.Id, account.Id, groupChannel.GroupName)
				So(err, ShouldBeNil)
				post2, err := getPost(post.Id, account.Id, groupChannel.GroupName)
				So(err, ShouldNotBeNil)
				So(post2, ShouldBeNil)
			})

			// handled by social worker
			Convey("message can be deleted by an admin", nil)
			Convey("message can not be edited by non-owner", nil)

			Convey("owner can like message", func() {
				post, err := createPost(groupChannel.Id, account.Id)
				So(err, ShouldBeNil)
				So(post, ShouldNotBeNil)

				err = addInteraction("like", post.Id, post.AccountId)
				So(err, ShouldBeNil)

				cmc, err := getPostWithRelatedData(post.Id, post.AccountId, groupName)
				So(err, ShouldBeNil)
				So(cmc, ShouldNotBeNil)

				// it is liked by author
				So(cmc.Interactions["like"].IsInteracted, ShouldBeTrue)

				// actor length should be 1
				So(len(cmc.Interactions["like"].Actors), ShouldEqual, 1)

			})

			Convey("non-owner can like message", func() {
				post, err := createPost(groupChannel.Id, account.Id)
				So(err, ShouldBeNil)
				So(post, ShouldNotBeNil)

				err = addInteraction("like", post.Id, nonOwnerAccount.Id)
				So(err, ShouldBeNil)

				cmc, err := getPostWithRelatedData(post.Id, nonOwnerAccount.Id, groupName)
				So(err, ShouldBeNil)
				So(cmc, ShouldNotBeNil)

				// it is liked by author
				So(cmc.Interactions["like"].IsInteracted, ShouldBeTrue)

				// actor length should be 1
				So(len(cmc.Interactions["like"].Actors), ShouldEqual, 1)

			})

			Convey("we should be able to get only interactions", func() {
				post, err := createPost(groupChannel.Id, account.Id)
				So(err, ShouldBeNil)
				So(post, ShouldNotBeNil)

				err = addInteraction("like", post.Id, nonOwnerAccount.Id)
				So(err, ShouldBeNil)

				likes, err := getInteractions("like", post.Id)
				So(err, ShouldBeNil)

				So(len(likes), ShouldEqual, 1)

			})

			Convey("users should be able to  un-like message", func() {
				post, err := createPost(groupChannel.Id, account.Id)
				So(err, ShouldBeNil)
				So(post, ShouldNotBeNil)

				err = addInteraction("like", post.Id, post.AccountId)
				So(err, ShouldBeNil)

				cmc, err := getPostWithRelatedData(post.Id, post.AccountId, groupName)
				So(err, ShouldBeNil)
				So(cmc, ShouldNotBeNil)

				// it is liked by author
				So(cmc.Interactions["like"].IsInteracted, ShouldBeTrue)

				// actor length should be 1
				So(len(cmc.Interactions["like"].Actors), ShouldEqual, 1)

				err = deleteInteraction("like", post.Id, account.Id)
				So(err, ShouldBeNil)

				cmc, err = getPostWithRelatedData(post.Id, post.AccountId, groupName)
				So(err, ShouldBeNil)
				So(cmc, ShouldNotBeNil)

				// it is liked by author
				So(cmc.Interactions["like"].IsInteracted, ShouldBeFalse)

				// actor length should be 1
				So(len(cmc.Interactions["like"].Actors), ShouldEqual, 0)
			})

			Convey("owner can post reply to message", func() {
				post, err := createPost(groupChannel.Id, account.Id)
				So(err, ShouldBeNil)
				So(post, ShouldNotBeNil)

				reply, err := addReply(post.Id, post.AccountId)
				So(err, ShouldBeNil)
				So(reply, ShouldNotBeNil)

				So(reply.AccountId, ShouldEqual, post.AccountId)

				cmc, err := getPostWithRelatedData(post.Id, post.AccountId, groupName)
				So(err, ShouldBeNil)
				So(cmc, ShouldNotBeNil)

				So(len(cmc.Replies), ShouldEqual, 1)

				So(cmc.Replies[0].Message.AccountId, ShouldEqual, post.AccountId)

			})

			Convey("non-owner can post reply to message", func() {
				post, err := createPost(groupChannel.Id, account.Id)
				So(err, ShouldBeNil)
				So(post, ShouldNotBeNil)

				reply, err := addReply(post.Id, nonOwnerAccount.Id)
				So(err, ShouldBeNil)
				So(reply, ShouldNotBeNil)

				So(reply.AccountId, ShouldEqual, nonOwnerAccount.Id)

				cmc, err := getPostWithRelatedData(post.Id, post.AccountId, groupName)
				So(err, ShouldBeNil)
				So(cmc, ShouldNotBeNil)

				So(len(cmc.Replies), ShouldEqual, 1)

				So(cmc.Replies[0].Message.AccountId, ShouldEqual, nonOwnerAccount.Id)
			})

			Convey("reply can be liked", func() {
				post, err := createPost(groupChannel.Id, account.Id)
				So(err, ShouldBeNil)
				So(post, ShouldNotBeNil)

				fmt.Println(post.Id, account.Id, nonOwnerAccount.Id)
				reply, err := addReply(post.Id, nonOwnerAccount.Id)
				So(err, ShouldBeNil)
				So(reply, ShouldNotBeNil)

				So(reply.AccountId, ShouldEqual, nonOwnerAccount.Id)

				err = addInteraction("like", reply.Id, nonOwnerAccount.Id)
				So(err, ShouldBeNil)

				cmc, err := getPostWithRelatedData(post.Id, account.Id, groupName)
				So(err, ShouldBeNil)
				So(cmc, ShouldNotBeNil)

				// it is liked by reply author, not post owner
				So(cmc.Interactions["like"].IsInteracted, ShouldBeFalse)

				// we didnt like the post, we liked the reply
				So(len(cmc.Interactions["like"].Actors), ShouldEqual, 0)

				So(len(cmc.Replies), ShouldEqual, 1)

				// we liked the reply
				So(len(cmc.Replies[0].Interactions["like"].Actors), ShouldEqual, 1)

				So(cmc.Replies[0].Interactions["like"].IsInteracted, ShouldBeFalse)

			})
		})
	})
}

func createPost(channelId, accountId int64) (*models.ChannelMessage, error) {
	return createPostWithBody(channelId, accountId, "create a message")
}

func createPostWithBody(channelId, accountId int64, body string) (*models.ChannelMessage, error) {
	cm := models.NewChannelMessage()
	cm.Body = body
	cm.AccountId = accountId

	url := fmt.Sprintf("/channel/%d/message", channelId)
	cmI, err := sendModel("POST", url, cm)
	if err != nil {
		return nil, err
	}
	return cmI.(*models.ChannelMessage), nil
}

func updatePost(cm *models.ChannelMessage) (*models.ChannelMessage, error) {
	cm.Body = "after update"

	url := fmt.Sprintf("/message/%d", cm.Id)
	cmI, err := sendModel("POST", url, cm)
	if err != nil {
		return nil, err
	}

	return cmI.(*models.ChannelMessage), nil
}

func getPost(id int64, accountId int64, groupName string) (*models.ChannelMessage, error) {
	url := fmt.Sprintf("/message/%d?accountId=%d&groupName=%s", id, accountId, groupName)
	cm := models.NewChannelMessage()
	cmI, err := sendModel("GET", url, cm)
	if err != nil {
		return nil, err
	}
	return cmI.(*models.ChannelMessage), nil
}

func getPostWithRelatedData(id int64, accountId int64, groupName string) (*models.ChannelMessageContainer, error) {
	url := fmt.Sprintf("/message/%d/related?accountId=%d&groupName=%s", id, accountId, groupName)
	cm := models.NewChannelMessageContainer()
	cmI, err := sendModel("GET", url, cm)
	if err != nil {
		return nil, err
	}
	return cmI.(*models.ChannelMessageContainer), nil
}

func deletePost(id int64, accountId int64, groupName string) error {
	url := fmt.Sprintf("/message/%d?accountId=%d&groupName=%s", id, accountId, groupName)
	_, err := sendRequest("DELETE", url, nil)
	return err
}

func addInteraction(interactionType string, postId, accountId int64) error {
	cm := models.NewInteraction()
	cm.AccountId = accountId
	cm.MessageId = postId

	url := fmt.Sprintf("/message/%d/interaction/%s/add", postId, interactionType)
	_, err := sendModel("POST", url, cm)
	if err != nil {
		return err
	}
	return nil
}

func getInteractions(interactionType string, postId int64) ([]int64, error) {
	url := fmt.Sprintf("/message/%d/interaction/%s", postId, interactionType)
	res, err := sendRequest("GET", url, nil)
	if err != nil {
		return nil, err
	}

	var interactions []int64
	err = json.Unmarshal(res, &interactions)
	if err != nil {
		return nil, err
	}

	return interactions, nil
}

func deleteInteraction(interactionType string, postId, accountId int64) error {
	cm := models.NewInteraction()
	cm.AccountId = accountId
	cm.MessageId = postId

	url := fmt.Sprintf("/message/%d/interaction/%s/delete", postId, interactionType)
	_, err := marshallAndSendRequest("POST", url, cm)
	if err != nil {
		return err
	}
	return nil
}
<commit_msg>Social: add test for fetching replies of a message<commit_after>package main

import (
	"encoding/json"
	"fmt"
	"math/rand"
	"socialapi/models"
	"strconv"
	"testing"
	. "github.com/smartystreets/goconvey/convey"
)

func TestChannelMessage(t *testing.T) {
	Convey("While testing channel messages given a channel", t, func() {
		groupName := "testgroup" + strconv.FormatInt(rand.Int63(), 10)

		Convey("first we should create the account", func() {
			account := models.NewAccount()
			account.OldId = AccountOldId.Hex()
			account, err := createAccount(account)
			So(err, ShouldBeNil)
			So(account, ShouldNotBeNil)

			nonOwnerAccount := models.NewAccount()
			nonOwnerAccount.OldId = AccountOldId2.Hex()
			nonOwnerAccount, err = createAccount(nonOwnerAccount)
			So(err, ShouldBeNil)
			So(nonOwnerAccount, ShouldNotBeNil)

			groupChannel, err := createChannelByGroupNameAndType(account.Id, groupName, models.Channel_TYPE_GROUP)
			So(err, ShouldBeNil)
			So(groupChannel, ShouldNotBeNil)

			Convey("message should be able added to the group channel", func() {
				post, err := createPost(groupChannel.Id, account.Id)
				So(err, ShouldBeNil)
				So(post, ShouldNotBeNil)
				So(post.Id, ShouldNotEqual, 0)
				So(post.Body, ShouldNotEqual, "")
				Convey("message can be edited by owner", func() {

					initialPostBody := post.Body
					post.Body = "edited message"

					editedPost, err := updatePost(post)
					So(err, ShouldBeNil)
					So(editedPost, ShouldNotBeNil)
					// body should not be same
					So(initialPostBody, ShouldNotEqual, editedPost.Body)
				})

				// for now social worker handles this issue
				Convey("message can be edited by an admin", nil)
				Convey("message can not be edited by non-owner", nil)

			})

			Convey("message can be deleted by owner", func() {
				post, err := createPost(groupChannel.Id, account.Id)
				So(err, ShouldBeNil)
				So(post, ShouldNotBeNil)
				err = deletePost(post.Id, account.Id, groupChannel.GroupName)
				So(err, ShouldBeNil)
				post2, err := getPost(post.Id, account.Id, groupChannel.GroupName)
				So(err, ShouldNotBeNil)
				So(post2, ShouldBeNil)
			})

			// handled by social worker
			Convey("message can be deleted by an admin", nil)
			Convey("message can not be edited by non-owner", nil)

			Convey("owner can like message", func() {
				post, err := createPost(groupChannel.Id, account.Id)
				So(err, ShouldBeNil)
				So(post, ShouldNotBeNil)

				err = addInteraction("like", post.Id, post.AccountId)
				So(err, ShouldBeNil)

				cmc, err := getPostWithRelatedData(post.Id, post.AccountId, groupName)
				So(err, ShouldBeNil)
				So(cmc, ShouldNotBeNil)

				// it is liked by author
				So(cmc.Interactions["like"].IsInteracted, ShouldBeTrue)

				// actor length should be 1
				So(len(cmc.Interactions["like"].Actors), ShouldEqual, 1)

			})

			Convey("non-owner can like message", func() {
				post, err := createPost(groupChannel.Id, account.Id)
				So(err, ShouldBeNil)
				So(post, ShouldNotBeNil)

				err = addInteraction("like", post.Id, nonOwnerAccount.Id)
				So(err, ShouldBeNil)

				cmc, err := getPostWithRelatedData(post.Id, nonOwnerAccount.Id, groupName)
				So(err, ShouldBeNil)
				So(cmc, ShouldNotBeNil)

				// it is liked by author
				So(cmc.Interactions["like"].IsInteracted, ShouldBeTrue)

				// actor length should be 1
				So(len(cmc.Interactions["like"].Actors), ShouldEqual, 1)

			})

			Convey("we should be able to get only interactions", func() {
				post, err := createPost(groupChannel.Id, account.Id)
				So(err, ShouldBeNil)
				So(post, ShouldNotBeNil)

				err = addInteraction("like", post.Id, nonOwnerAccount.Id)
				So(err, ShouldBeNil)

				likes, err := getInteractions("like", post.Id)
				So(err, ShouldBeNil)

				So(len(likes), ShouldEqual, 1)

			})

			Convey("users should be able to  un-like message", func() {
				post, err := createPost(groupChannel.Id, account.Id)
				So(err, ShouldBeNil)
				So(post, ShouldNotBeNil)

				err = addInteraction("like", post.Id, post.AccountId)
				So(err, ShouldBeNil)

				cmc, err := getPostWithRelatedData(post.Id, post.AccountId, groupName)
				So(err, ShouldBeNil)
				So(cmc, ShouldNotBeNil)

				// it is liked by author
				So(cmc.Interactions["like"].IsInteracted, ShouldBeTrue)

				// actor length should be 1
				So(len(cmc.Interactions["like"].Actors), ShouldEqual, 1)

				err = deleteInteraction("like", post.Id, account.Id)
				So(err, ShouldBeNil)

				cmc, err = getPostWithRelatedData(post.Id, post.AccountId, groupName)
				So(err, ShouldBeNil)
				So(cmc, ShouldNotBeNil)

				// it is liked by author
				So(cmc.Interactions["like"].IsInteracted, ShouldBeFalse)

				// actor length should be 1
				So(len(cmc.Interactions["like"].Actors), ShouldEqual, 0)
			})

			Convey("owner can post reply to message", func() {
				post, err := createPost(groupChannel.Id, account.Id)
				So(err, ShouldBeNil)
				So(post, ShouldNotBeNil)

				reply, err := addReply(post.Id, post.AccountId)
				So(err, ShouldBeNil)
				So(reply, ShouldNotBeNil)

				So(reply.AccountId, ShouldEqual, post.AccountId)

				cmc, err := getPostWithRelatedData(post.Id, post.AccountId, groupName)
				So(err, ShouldBeNil)
				So(cmc, ShouldNotBeNil)

				So(len(cmc.Replies), ShouldEqual, 1)

				So(cmc.Replies[0].Message.AccountId, ShouldEqual, post.AccountId)

			})

			Convey("we should be able to get only replies", func() {
				post, err := createPost(groupChannel.Id, account.Id)
				So(err, ShouldBeNil)
				So(post, ShouldNotBeNil)

				reply, err := addReply(post.Id, post.AccountId)
				So(err, ShouldBeNil)
				So(reply, ShouldNotBeNil)

				reply, err = addReply(post.Id, post.AccountId)
				So(err, ShouldBeNil)
				So(reply, ShouldNotBeNil)

				replies, err := getReplies(post.Id, post.AccountId, groupName)
				So(err, ShouldBeNil)
				So(len(replies), ShouldEqual, 2)

			})

			Convey("non-owner can post reply to message", func() {
				post, err := createPost(groupChannel.Id, account.Id)
				So(err, ShouldBeNil)
				So(post, ShouldNotBeNil)

				reply, err := addReply(post.Id, nonOwnerAccount.Id)
				So(err, ShouldBeNil)
				So(reply, ShouldNotBeNil)

				So(reply.AccountId, ShouldEqual, nonOwnerAccount.Id)

				cmc, err := getPostWithRelatedData(post.Id, post.AccountId, groupName)
				So(err, ShouldBeNil)
				So(cmc, ShouldNotBeNil)

				So(len(cmc.Replies), ShouldEqual, 1)

				So(cmc.Replies[0].Message.AccountId, ShouldEqual, nonOwnerAccount.Id)
			})

			Convey("reply can be liked", func() {
				post, err := createPost(groupChannel.Id, account.Id)
				So(err, ShouldBeNil)
				So(post, ShouldNotBeNil)

				fmt.Println(post.Id, account.Id, nonOwnerAccount.Id)
				reply, err := addReply(post.Id, nonOwnerAccount.Id)
				So(err, ShouldBeNil)
				So(reply, ShouldNotBeNil)

				So(reply.AccountId, ShouldEqual, nonOwnerAccount.Id)

				err = addInteraction("like", reply.Id, nonOwnerAccount.Id)
				So(err, ShouldBeNil)

				cmc, err := getPostWithRelatedData(post.Id, account.Id, groupName)
				So(err, ShouldBeNil)
				So(cmc, ShouldNotBeNil)

				// it is liked by reply author, not post owner
				So(cmc.Interactions["like"].IsInteracted, ShouldBeFalse)

				// we didnt like the post, we liked the reply
				So(len(cmc.Interactions["like"].Actors), ShouldEqual, 0)

				So(len(cmc.Replies), ShouldEqual, 1)

				// we liked the reply
				So(len(cmc.Replies[0].Interactions["like"].Actors), ShouldEqual, 1)

				So(cmc.Replies[0].Interactions["like"].IsInteracted, ShouldBeFalse)

			})
		})
	})
}

func createPost(channelId, accountId int64) (*models.ChannelMessage, error) {
	return createPostWithBody(channelId, accountId, "create a message")
}

func createPostWithBody(channelId, accountId int64, body string) (*models.ChannelMessage, error) {
	cm := models.NewChannelMessage()
	cm.Body = body
	cm.AccountId = accountId

	url := fmt.Sprintf("/channel/%d/message", channelId)
	cmI, err := sendModel("POST", url, cm)
	if err != nil {
		return nil, err
	}
	return cmI.(*models.ChannelMessage), nil
}

func updatePost(cm *models.ChannelMessage) (*models.ChannelMessage, error) {
	cm.Body = "after update"

	url := fmt.Sprintf("/message/%d", cm.Id)
	cmI, err := sendModel("POST", url, cm)
	if err != nil {
		return nil, err
	}

	return cmI.(*models.ChannelMessage), nil
}

func getPost(id int64, accountId int64, groupName string) (*models.ChannelMessage, error) {
	url := fmt.Sprintf("/message/%d?accountId=%d&groupName=%s", id, accountId, groupName)
	cm := models.NewChannelMessage()
	cmI, err := sendModel("GET", url, cm)
	if err != nil {
		return nil, err
	}
	return cmI.(*models.ChannelMessage), nil
}

func getPostWithRelatedData(id int64, accountId int64, groupName string) (*models.ChannelMessageContainer, error) {
	url := fmt.Sprintf("/message/%d/related?accountId=%d&groupName=%s", id, accountId, groupName)
	cm := models.NewChannelMessageContainer()
	cmI, err := sendModel("GET", url, cm)
	if err != nil {
		return nil, err
	}
	return cmI.(*models.ChannelMessageContainer), nil
}

func deletePost(id int64, accountId int64, groupName string) error {
	url := fmt.Sprintf("/message/%d?accountId=%d&groupName=%s", id, accountId, groupName)
	_, err := sendRequest("DELETE", url, nil)
	return err
}

func addInteraction(interactionType string, postId, accountId int64) error {
	cm := models.NewInteraction()
	cm.AccountId = accountId
	cm.MessageId = postId

	url := fmt.Sprintf("/message/%d/interaction/%s/add", postId, interactionType)
	_, err := sendModel("POST", url, cm)
	if err != nil {
		return err
	}
	return nil
}

func getInteractions(interactionType string, postId int64) ([]int64, error) {
	url := fmt.Sprintf("/message/%d/interaction/%s", postId, interactionType)
	res, err := sendRequest("GET", url, nil)
	if err != nil {
		return nil, err
	}

	var interactions []int64
	err = json.Unmarshal(res, &interactions)
	if err != nil {
		return nil, err
	}

	return interactions, nil
}

func deleteInteraction(interactionType string, postId, accountId int64) error {
	cm := models.NewInteraction()
	cm.AccountId = accountId
	cm.MessageId = postId

	url := fmt.Sprintf("/message/%d/interaction/%s/delete", postId, interactionType)
	_, err := marshallAndSendRequest("POST", url, cm)
	if err != nil {
		return err
	}
	return nil
}
<|endoftext|>


<commit_before>package main

import (
	"fmt"
	"socialapi/workers/common/manager"
	"socialapi/workers/common/runner"
	"socialapi/workers/followingfeed/followingfeed"
)

var (
	Name = "FollowingFeed"
)

func main() {
	r := runner.New(Name)
	if err := r.Init(); err != nil {
		fmt.Println(err)
		return
	}

	m := manager.New()
	m.Controller(followingfeed.New(r.Log))

	m.HandleFunc("api.channel_message_created", (*followingfeed.Controller).MessageSaved)
	m.HandleFunc("api.channel_message_update", (*followingfeed.Controller).MessageUpdated)
	m.HandleFunc("api.channel_message_deleted", (*followingfeed.Controller).MessageDeleted)

	r.Listen(m)
	r.Wait()
}
<commit_msg>Social: HandleFunc --> ListerFor<commit_after>package main

import (
	"fmt"
	"socialapi/workers/common/runner"
	"socialapi/workers/followingfeed/followingfeed"
)

var (
	Name = "FollowingFeed"
)

func main() {
	r := runner.New(Name)
	if err := r.Init(); err != nil {
		fmt.Println(err)
		return
	}

	r.SetContext(followingfeed.New(r.Log))
	r.ListenFor("api.channel_message_created", (*followingfeed.Controller).MessageSaved)
	r.ListenFor("api.channel_message_update", (*followingfeed.Controller).MessageUpdated)
	r.ListenFor("api.channel_message_deleted", (*followingfeed.Controller).MessageDeleted)
	r.Listen()
	r.Wait()
}
<|endoftext|>


<commit_before>package detailed_test

import (
	"sort"
	"testing"
	"time"

	"github.com/weaveworks/common/mtime"
	"github.com/weaveworks/common/test"
	"github.com/weaveworks/scope/probe/docker"
	"github.com/weaveworks/scope/probe/host"
	"github.com/weaveworks/scope/probe/process"
	"github.com/weaveworks/scope/render"
	"github.com/weaveworks/scope/render/detailed"
	"github.com/weaveworks/scope/render/expected"
	"github.com/weaveworks/scope/report"
	"github.com/weaveworks/scope/test/fixture"
	"github.com/weaveworks/scope/test/reflect"
)

func TestSummaries(t *testing.T) {
	{
		// Just a convenient source of some rendered nodes
		have := detailed.Summaries(report.RenderContext{Report: fixture.Report}, render.ProcessRenderer.Render(fixture.Report).Nodes)
		// The ids of the processes rendered above
		expectedIDs := []string{
			fixture.ClientProcess1NodeID,
			fixture.ClientProcess2NodeID,
			fixture.ServerProcessNodeID,
			fixture.NonContainerProcessNodeID,
			render.IncomingInternetID,
			render.OutgoingInternetID,
		}
		sort.Strings(expectedIDs)

		// It should summarize each node
		ids := []string{}
		for id := range have {
			ids = append(ids, id)
		}
		sort.Strings(ids)
		if !reflect.DeepEqual(expectedIDs, ids) {
			t.Fatalf("Expected Summaries to have summarized every node in the process renderer: %v, but got %v", expectedIDs, ids)
		}
	}

	// It should summarize nodes' metrics
	{
		t1, t2 := mtime.Now().Add(-1*time.Minute), mtime.Now()
		metric := report.MakeMetric([]report.Sample{{Timestamp: t1, Value: 1}, {Timestamp: t2, Value: 2}})
		input := fixture.Report.Copy()

		input.Process.Nodes[fixture.ClientProcess1NodeID].Metrics[process.CPUUsage] = metric
		have := detailed.Summaries(report.RenderContext{Report: input}, render.ProcessRenderer.Render(input).Nodes)

		node, ok := have[fixture.ClientProcess1NodeID]
		if !ok {
			t.Fatalf("Expected output to have the node we added the metric to")
		}

		var row report.MetricRow
		ok = false
		for _, metric := range node.Metrics {
			if metric.ID == process.CPUUsage {
				row = metric
				ok = true
				break
			}
		}
		if !ok {
			t.Fatalf("Expected node to have the metric we added")
		}

		// Our summarized MetricRow
		want := report.MetricRow{
			ID:       process.CPUUsage,
			Label:    "CPU",
			Format:   "percent",
			Value:    2,
			Priority: 1,
			Metric: &report.Metric{
				Samples: nil,
				Min:     metric.Min,
				Max:     metric.Max,
				First:   metric.First,
				Last:    metric.Last,
			},
		}
		if !reflect.DeepEqual(want, row) {
			t.Fatalf("Expected to have summarized the node's metrics: %s", test.Diff(want, row))
		}
	}
}

func TestMakeNodeSummary(t *testing.T) {
	testcases := []struct {
		name  string
		input report.Node
		ok    bool
		want  detailed.NodeSummary
	}{
		{
			name:  "single process rendering",
			input: expected.RenderedProcesses[fixture.ClientProcess1NodeID],
			ok:    true,
			want: detailed.NodeSummary{
				ID:         fixture.ClientProcess1NodeID,
				Label:      fixture.Client1Name,
				LabelMinor: "client.hostname.com (10001)",
				Rank:       fixture.Client1Name,
				Shape:      "square",
				Metadata: []report.MetadataRow{
					{ID: process.PID, Label: "PID", Value: fixture.Client1PID, Priority: 1, Datatype: report.Number},
				},
				Adjacency: report.MakeIDList(fixture.ServerProcessNodeID),
			},
		},
		{
			name:  "single container rendering",
			input: expected.RenderedContainers[fixture.ClientContainerNodeID],
			ok:    true,
			want: detailed.NodeSummary{
				ID:         fixture.ClientContainerNodeID,
				Label:      fixture.ClientContainerName,
				LabelMinor: fixture.ClientHostName,
				Rank:       fixture.ClientContainerImageName,
				Shape:      "hexagon",
				Linkable:   true,
				Metadata: []report.MetadataRow{
					{ID: docker.ImageName, Label: "Image", Value: fixture.ClientContainerImageName, Priority: 1},
					{ID: docker.ContainerID, Label: "ID", Value: fixture.ClientContainerID, Priority: 10, Truncate: 12},
				},
				Adjacency: report.MakeIDList(fixture.ServerContainerNodeID),
			},
		},
		{
			name:  "single container image rendering",
			input: expected.RenderedContainerImages[expected.ClientContainerImageNodeID],
			ok:    true,
			want: detailed.NodeSummary{
				ID:         expected.ClientContainerImageNodeID,
				Label:      fixture.ClientContainerImageName,
				LabelMinor: "1 container",
				Rank:       fixture.ClientContainerImageName,
				Shape:      "hexagon",
				Linkable:   true,
				Stack:      true,
				Metadata: []report.MetadataRow{
					{ID: report.Container, Label: "# Containers", Value: "1", Priority: 2, Datatype: report.Number},
				},
				Adjacency: report.MakeIDList(expected.ServerContainerImageNodeID),
			},
		},
		{
			name:  "single host rendering",
			input: expected.RenderedHosts[fixture.ClientHostNodeID],
			ok:    true,
			want: detailed.NodeSummary{
				ID:         fixture.ClientHostNodeID,
				Label:      "client",
				LabelMinor: "hostname.com",
				Rank:       "hostname.com",
				Shape:      "circle",
				Linkable:   true,
				Metadata: []report.MetadataRow{
					{ID: host.HostName, Label: "Hostname", Value: fixture.ClientHostName, Priority: 11},
				},
				Adjacency: report.MakeIDList(fixture.ServerHostNodeID),
			},
		},
		{
			name:  "group node rendering",
			input: expected.RenderedProcessNames[fixture.ServerName],
			ok:    true,
			want: detailed.NodeSummary{
				ID:         "apache",
				Label:      "apache",
				LabelMinor: "1 process",
				Rank:       "apache",
				Shape:      "square",
				Stack:      true,
				Linkable:   true,
			},
		},
	}
	for _, testcase := range testcases {
		have, ok := detailed.MakeNodeSummary(report.RenderContext{Report: fixture.Report}, testcase.input)
		if ok != testcase.ok {
			t.Errorf("%s: MakeNodeSummary failed: expected ok value to be: %v", testcase.name, testcase.ok)
			continue
		}

		if !reflect.DeepEqual(testcase.want, have) {
			t.Errorf("%s: Node Summary did not match: %s", testcase.name, test.Diff(testcase.want, have))
		}
	}
}
<commit_msg>fix accidental report fixture modification<commit_after>package detailed_test

import (
	"sort"
	"testing"
	"time"

	"github.com/weaveworks/common/mtime"
	"github.com/weaveworks/common/test"
	"github.com/weaveworks/scope/probe/docker"
	"github.com/weaveworks/scope/probe/host"
	"github.com/weaveworks/scope/probe/process"
	"github.com/weaveworks/scope/render"
	"github.com/weaveworks/scope/render/detailed"
	"github.com/weaveworks/scope/render/expected"
	"github.com/weaveworks/scope/report"
	"github.com/weaveworks/scope/test/fixture"
	"github.com/weaveworks/scope/test/reflect"
)

func TestSummaries(t *testing.T) {
	{
		// Just a convenient source of some rendered nodes
		have := detailed.Summaries(report.RenderContext{Report: fixture.Report}, render.ProcessRenderer.Render(fixture.Report).Nodes)
		// The ids of the processes rendered above
		expectedIDs := []string{
			fixture.ClientProcess1NodeID,
			fixture.ClientProcess2NodeID,
			fixture.ServerProcessNodeID,
			fixture.NonContainerProcessNodeID,
			render.IncomingInternetID,
			render.OutgoingInternetID,
		}
		sort.Strings(expectedIDs)

		// It should summarize each node
		ids := []string{}
		for id := range have {
			ids = append(ids, id)
		}
		sort.Strings(ids)
		if !reflect.DeepEqual(expectedIDs, ids) {
			t.Fatalf("Expected Summaries to have summarized every node in the process renderer: %v, but got %v", expectedIDs, ids)
		}
	}

	// It should summarize nodes' metrics
	{
		t1, t2 := mtime.Now().Add(-1*time.Minute), mtime.Now()
		metric := report.MakeMetric([]report.Sample{{Timestamp: t1, Value: 1}, {Timestamp: t2, Value: 2}})
		input := fixture.Report.Copy()
		processNode := input.Process.Nodes[fixture.ClientProcess1NodeID]
		processNode.Metrics = processNode.Metrics.Copy()
		processNode.Metrics[process.CPUUsage] = metric
		input.Process.Nodes[fixture.ClientProcess1NodeID] = processNode
		have := detailed.Summaries(report.RenderContext{Report: input}, render.ProcessRenderer.Render(input).Nodes)

		node, ok := have[fixture.ClientProcess1NodeID]
		if !ok {
			t.Fatalf("Expected output to have the node we added the metric to")
		}

		var row report.MetricRow
		ok = false
		for _, metric := range node.Metrics {
			if metric.ID == process.CPUUsage {
				row = metric
				ok = true
				break
			}
		}
		if !ok {
			t.Fatalf("Expected node to have the metric we added")
		}

		// Our summarized MetricRow
		want := report.MetricRow{
			ID:       process.CPUUsage,
			Label:    "CPU",
			Format:   "percent",
			Value:    2,
			Priority: 1,
			Metric: &report.Metric{
				Samples: nil,
				Min:     metric.Min,
				Max:     metric.Max,
				First:   metric.First,
				Last:    metric.Last,
			},
		}
		if !reflect.DeepEqual(want, row) {
			t.Fatalf("Expected to have summarized the node's metrics: %s", test.Diff(want, row))
		}
	}
}

func TestMakeNodeSummary(t *testing.T) {
	testcases := []struct {
		name  string
		input report.Node
		ok    bool
		want  detailed.NodeSummary
	}{
		{
			name:  "single process rendering",
			input: expected.RenderedProcesses[fixture.ClientProcess1NodeID],
			ok:    true,
			want: detailed.NodeSummary{
				ID:         fixture.ClientProcess1NodeID,
				Label:      fixture.Client1Name,
				LabelMinor: "client.hostname.com (10001)",
				Rank:       fixture.Client1Name,
				Shape:      "square",
				Metadata: []report.MetadataRow{
					{ID: process.PID, Label: "PID", Value: fixture.Client1PID, Priority: 1, Datatype: report.Number},
				},
				Adjacency: report.MakeIDList(fixture.ServerProcessNodeID),
			},
		},
		{
			name:  "single container rendering",
			input: expected.RenderedContainers[fixture.ClientContainerNodeID],
			ok:    true,
			want: detailed.NodeSummary{
				ID:         fixture.ClientContainerNodeID,
				Label:      fixture.ClientContainerName,
				LabelMinor: fixture.ClientHostName,
				Rank:       fixture.ClientContainerImageName,
				Shape:      "hexagon",
				Linkable:   true,
				Metadata: []report.MetadataRow{
					{ID: docker.ImageName, Label: "Image", Value: fixture.ClientContainerImageName, Priority: 1},
					{ID: docker.ContainerID, Label: "ID", Value: fixture.ClientContainerID, Priority: 10, Truncate: 12},
				},
				Adjacency: report.MakeIDList(fixture.ServerContainerNodeID),
			},
		},
		{
			name:  "single container image rendering",
			input: expected.RenderedContainerImages[expected.ClientContainerImageNodeID],
			ok:    true,
			want: detailed.NodeSummary{
				ID:         expected.ClientContainerImageNodeID,
				Label:      fixture.ClientContainerImageName,
				LabelMinor: "1 container",
				Rank:       fixture.ClientContainerImageName,
				Shape:      "hexagon",
				Linkable:   true,
				Stack:      true,
				Metadata: []report.MetadataRow{
					{ID: report.Container, Label: "# Containers", Value: "1", Priority: 2, Datatype: report.Number},
				},
				Adjacency: report.MakeIDList(expected.ServerContainerImageNodeID),
			},
		},
		{
			name:  "single host rendering",
			input: expected.RenderedHosts[fixture.ClientHostNodeID],
			ok:    true,
			want: detailed.NodeSummary{
				ID:         fixture.ClientHostNodeID,
				Label:      "client",
				LabelMinor: "hostname.com",
				Rank:       "hostname.com",
				Shape:      "circle",
				Linkable:   true,
				Metadata: []report.MetadataRow{
					{ID: host.HostName, Label: "Hostname", Value: fixture.ClientHostName, Priority: 11},
				},
				Adjacency: report.MakeIDList(fixture.ServerHostNodeID),
			},
		},
		{
			name:  "group node rendering",
			input: expected.RenderedProcessNames[fixture.ServerName],
			ok:    true,
			want: detailed.NodeSummary{
				ID:         "apache",
				Label:      "apache",
				LabelMinor: "1 process",
				Rank:       "apache",
				Shape:      "square",
				Stack:      true,
				Linkable:   true,
			},
		},
	}
	for _, testcase := range testcases {
		have, ok := detailed.MakeNodeSummary(report.RenderContext{Report: fixture.Report}, testcase.input)
		if ok != testcase.ok {
			t.Errorf("%s: MakeNodeSummary failed: expected ok value to be: %v", testcase.name, testcase.ok)
			continue
		}

		if !reflect.DeepEqual(testcase.want, have) {
			t.Errorf("%s: Node Summary did not match: %s", testcase.name, test.Diff(testcase.want, have))
		}
	}
}
<|endoftext|>


<commit_before>package main

import  (
        "fmt"
        "github.com/spf13/cobra"
        "github.com/rakyll/globalconf"
        "../go-notetxt"
        "flag"
        "os/user"
        "time"
        "strings"
        "strconv"
)

func main() {

        conf, _ := globalconf.New("gonote")

        var flagNotedir = flag.String("dir", "", "Location of the note.txt directory.")
        var dir string

        var today bool

        var cmdAdd = &cobra.Command{
            Use:   "add [title] [tag]",
            Short: "Add a note.",
            Long:  `Add a note and tag it.`,
            Run: func(cmd *cobra.Command, args []string) {
                if len(args) < 1 && !today {
                        fmt.Println("I need something to add.")
                        return
                }

                var text string
                t := time.Now().Local()

                if today {
                        text = fmt.Sprintf("Daily journal, date %s", t.Format("02. 01. 2006"))
                } else {
                        text = strings.Join(args, " ")
                }

                file, err := notetxt.CreateNote(text, t.Format("2006/01/"), dir)
                if err != nil {
                        panic(err);
                }

                notetxt.OpenFileInEditor(file)
            },
        }
        cmdAdd.Flags().BoolVarP(&today, "today", "T", false,
                                 "Add today's journal entry.")

        var cmdList = &cobra.Command{
            Use:   "list",
            Short: "List notes.",
            Long:  `List all valid note files in the directory.`,
            Run: func(cmd *cobra.Command, args []string) {
                notes, err := notetxt.ParseDir(dir)
                if err != nil {
                    panic(err)
                }

                needle := strings.Join(args, " ")

                for i, note := range notes {
                    if note.Matches(needle) {
                        fmt.Printf("%d %s - %v\n", i, note.Name, note.Tags)
                    }
                }
            },
        }

        var cmdEdit = &cobra.Command{
            Use:   "edit <id>|<selector>",
            Short: "Edit notes.",
            Long:  `Edit a note identified by either an ID or a selector.`,
            Run: func(cmd *cobra.Command, args []string) {
                if len(args) < 1 {
                        fmt.Println("Either a note ID or a selector is required.")
                        return
                }

                noteid, err := strconv.Atoi(args[0])
                if err != nil {
                        fmt.Printf("Notes matching your selector:\n")
                        cmdList.Run(cmd, args)
                        return
                }

                notes, err := notetxt.ParseDir(dir)
                if err != nil {
                    panic(err)
                }

                if noteid > len(notes) || noteid < 0 {
                        fmt.Printf("Invalid note ID (%v)\n", noteid)
                        return
                }

                notetxt.OpenFileInEditor(notes[noteid].Filename)

            },
        }

        var cmdTag = &cobra.Command{
            Use:   "tag <noteid> <tag-name>",
            Short: "Attaches a tag to a note.",
            Long:  `Tags a note with a one or more tags.`,
            Run: func(cmd *cobra.Command, args []string) {
                if len(args) < 2 {
                        fmt.Printf("Too few arguments.")
                }

                notes, err := notetxt.ParseDir(dir)
                if err != nil {
                    panic(err)
                }

                noteid, err := strconv.Atoi(args[0])
                if err != nil {
                        fmt.Printf("Do you really consider that a number? %v\n", err)
                        return
                }

                if noteid > len(notes) || noteid < 0 {
                        fmt.Printf("Invalid note ID (%v)\n", noteid)
                        return
                }

                file := notes[noteid].Filename
                tag := args[1]
                err = notetxt.TagNote(file, tag, dir)
                if err != nil {
                        panic(err)
                }

            },
        }

        var GonoterCmd = &cobra.Command{
            Use:   "gonote",
            Short: "gonote is a go implementation of note.txt specification.",
            Long: `A small, fast and fun implementation of note.txt`,
            Run: func(cmd *cobra.Command, args []string) {
            },
        }

        GonoterCmd.PersistentFlags().StringVarP(&dir, "directory", "", "",
                                     "Location of the note.txt directory.")

        conf.ParseAll()
        if dir == "" {
                if *flagNotedir == "" {
                        usr, err := user.Current()
                        if err != nil {
                                panic(err)
                        }

                        dir = usr.HomeDir + "/notes"
                } else {
                        dir = *flagNotedir
                }
        }

        GonoterCmd.AddCommand(cmdAdd)
        GonoterCmd.AddCommand(cmdList)
        GonoterCmd.AddCommand(cmdTag)
        GonoterCmd.AddCommand(cmdEdit)
        GonoterCmd.Execute()
}
<commit_msg>update: `ls` as the default<commit_after>package main

import  (
        "fmt"
        "github.com/spf13/cobra"
        "github.com/rakyll/globalconf"
        "../go-notetxt"
        "flag"
        "os/user"
        "time"
        "strings"
        "strconv"
)

func main() {

        conf, _ := globalconf.New("gonote")

        var flagNotedir = flag.String("dir", "", "Location of the note.txt directory.")
        var dir string

        var today bool

        var cmdAdd = &cobra.Command{
            Use:   "add [title] [tag]",
            Short: "Add a note.",
            Long:  `Add a note and tag it.`,
            Run: func(cmd *cobra.Command, args []string) {
                if len(args) < 1 && !today {
                        fmt.Println("I need something to add.")
                        return
                }

                var text string
                t := time.Now().Local()

                if today {
                        text = fmt.Sprintf("Daily journal, date %s", t.Format("02. 01. 2006"))
                } else {
                        text = strings.Join(args, " ")
                }

                file, err := notetxt.CreateNote(text, t.Format("2006/01/"), dir)
                if err != nil {
                        panic(err);
                }

                notetxt.OpenFileInEditor(file)
            },
        }
        cmdAdd.Flags().BoolVarP(&today, "today", "T", false,
                                 "Add today's journal entry.")

        var cmdList = &cobra.Command{
            Use:   "ls",
            Short: "List notes.",
            Long:  `List all valid note files in the directory.`,
            Run: func(cmd *cobra.Command, args []string) {
                notes, err := notetxt.ParseDir(dir)
                if err != nil {
                    panic(err)
                }

                needle := strings.Join(args, " ")

                for i, note := range notes {
                    if note.Matches(needle) {
                        fmt.Printf("%d %s - %v\n", i, note.Name, note.Tags)
                    }
                }
            },
        }

        var cmdEdit = &cobra.Command{
            Use:   "edit <id>|<selector>",
            Short: "Edit notes.",
            Long:  `Edit a note identified by either an ID or a selector.`,
            Run: func(cmd *cobra.Command, args []string) {
                if len(args) < 1 {
                        fmt.Println("Either a note ID or a selector is required.")
                        return
                }

                noteid, err := strconv.Atoi(args[0])
                if err != nil {
                        fmt.Printf("Notes matching your selector:\n")
                        cmdList.Run(cmd, args)
                        return
                }

                notes, err := notetxt.ParseDir(dir)
                if err != nil {
                    panic(err)
                }

                if noteid > len(notes) || noteid < 0 {
                        fmt.Printf("Invalid note ID (%v)\n", noteid)
                        return
                }

                notetxt.OpenFileInEditor(notes[noteid].Filename)

            },
        }

        var cmdTag = &cobra.Command{
            Use:   "tag <noteid> <tag-name>",
            Short: "Attaches a tag to a note.",
            Long:  `Tags a note with a one or more tags.`,
            Run: func(cmd *cobra.Command, args []string) {
                if len(args) < 2 {
                        fmt.Printf("Too few arguments.")
                }

                notes, err := notetxt.ParseDir(dir)
                if err != nil {
                    panic(err)
                }

                noteid, err := strconv.Atoi(args[0])
                if err != nil {
                        fmt.Printf("Do you really consider that a number? %v\n", err)
                        return
                }

                if noteid > len(notes) || noteid < 0 {
                        fmt.Printf("Invalid note ID (%v)\n", noteid)
                        return
                }

                file := notes[noteid].Filename
                tag := args[1]
                err = notetxt.TagNote(file, tag, dir)
                if err != nil {
                        panic(err)
                }

            },
        }

        var GonoterCmd = &cobra.Command{
            Use:   "gonote",
            Short: "gonote is a go implementation of note.txt specification.",
            Long: `A small, fast and fun implementation of note.txt`,
            Run: func(cmd *cobra.Command, args []string) {
                cmdList.Run(cmd, args)
            },
        }

        GonoterCmd.PersistentFlags().StringVarP(&dir, "directory", "", "",
                                     "Location of the note.txt directory.")

        conf.ParseAll()
        if dir == "" {
                if *flagNotedir == "" {
                        usr, err := user.Current()
                        if err != nil {
                                panic(err)
                        }

                        dir = usr.HomeDir + "/notes"
                } else {
                        dir = *flagNotedir
                }
        }

        GonoterCmd.AddCommand(cmdAdd)
        GonoterCmd.AddCommand(cmdList)
        GonoterCmd.AddCommand(cmdTag)
        GonoterCmd.AddCommand(cmdEdit)
        GonoterCmd.Execute()
}
<|endoftext|>


<commit_before>package gopher

import (
	"github.com/gopherlabs/gopher-framework"
	"github.com/gopherlabs/gopher-services"
)

const (
	LOGGER   = framework.LOGGER
	ROUTER   = framework.ROUTER
	RENDERER = framework.RENDERER
	PARAMS   = framework.PARAMS
	SAMPLE   = framework.SAMPLE
)

var (
	Log     framework.Loggable
	Router  framework.Routable
	Context framework.Mappable
)

func Initialize(config ...framework.Config) *framework.Container {
	appConf := framework.Config{}
	if len(config) > 0 {
		appConf = config[0]
	}
	container := framework.NewContainer(appConf)
	container.Use(framework.LoggerMiddleware)
	registerProviders(container)
	return container
}

func registerProviders(c *framework.Container) {

	c.RegisterProvider(new(services.LogProvider))
	Log = c.Log

	c.RegisterProvider(new(services.MapProvider))
	Context = c.Context

	c.RegisterProvider(new(services.RouteProvider))
	Router = c.Router

	c.RegisterProvider(new(services.ParameterProvider))

	c.RegisterProvider(new(services.RenderProvider))
}

func ListenAndServe() {
	Router.Serve()
}
<commit_msg>Added auto-initialization<commit_after>package gopher

import (
	. "github.com/gopherlabs/gopher-framework"
	"github.com/gopherlabs/gopher-services"
)

//const (
//	LOGGER   = framework.LOGGER
//	ROUTER   = framework.ROUTER
//	RENDERER = framework.RENDERER
//	PARAMS   = framework.PARAMS
//	SAMPLE   = framework.SAMPLE
//)

var (
	log     Loggable
	router  Routable
	context Mappable
)

func Log() Loggable {
	if log == nil {
		initialize()
	}
	return log
}

func Router() Routable {
	if router == nil {
		initialize()
	}
	return router
}

func Context() Mappable {
	if context == nil {
		initialize()
	}
	return context
}

func initialize(config ...Config) *Container {
	appConf := Config{}
	if len(config) > 0 {
		appConf = config[0]
	}
	container := NewContainer(appConf)
	container.Use(LoggerMiddleware)
	registerProviders(container)
	return container
}

func registerProviders(c *Container) {

	c.RegisterProvider(new(services.LogProvider))
	log = c.Log

	c.RegisterProvider(new(services.MapProvider))
	context = c.Context

	c.RegisterProvider(new(services.RouteProvider))
	router = c.Router

	c.RegisterProvider(new(services.ParameterProvider))

	c.RegisterProvider(new(services.RenderProvider))
}

func ListenAndServe() {
	router.Serve()
}
<|endoftext|>


<commit_before>package main

import (
	"flag"
)

var (
	listen = flag.String("listen", "8080", "bla")
	to     = flag.String("to", "", "bla")
)

func runServer() {

}

func runClient() {

}

func main() {
	flag.Parse()

	if *to == "" {
		runServer()
	} else {
		runClient()
	}
}
<commit_msg>server<commit_after>package main

import (
	"encoding/json"
	"flag"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"os/exec"
	"strings"

	"github.com/howeyc/fsnotify"
	"github.com/ungerik/go-quick"
)

var (
	listen string
	to     string
	cmd    string
)

func getCheckSums(path string, checkSums map[string]uint64) error {
	if path == "" {
		path = "."
	}
	if quick.FileIsDir(path) {
		dirFiles, err := ioutil.ReadDir(path)
		for i := 0; i < len(dirFiles) && err == nil; i++ {
			filename := dirFiles[i].Name()
			if filename != "." && filename != ".." {
				err = getCheckSums(path+"/"+filename, checkSums)
			}
		}
		return err
	} else {
		crc, err := quick.FileCRC64(path)
		if err == nil {
			checkSums[strings.TrimPrefix(path, "./")] = crc
		}
		return err
	}
}

func runCmd(response http.ResponseWriter) {
	if cmd == "" {
		return
	}
	split := strings.Split(cmd, " ")
	output, err := exec.Command(split[0], split[1:]...).CombinedOutput()
	if err != nil {
		internalServerError(err, response)
		return
	}
	response.Header().Set("Content-Type", "text/plain")
	response.Write(output)
}

func internalServerError(err error, response http.ResponseWriter) {
	log.Print(err)
	http.Error(response, "Internal server error: "+err.Error(), 500)
}

func serverHandler(response http.ResponseWriter, request *http.Request) {
	path := request.URL.Path[1:] // remove leading slash
	switch request.Method {
	case "GET":
		if !quick.FileExists(path) {
			http.NotFound(response, request)
			return
		}
		checkSums := make(map[string]uint64)
		err := getCheckSums(path, checkSums)
		if err != nil {
			internalServerError(err, response)
			return
		}
		response.Header().Set("Content-Type", "application/json")
		body, _ := json.MarshalIndent(checkSums, "", "\t")
		response.Write(body)

	case "POST":
		if pos := strings.LastIndex(path, "/"); pos != -1 {
			err := os.MkdirAll(path[0:pos], 0777)
			if err != nil {
				internalServerError(err, response)
				return
			}
		}
		defer request.Body.Close()
		body, err := ioutil.ReadAll(request.Body)
		if err != nil {
			internalServerError(err, response)
			return
		}
		err = quick.FileSetBytes(path, body)
		if err != nil {
			internalServerError(err, response)
			return
		}
		runCmd(response)

	case "DELETE":
		if !quick.FileExists(path) {
			http.NotFound(response, request)
			return
		}
		err := os.RemoveAll(path)
		if err != nil {
			internalServerError(err, response)
			return
		}
		runCmd(response)

	default:
		http.Error(response, "Method not allowed", 405)
	}
}

func runServer() {
	http.HandleFunc("/", serverHandler)
	log.Fatal(http.ListenAndServe("listen", nil))
}

func runClient() {
	watcher, err := fsnotify.NewWatcher()
	if err != nil {
		log.Fatal(err)
	}
	defer watcher.Close()

	err = watcher.Watch("testDir")
	if err != nil {
		log.Fatal(err)
	}

	for {
		select {
		case ev := <-watcher.Event:
			log.Println("event:", ev)
		case err := <-watcher.Error:
			log.Println("error:", err)
		}
	}
}

func main() {
	flag.StringVar(&listen, "listen", ":8080", "bla")
	flag.StringVar(&to, "to", "", "bla")
	flag.StringVar(&cmd, "cmd", "go build", "command that will be executed after files have been written")
	flag.Parse()

	if to == "" {
		runServer()
	} else {
		runClient()
	}
}
<|endoftext|>


<commit_before>package main

import (
	"errors"
	"flag"
	"fmt"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	"github.com/jack-zh/gotodo/task"
)

var noAct = errors.New("didn't act")

var (
	file = flag.String("file", defaultFile(".todo", "TODO"), "file in which to store tasks")
	log  = flag.String("log", defaultFile(".todo-log", "TODOLOG"), "file in which to log removed tasks")
	now  = flag.Bool("now", false, "when adding, insert at head")
	done = flag.Bool("done", false, "don't actually add; just append to log file")
)

func defaultFile(name, env string) string {
	if f := os.Getenv(env); f != "" {
		return f
	}
	return filepath.Join(os.Getenv("HOME"), name)
}

const usage = `Usage:
	todo
		Show top task
	todo ls
		Show all tasks
	todo N
		Promote task N to top
	todo rm
		Remove top task
	todo rm N
		Remove task N
Flags:
`

func main() {
	flag.Usage = func() {
		fmt.Fprint(os.Stderr, usage)
		flag.PrintDefaults()
	}
	flag.Parse()

	list := task.NewList(*file)
	a, n := flag.Arg(0), len(flag.Args())

	err := noAct
	switch {
	case a == "ls" && n == 1:
		// list tasks
		var tasks []string
		tasks, err = list.Get()
		for i := len(tasks) - 1; i >= 0; i-- {
			fmt.Printf("%2d: %s\n", i, tasks[i])
		}
	case a == "rm" && n <= 2:
		i, err2 := strconv.Atoi(flag.Arg(1))
		if err2 != nil && n == 2 {
			break
		}
		if n == 1 {
			i = 0
		}
		var t string
		t, err = list.GetTask(i)
		if err != nil {
			break
		}
		if err2 := logRemovedTask(t); err2 != nil {
			fmt.Fprintln(os.Stderr, "logging:", err2)
		}
		err = list.RemoveTask(i)
		if err != nil || n == 2 {
			break
		}
		fallthrough
	case n == 0:
		var tasks []string
		tasks, err = list.Get()
		if len(tasks) > 0 {
			fmt.Println(tasks[0])
		}
	case n == 1:
		i, err2 := strconv.Atoi(flag.Arg(0))
		if err2 != nil {
			break
		}
		var t string
		t, err = list.GetTask(i)
		if err != nil {
			break
		}
		err = list.RemoveTask(i)
		if err != nil {
			break
		}
		err = list.AddTask(t, true)
		if err == nil {
			fmt.Println(t)
		}
	}
	if err == noAct {
		t := strings.Join(flag.Args(), " ")
		if *done {
			err = logRemovedTask(t)
		} else {
			err = list.AddTask(t, *now)
		}
	}
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
}

func logRemovedTask(t string) error {
	f, err := os.OpenFile(*log, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
	if err != nil {
		return err
	}
	defer f.Close()
	now := time.Now().Format("1986-02-17")
	_, err = fmt.Fprintf(f, "%s %s\n", now, t)
	return err
}
<commit_msg>gotodo ls->list<commit_after>package main

import (
	"errors"
	"flag"
	"fmt"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	"github.com/jack-zh/gotodo/task"
)

var noAct = errors.New("didn't act")

var (
	file = flag.String("file", defaultFile(".zgotodo", "TODO"), "file in which to store tasks")
	log  = flag.String("log", defaultFile(".zgotodo-log", "TODOLOG"), "file in which to log removed tasks")
	now  = flag.Bool("now", false, "when adding, insert at head")
	done = flag.Bool("done", false, "don't actually add; just append to log file")
)

func defaultFile(name, env string) string {
	if f := os.Getenv(env); f != "" {
		return f
	}
	return filepath.Join(os.Getenv("HOME"), name)
}

const usage = `Usage:
	gotodo
		Show top task
	gotodo list
		Show all tasks
	gotodo N
		Promote task N to top
	gotodo rm
		Remove top task
	gotodo rm N
		Remove task N
Flags:
`

func main() {
	flag.Usage = func() {
		fmt.Fprint(os.Stderr, usage)
		flag.PrintDefaults()
	}
	flag.Parse()

	list := task.NewList(*file)
	a, n := flag.Arg(0), len(flag.Args())

	err := noAct
	switch {
	case a == "list" && n == 1:
		var tasks []string
		tasks, err = list.Get()
		for i := len(tasks) - 1; i >= 0; i-- {
			fmt.Printf("%2d: %s\n", i, tasks[i])
		}
	case a == "rm" && n <= 2:
		i, err2 := strconv.Atoi(flag.Arg(1))
		if err2 != nil && n == 2 {
			break
		}
		if n == 1 {
			i = 0
		}
		var t string
		t, err = list.GetTask(i)
		if err != nil {
			break
		}
		if err2 := logRemovedTask(t); err2 != nil {
			fmt.Fprintln(os.Stderr, "logging:", err2)
		}
		err = list.RemoveTask(i)
		if err != nil || n == 2 {
			break
		}
		fallthrough
	case n == 0:
		var tasks []string
		tasks, err = list.Get()
		if len(tasks) > 0 {
			fmt.Println(tasks[0])
		}
	case n == 1:
		i, err2 := strconv.Atoi(flag.Arg(0))
		if err2 != nil {
			break
		}
		var t string
		t, err = list.GetTask(i)
		if err != nil {
			break
		}
		err = list.RemoveTask(i)
		if err != nil {
			break
		}
		err = list.AddTask(t, true)
		if err == nil {
			fmt.Println(t)
		}
	}
	if err == noAct {
		t := strings.Join(flag.Args(), " ")
		if *done {
			err = logRemovedTask(t)
		} else {
			err = list.AddTask(t, *now)
		}
	}
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
}

func logRemovedTask(t string) error {
	f, err := os.OpenFile(*log, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
	if err != nil {
		return err
	}
	defer f.Close()
	now := time.Now().Format("1986-02-17")
	_, err = fmt.Fprintf(f, "%s %s\n", now, t)
	return err
}
<|endoftext|>


<commit_before>// Package graphigo provides a simple go client for the graphite monitoring tool.
// See http://graphite.readthedocs.org/en/latest/overview.html
package graphigo

import (
	"bytes"
	"fmt"
	"io"
	"net"
	"time"
)

// Client is a simple TCP client for the graphite monitoring tool.
type Client struct {
	// Address is used when connecting to the graphite server. Use address:port notation.
	Address string

	// Timeout is the maximum duration that the client will wait for a response from the server.
	// If timeout is zero then the DefaultTimeout is used.
	// Setting Timeout to -1 disables the timeout.
	Timeout time.Duration

	// Prefix is prepended to all metric names (separated from original name by a dot).
	// If Prefix is empty this does nothing.
	Prefix string

	// Connection is used to communicate with the graphite client.
	// You would normally not interfere with this but this can be handy for testing.
	Connection io.WriteCloser
}

const (
	// DefaultTimeout is the timeout that is applied when connecting to a graphite server.
	// It is used if no explicit timeout has been configured on a client.
	DefaultTimeout = 5

	// TimeoutDisabled is used to disable the client timeout entirely.
	TimeoutDisabled = -1
)

// NewClient creates a new instance of a graphite client.
// Use the address:port notation to specify the port.
func NewClient(address string) *Client {
	return &Client{Address: address}
}

// Connect attempts to establish the connection to the graphite server.
// This will return an error if a TCP connection can not or has already been established.
func (g *Client) Connect() (err error) {
	if g == nil {
		return nil
	}

	if g.Connection != nil {
		return fmt.Errorf("Graphigo is already connected. Call Disconnect first if you want to reconnect")
	}

	if g.Timeout == 0 {
		g.Timeout = DefaultTimeout * time.Second
	}

	timeout := g.Timeout
	if g.Timeout == -1 {
		timeout = 0
	}

	g.Connection, err = net.DialTimeout("tcp", g.Address, timeout)
	return err
}

// Disconnect closes the underlying connection to graphite.
func (g *Client) Disconnect() error {
	if g == nil {
		return nil
	}

	err := g.Connection.Close()
	g.Connection = nil
	return err
}

// SendValue creates a new graphigo.Metric with the metric timestamp set to now and sends it to graphite.
//
// Use Send(metric) if you want to split metric recording and sending.
// This will return an error if the client has not yet been connected or the metric name is empty.
func (g *Client) SendValue(name string, value interface{}) error {
	if g == nil {
		return nil
	}

	return g.Send(Metric{
		Name:  name,
		Value: value,
	})
}

// Send sends a graphigo.Metric to graphite.
// This can be used to send a metric which has been recorded earlier.
//
// Use SendValue if you want to create and send a metric in one step.
// Use SendAll if you want to send multiple metrics at once.
// This will return an error if the client has not yet been connected or the metric name is empty.
func (g *Client) Send(metric Metric) error {
	if g == nil {
		return nil
	}

	return g.SendAll([]Metric{metric})
}

// SendAll sends multiple graphigo.Metric to graphite.
// This can be used to send multiple metrics that have been recorded earlier.
//
// Use Send if you want to send a single metric.
// This will return an error if the client has not yet been connected or if any of the metrics has an empty name.
func (g *Client) SendAll(metrics []Metric) (err error) {
	if g == nil {
		return nil
	}

	if g.Connection == nil {
		return fmt.Errorf("Graphigo is not connected yet. Did you forget to call Connect() ?")
	}

	buffer := &bytes.Buffer{}
	for _, metric := range metrics {
		if metric.Name == "" {
			return fmt.Errorf("Could not send graphite metric: no metric name given")
		}

		if metric.Timestamp.IsZero() {
			metric.Timestamp = time.Now()
		}

		if g.Prefix != "" {
			metric.Name = fmt.Sprintf("%s.%s", g.Prefix, metric.Name)
		}

		_, err = fmt.Fprintf(buffer, "%s %v %d\n", metric.Name, metric.Value, metric.UnixTimestamp())
		if err != nil {
			return fmt.Errorf("Could not write metric to send buffer: %s", err.Error())
		}
	}

	_, err = g.Connection.Write(buffer.Bytes())
	return err
}
<commit_msg>More documentation<commit_after>// Package graphigo provides a simple go client for the graphite monitoring tool.
// See http://graphite.readthedocs.org/en/latest/overview.html
package graphigo

import (
	"bytes"
	"fmt"
	"io"
	"net"
	"time"
)

// Client is a simple TCP client for the graphite monitoring tool.
type Client struct {
	// Address is used when connecting to the graphite server. Use address:port notation.
	Address string

	// Timeout is the maximum duration that the client will wait for a response from the server.
	// If timeout is zero then the DefaultTimeout is used.
	// Setting Timeout to -1 disables the timeout.
	Timeout time.Duration

	// Prefix is prepended to all metric names (separated from original name by a dot).
	// If Prefix is empty this does nothing.
	Prefix string

	// Connection is used to communicate with the graphite client.
	// You would normally not interfere with this but this can be handy for testing.
	Connection io.WriteCloser
}

const (
	// DefaultTimeout is the timeout that is applied when connecting to a graphite server.
	// It is used if no explicit timeout has been configured on a client.
	DefaultTimeout = 5

	// TimeoutDisabled is used to disable the client timeout entirely.
	TimeoutDisabled = -1
)

// NewClient creates a new instance of a graphite client.
// Use the address:port notation to specify the port.
// Note that the client will not connect to the given address automatically.
// You still need to call Connect() before you can start sending values.
func NewClient(address string) *Client {
	return &Client{Address: address}
}

// Connect attempts to establish the connection to the graphite server.
// This will return an error if a TCP connection can not or has already been established.
func (g *Client) Connect() (err error) {
	if g == nil {
		return nil
	}

	if g.Connection != nil {
		return fmt.Errorf("Graphigo is already connected. Call Disconnect first if you want to reconnect")
	}

	if g.Timeout == 0 {
		g.Timeout = DefaultTimeout * time.Second
	}

	timeout := g.Timeout
	if g.Timeout == -1 {
		timeout = 0
	}

	g.Connection, err = net.DialTimeout("tcp", g.Address, timeout)
	return err
}

// Disconnect closes the underlying connection to graphite.
func (g *Client) Disconnect() error {
	if g == nil {
		return nil
	}

	err := g.Connection.Close()
	g.Connection = nil
	return err
}

// SendValue creates a new graphigo.Metric with the metric timestamp set to now and sends it to graphite.
//
// Use Send(metric) if you want to split metric recording and sending.
// This will return an error if the client has not yet been connected or the metric name is empty.
func (g *Client) SendValue(name string, value interface{}) error {
	if g == nil {
		return nil
	}

	return g.Send(Metric{
		Name:  name,
		Value: value,
	})
}

// Send sends a graphigo.Metric to graphite.
// This can be used to send a metric which has been recorded earlier.
//
// Use SendValue if you want to create and send a metric in one step.
// Use SendAll if you want to send multiple metrics at once.
// This will return an error if the client has not yet been connected or the metric name is empty.
func (g *Client) Send(metric Metric) error {
	if g == nil {
		return nil
	}

	return g.SendAll([]Metric{metric})
}

// SendAll sends multiple graphigo.Metric to graphite.
// This can be used to send multiple metrics that have been recorded earlier.
//
// Use Send if you want to send a single metric.
// This will return an error if the client has not yet been connected or if any of the metrics has an empty name.
func (g *Client) SendAll(metrics []Metric) (err error) {
	if g == nil {
		return nil
	}

	if g.Connection == nil {
		return fmt.Errorf("Graphigo is not connected yet. Did you forget to call Connect() ?")
	}

	buffer := &bytes.Buffer{}
	for _, metric := range metrics {
		if metric.Name == "" {
			return fmt.Errorf("Could not send graphite metric: no metric name given")
		}

		if metric.Timestamp.IsZero() {
			metric.Timestamp = time.Now()
		}

		if g.Prefix != "" {
			metric.Name = fmt.Sprintf("%s.%s", g.Prefix, metric.Name)
		}

		_, err = fmt.Fprintf(buffer, "%s %v %d\n", metric.Name, metric.Value, metric.UnixTimestamp())
		if err != nil {
			return fmt.Errorf("Could not write metric to send buffer: %s", err.Error())
		}
	}

	_, err = g.Connection.Write(buffer.Bytes())
	return err
}
<|endoftext|>


<commit_before>// Copyright 2016 The Upspin 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 upspinserver is a combined DirServer and StoreServer for use on
// stand-alone machines. It provides only the production implementations of the
// dir and store servers (dir/server and store/server).
// It provides the common cod used by all upspinserver commands.
package upspinserver // import "upspin.io/serverutil/upspinserver"

import (
	"encoding/base64"
	"encoding/json"
	"flag"
	"fmt"
	"io/ioutil"
	"net"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"sync"

	"upspin.io/client"
	"upspin.io/cloud/https"
	"upspin.io/config"
	dirServer "upspin.io/dir/server"
	"upspin.io/errors"
	"upspin.io/factotum"
	"upspin.io/flags"
	"upspin.io/log"
	"upspin.io/rpc/dirserver"
	"upspin.io/rpc/storeserver"
	"upspin.io/serverutil/perm"
	storeServer "upspin.io/store/server"
	"upspin.io/subcmd"
	"upspin.io/upspin"

	// Packers.
	_ "upspin.io/pack/ee"
	_ "upspin.io/pack/eeintegrity"
	_ "upspin.io/pack/plain"

	// Required transports.
	_ "upspin.io/transports"
)

var (
	cfgPath   = flag.String("serverconfig", filepath.Join(config.Home(), "upspin", "server"), "server configuration `directory`")
	letsPath  = flag.String("letscache", filepath.Join(config.Home(), "upspin", "letsencrypt"), "Let's Encrypt cache `directory`")
	enableWeb = flag.Bool("web", false, "enable Upspin web interface")
	ready     = make(chan struct{})
)

func Main() {
	flags.Parse("http", "https", "insecure", "letscache", "tls", "log")

	server, cfg, perm, err := initServer(startup)
	if err == noConfig {
		log.Print("Configuration file not found. Running in setup mode.")
		http.Handle("/", &setupHandler{})
	} else if err != nil {
		log.Fatal(err)
	} else {
		http.Handle("/", newWeb(cfg, perm))
	}

	// Set up HTTPS server.
	var opt https.Options
	if flags.TLSCertFile != "" && flags.TLSKeyFile != "" {
		opt.CertFile = flags.TLSCertFile
		opt.KeyFile = flags.TLSKeyFile
	} else {
		opt.LetsEncryptCache = *letsPath
		if server != nil {
			host, _, err := net.SplitHostPort(string(server.Addr))
			if err != nil {
				log.Printf("Error parsing addr from config %q: %v", server.Addr, err)
				log.Printf("Warning: Let's Encrypt certificates will be fetched for any host.")
			} else {
				opt.LetsEncryptHosts = []string{host}
			}
		}
	}
	https.ListenAndServe(ready, "upspinserver", flags.HTTPSAddr, &opt)
}

var noConfig = errors.Str("no configuration")

type initMode int

const (
	startup initMode = iota
	setupServer
)

func initServer(mode initMode) (*subcmd.ServerConfig, upspin.Config, *perm.Perm, error) {
	serverConfig, err := readServerConfig()
	if os.IsNotExist(err) {
		return nil, nil, nil, noConfig
	} else if err != nil {
		return nil, nil, nil, err
	}

	cfg := config.New()
	cfg = config.SetUserName(cfg, serverConfig.User)

	f, err := factotum.NewFromDir(*cfgPath)
	if err != nil {
		return nil, nil, nil, err
	}
	cfg = config.SetFactotum(cfg, f)

	ep := upspin.Endpoint{
		Transport: upspin.Remote,
		NetAddr:   serverConfig.Addr,
	}
	cfg = config.SetDirEndpoint(cfg, ep)
	cfg = config.SetStoreEndpoint(cfg, ep)

	storeCfg := config.SetPacking(cfg, upspin.EEIntegrityPack)
	dirCfg := config.SetPacking(cfg, upspin.EEPack)

	// Set up StoreServer.
	var storeServerConfig []string
	switch {
	case len(serverConfig.StoreConfig) > 0:
		// Use the provided configuration, if available.
		storeServerConfig = serverConfig.StoreConfig
	default:
		// No bucket configured, use simple on-disk store.
		storagePath := filepath.Join(*cfgPath, "storage")
		storeServerConfig = []string{"backend=Disk", "basePath=" + storagePath}
	}
	store, err := storeServer.New(storeServerConfig...)
	if err != nil {
		return nil, nil, nil, err
	}

	// Set up DirServer.
	logDir := filepath.Join(*cfgPath, "dirserver-logs")
	if err := os.MkdirAll(logDir, 0700); err != nil {
		return nil, nil, nil, err
	}
	dir, err := dirServer.New(dirCfg, "userCacheSize=1000", "logDir="+logDir)
	if err != nil {
		return nil, nil, nil, err
	}

	// Wrap store and dir with permission checking.
	perm := perm.NewWithDir(dirCfg, ready, serverConfig.User, dir)
	store = perm.WrapStore(store)
	dir = perm.WrapDir(dir)

	// Set up RPC server.
	httpStore := storeserver.New(storeCfg, store, serverConfig.Addr)
	httpDir := dirserver.New(dirCfg, dir, serverConfig.Addr)
	http.Handle("/api/Store/", httpStore)
	http.Handle("/api/Dir/", httpDir)

	log.Println("Store and Directory servers initialized.")
	log.Printf("Store server configuration: %s", fmtStoreConfig(storeServerConfig))

	if mode == setupServer {
		// Create Writers file if this was triggered by 'upspin setupserver'.
		go func() {
			if err := setupWriters(storeCfg); err != nil {
				log.Printf("Error creating Writers file: %v", err)
			}
		}()
	}
	return serverConfig, cfg, perm, nil
}

// fmtStoreConfig formats a ServerConfig.StoreConfig value as a string,
// ommitting any privateKeyData fields as they include sensitive information.
func fmtStoreConfig(cfg []string) string {
	var out []string
	for _, s := range cfg {
		if !strings.HasPrefix(s, "privateKeyData=") {
			out = append(out, s)
		}
	}
	return strings.Join(out, " ")
}

type setupHandler struct {
	mu   sync.Mutex
	done bool
	web  http.Handler
}

func (h *setupHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	h.mu.Lock()
	if h.done {
		web := h.web
		h.mu.Unlock()
		if web != nil {
			web.ServeHTTP(w, r)
		} else {
			http.NotFound(w, r)
		}
		return
	}
	defer h.mu.Unlock()

	switch r.URL.Path {
	case "/":
		fmt.Fprint(w, "Unconfigured Upspin Server")
		return
	default:
		http.NotFound(w, r)
		return
	case "/setupserver":
		if r.Method != "POST" {
			http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
			return
		}
		// The rest of this function is the setupserver handler.
	}

	files := map[string][]byte{}
	if err := json.NewDecoder(r.Body).Decode(&files); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	if err := os.MkdirAll(*cfgPath, 0700); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	for _, name := range subcmd.SetupServerFiles {
		body, ok := files[name]
		if !ok {
			http.Error(w, fmt.Sprintf("missing config file %q", name), http.StatusBadRequest)
			return
		}
		err := ioutil.WriteFile(filepath.Join(*cfgPath, name), body, 0600)
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			os.RemoveAll(*cfgPath)
			return
		}
	}
	_, cfg, perm, err := initServer(setupServer)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	fmt.Fprintf(w, "OK")

	h.done = true
	h.web = newWeb(cfg, perm)
}

func setupWriters(cfg upspin.Config) error {
	writers, err := ioutil.ReadFile(filepath.Join(*cfgPath, "Writers"))
	if err != nil {
		return err
	}

	c := client.New(cfg)

	dir := upspin.PathName(cfg.UserName())
	if err := existsOK(c.MakeDirectory(dir)); err != nil {
		return err
	}

	dir += "/Group"
	if err := existsOK(c.MakeDirectory(dir)); err != nil {
		return err
	}

	file := dir + "/Writers"
	_, err = c.Put(file, writers)
	return err
}

// existsOK returns err if it is not an errors.Exist error,
// in which case it returns nil.
func existsOK(_ *upspin.DirEntry, err error) error {
	if errors.Match(errors.E(errors.Exist), err) {
		return nil
	}
	return err
}

func readServerConfig() (*subcmd.ServerConfig, error) {
	cfgFile := filepath.Join(*cfgPath, subcmd.ServerConfigFile)
	b, err := ioutil.ReadFile(cfgFile)
	if err != nil {
		return nil, err
	}
	cfg := &subcmd.ServerConfig{}
	if err := json.Unmarshal(b, cfg); err != nil {
		return nil, err
	}

	return cfg, updateServerConfig(cfg)
}

// If Bucket is set, look for the old style serviceaccount.json file, stuff it
// into the StoreConfig field, clear the Bucket field, and rewrite the config.
// TODO(adg): remove this at the same time as removing the Bucket field.
func updateServerConfig(cfg *subcmd.ServerConfig) error {
	if cfg.Bucket != "" {
		cfgFile := filepath.Join(*cfgPath, subcmd.ServerConfigFile)
		serviceFile := filepath.Join(*cfgPath, "serviceaccount.json")
		b, err := ioutil.ReadFile(serviceFile)
		if err != nil {
			return fmt.Errorf("Bucket set in %v, but: %v", cfgFile, err)
		}
		privateKeyData := base64.StdEncoding.EncodeToString(b)
		cfg.StoreConfig = []string{
			"backend=GCS",
			"defaultACL=publicRead",
			"gcpBucketName=" + cfg.Bucket,
			"privateKeyData=" + privateKeyData,
		}
		cfg.Bucket = ""
		b, err = json.Marshal(cfg)
		if err != nil {
			return fmt.Errorf("encoding config %v: %v", cfgFile, err)
		}
		err = ioutil.WriteFile(cfgFile, b, 0700)
		if err != nil {
			return fmt.Errorf("rewriting config: %v", err)
		}
		os.Remove(serviceFile)
	}
	return nil
}
<commit_msg>serverutil/upspinserver: do not re-register letscache flag<commit_after>// Copyright 2016 The Upspin 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 upspinserver is a combined DirServer and StoreServer for use on
// stand-alone machines. It provides only the production implementations of the
// dir and store servers (dir/server and store/server).
// It provides the common cod used by all upspinserver commands.
package upspinserver // import "upspin.io/serverutil/upspinserver"

import (
	"encoding/base64"
	"encoding/json"
	"flag"
	"fmt"
	"io/ioutil"
	"net"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"sync"

	"upspin.io/client"
	"upspin.io/cloud/https"
	"upspin.io/config"
	dirServer "upspin.io/dir/server"
	"upspin.io/errors"
	"upspin.io/factotum"
	"upspin.io/flags"
	"upspin.io/log"
	"upspin.io/rpc/dirserver"
	"upspin.io/rpc/storeserver"
	"upspin.io/serverutil/perm"
	storeServer "upspin.io/store/server"
	"upspin.io/subcmd"
	"upspin.io/upspin"

	// Packers.
	_ "upspin.io/pack/ee"
	_ "upspin.io/pack/eeintegrity"
	_ "upspin.io/pack/plain"

	// Required transports.
	_ "upspin.io/transports"
)

var (
	cfgPath   = flag.String("serverconfig", filepath.Join(config.Home(), "upspin", "server"), "server configuration `directory`")
	letsPath  = flag.String("letscache", filepath.Join(config.Home(), "upspin", "letsencrypt"), "Let's Encrypt cache `directory`")
	enableWeb = flag.Bool("web", false, "enable Upspin web interface")
	ready     = make(chan struct{})
)

func Main() {
	flags.Parse("http", "https", "insecure", "tls", "log")

	server, cfg, perm, err := initServer(startup)
	if err == noConfig {
		log.Print("Configuration file not found. Running in setup mode.")
		http.Handle("/", &setupHandler{})
	} else if err != nil {
		log.Fatal(err)
	} else {
		http.Handle("/", newWeb(cfg, perm))
	}

	// Set up HTTPS server.
	var opt https.Options
	if flags.TLSCertFile != "" && flags.TLSKeyFile != "" {
		opt.CertFile = flags.TLSCertFile
		opt.KeyFile = flags.TLSKeyFile
	} else {
		opt.LetsEncryptCache = *letsPath
		if server != nil {
			host, _, err := net.SplitHostPort(string(server.Addr))
			if err != nil {
				log.Printf("Error parsing addr from config %q: %v", server.Addr, err)
				log.Printf("Warning: Let's Encrypt certificates will be fetched for any host.")
			} else {
				opt.LetsEncryptHosts = []string{host}
			}
		}
	}
	https.ListenAndServe(ready, "upspinserver", flags.HTTPSAddr, &opt)
}

var noConfig = errors.Str("no configuration")

type initMode int

const (
	startup initMode = iota
	setupServer
)

func initServer(mode initMode) (*subcmd.ServerConfig, upspin.Config, *perm.Perm, error) {
	serverConfig, err := readServerConfig()
	if os.IsNotExist(err) {
		return nil, nil, nil, noConfig
	} else if err != nil {
		return nil, nil, nil, err
	}

	cfg := config.New()
	cfg = config.SetUserName(cfg, serverConfig.User)

	f, err := factotum.NewFromDir(*cfgPath)
	if err != nil {
		return nil, nil, nil, err
	}
	cfg = config.SetFactotum(cfg, f)

	ep := upspin.Endpoint{
		Transport: upspin.Remote,
		NetAddr:   serverConfig.Addr,
	}
	cfg = config.SetDirEndpoint(cfg, ep)
	cfg = config.SetStoreEndpoint(cfg, ep)

	storeCfg := config.SetPacking(cfg, upspin.EEIntegrityPack)
	dirCfg := config.SetPacking(cfg, upspin.EEPack)

	// Set up StoreServer.
	var storeServerConfig []string
	switch {
	case len(serverConfig.StoreConfig) > 0:
		// Use the provided configuration, if available.
		storeServerConfig = serverConfig.StoreConfig
	default:
		// No bucket configured, use simple on-disk store.
		storagePath := filepath.Join(*cfgPath, "storage")
		storeServerConfig = []string{"backend=Disk", "basePath=" + storagePath}
	}
	store, err := storeServer.New(storeServerConfig...)
	if err != nil {
		return nil, nil, nil, err
	}

	// Set up DirServer.
	logDir := filepath.Join(*cfgPath, "dirserver-logs")
	if err := os.MkdirAll(logDir, 0700); err != nil {
		return nil, nil, nil, err
	}
	dir, err := dirServer.New(dirCfg, "userCacheSize=1000", "logDir="+logDir)
	if err != nil {
		return nil, nil, nil, err
	}

	// Wrap store and dir with permission checking.
	perm := perm.NewWithDir(dirCfg, ready, serverConfig.User, dir)
	store = perm.WrapStore(store)
	dir = perm.WrapDir(dir)

	// Set up RPC server.
	httpStore := storeserver.New(storeCfg, store, serverConfig.Addr)
	httpDir := dirserver.New(dirCfg, dir, serverConfig.Addr)
	http.Handle("/api/Store/", httpStore)
	http.Handle("/api/Dir/", httpDir)

	log.Println("Store and Directory servers initialized.")
	log.Printf("Store server configuration: %s", fmtStoreConfig(storeServerConfig))

	if mode == setupServer {
		// Create Writers file if this was triggered by 'upspin setupserver'.
		go func() {
			if err := setupWriters(storeCfg); err != nil {
				log.Printf("Error creating Writers file: %v", err)
			}
		}()
	}
	return serverConfig, cfg, perm, nil
}

// fmtStoreConfig formats a ServerConfig.StoreConfig value as a string,
// ommitting any privateKeyData fields as they include sensitive information.
func fmtStoreConfig(cfg []string) string {
	var out []string
	for _, s := range cfg {
		if !strings.HasPrefix(s, "privateKeyData=") {
			out = append(out, s)
		}
	}
	return strings.Join(out, " ")
}

type setupHandler struct {
	mu   sync.Mutex
	done bool
	web  http.Handler
}

func (h *setupHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	h.mu.Lock()
	if h.done {
		web := h.web
		h.mu.Unlock()
		if web != nil {
			web.ServeHTTP(w, r)
		} else {
			http.NotFound(w, r)
		}
		return
	}
	defer h.mu.Unlock()

	switch r.URL.Path {
	case "/":
		fmt.Fprint(w, "Unconfigured Upspin Server")
		return
	default:
		http.NotFound(w, r)
		return
	case "/setupserver":
		if r.Method != "POST" {
			http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
			return
		}
		// The rest of this function is the setupserver handler.
	}

	files := map[string][]byte{}
	if err := json.NewDecoder(r.Body).Decode(&files); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	if err := os.MkdirAll(*cfgPath, 0700); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	for _, name := range subcmd.SetupServerFiles {
		body, ok := files[name]
		if !ok {
			http.Error(w, fmt.Sprintf("missing config file %q", name), http.StatusBadRequest)
			return
		}
		err := ioutil.WriteFile(filepath.Join(*cfgPath, name), body, 0600)
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			os.RemoveAll(*cfgPath)
			return
		}
	}
	_, cfg, perm, err := initServer(setupServer)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	fmt.Fprintf(w, "OK")

	h.done = true
	h.web = newWeb(cfg, perm)
}

func setupWriters(cfg upspin.Config) error {
	writers, err := ioutil.ReadFile(filepath.Join(*cfgPath, "Writers"))
	if err != nil {
		return err
	}

	c := client.New(cfg)

	dir := upspin.PathName(cfg.UserName())
	if err := existsOK(c.MakeDirectory(dir)); err != nil {
		return err
	}

	dir += "/Group"
	if err := existsOK(c.MakeDirectory(dir)); err != nil {
		return err
	}

	file := dir + "/Writers"
	_, err = c.Put(file, writers)
	return err
}

// existsOK returns err if it is not an errors.Exist error,
// in which case it returns nil.
func existsOK(_ *upspin.DirEntry, err error) error {
	if errors.Match(errors.E(errors.Exist), err) {
		return nil
	}
	return err
}

func readServerConfig() (*subcmd.ServerConfig, error) {
	cfgFile := filepath.Join(*cfgPath, subcmd.ServerConfigFile)
	b, err := ioutil.ReadFile(cfgFile)
	if err != nil {
		return nil, err
	}
	cfg := &subcmd.ServerConfig{}
	if err := json.Unmarshal(b, cfg); err != nil {
		return nil, err
	}

	return cfg, updateServerConfig(cfg)
}

// If Bucket is set, look for the old style serviceaccount.json file, stuff it
// into the StoreConfig field, clear the Bucket field, and rewrite the config.
// TODO(adg): remove this at the same time as removing the Bucket field.
func updateServerConfig(cfg *subcmd.ServerConfig) error {
	if cfg.Bucket != "" {
		cfgFile := filepath.Join(*cfgPath, subcmd.ServerConfigFile)
		serviceFile := filepath.Join(*cfgPath, "serviceaccount.json")
		b, err := ioutil.ReadFile(serviceFile)
		if err != nil {
			return fmt.Errorf("Bucket set in %v, but: %v", cfgFile, err)
		}
		privateKeyData := base64.StdEncoding.EncodeToString(b)
		cfg.StoreConfig = []string{
			"backend=GCS",
			"defaultACL=publicRead",
			"gcpBucketName=" + cfg.Bucket,
			"privateKeyData=" + privateKeyData,
		}
		cfg.Bucket = ""
		b, err = json.Marshal(cfg)
		if err != nil {
			return fmt.Errorf("encoding config %v: %v", cfgFile, err)
		}
		err = ioutil.WriteFile(cfgFile, b, 0700)
		if err != nil {
			return fmt.Errorf("rewriting config: %v", err)
		}
		os.Remove(serviceFile)
	}
	return nil
}
<|endoftext|>


<commit_before>//Package email is designed to provide an "email interface for humans."
//Designed to be robust and flexible, the email package aims to make sending email easy without getting in the way.
package email

import (
	"bytes"
	"encoding/base64"
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"mime"
	"mime/multipart"
	"net/mail"
	"net/smtp"
	"net/textproto"
	"os"
	"path/filepath"
	"strings"
)

const (
	MAX_LINE_LENGTH = 76 //The maximum line length per RFC 2045
)

//Email is the type used for email messages
type Email struct {
	From        string
	To          []string
	Bcc         []string
	Cc          []string
	Subject     string
	Text        string //Plaintext message (optional)
	Html        string //Html message (optional)
	Headers     textproto.MIMEHeader
	Attachments map[string]*Attachment
	ReadReceipt []string
}

//NewEmail creates an Email, and returns the pointer to it.
func NewEmail() *Email {
	return &Email{Attachments: make(map[string]*Attachment), Headers: textproto.MIMEHeader{}}
}

//Attach is used to attach a file to the email.
//It attempts to open the file referenced by filename and, if successful, creates an Attachment.
//This Attachment is then appended to the slice of Email.Attachments.
//The function will then return the Attachment for reference, as well as nil for the error, if successful.
func (e *Email) Attach(filename string) (a *Attachment, err error) {
	//Check if the file exists, return any error
	if _, err := os.Stat(filename); os.IsNotExist(err) {
		return nil, err
	}
	//Read the file, and set the appropriate headers
	buffer, _ := ioutil.ReadFile(filename)
	e.Attachments[filename] = &Attachment{
		Filename: filename,
		Header:   textproto.MIMEHeader{},
		Content:  buffer}
	at := e.Attachments[filename]
	//Get the Content-Type to be used in the MIMEHeader
	ct := mime.TypeByExtension(filepath.Ext(filename))
	if ct != "" {
		at.Header.Set("Content-Type", ct)
	} else {
		//If the Content-Type is blank, set the Content-Type to "application/octet-stream"
		at.Header.Set("Content-Type", "application/octet-stream")
	}
	at.Header.Set("Content-Disposition", fmt.Sprintf("attachment;\r\n filename=\"%s\"", filename))
	at.Header.Set("Content-Transfer-Encoding", "base64")
	return e.Attachments[filename], nil
}

//Bytes converts the Email object to a []byte representation, including all needed MIMEHeaders, boundaries, etc.
func (e *Email) Bytes() ([]byte, error) {
	buff := &bytes.Buffer{}
	w := multipart.NewWriter(buff)
	//Set the appropriate headers (overwriting any conflicts)
	//Leave out Bcc (only included in envelope headers)
	//TODO: Support wrapping on 76 characters (ref: MIME RFC)
	e.Headers.Set("To", strings.Join(e.To, ","))
	if e.Cc != nil {
		e.Headers.Set("Cc", strings.Join(e.Cc, ","))
	}
	e.Headers.Set("From", e.From)
	e.Headers.Set("Subject", e.Subject)
	if len(e.ReadReceipt) != 0 {
		e.Headers.Set("Disposition-Notification-To", strings.Join(e.ReadReceipt, ","))
	}
	e.Headers.Set("MIME-Version", "1.0")
	e.Headers.Set("Content-Type", fmt.Sprintf("multipart/mixed;\r\n boundary=%s\r\n", w.Boundary()))

	//Write the envelope headers (including any custom headers)
	if err := headerToBytes(buff, e.Headers); err != nil {
	}
	//Start the multipart/mixed part
	fmt.Fprintf(buff, "--%s\r\n", w.Boundary())
	header := textproto.MIMEHeader{}
	//Check to see if there is a Text or HTML field
	if e.Text != "" || e.Html != "" {
		subWriter := multipart.NewWriter(buff)
		//Create the multipart alternative part
		header.Set("Content-Type", fmt.Sprintf("multipart/alternative;\r\n boundary=%s\r\n", subWriter.Boundary()))
		//Write the header
		if err := headerToBytes(buff, header); err != nil {

		}
		//Create the body sections
		if e.Text != "" {
			header.Set("Content-Type", fmt.Sprintf("text/plain; charset=UTF-8"))
			/*part, err := subWriter.CreatePart(header)
			if err != nil {

			}*/
			// Write the text, splitting it into chunks of MAX_LINE_LENGTH
			//splitStr := lineSplit(e.Text)
		}
		if e.Html != "" {
			header.Set("Content-Type", fmt.Sprintf("text/html; charset=UTF-8"))
			header.Set("Content-Transfer-Encoding", "quoted-printable")
			subWriter.CreatePart(header)
			// Write the text
			if err := quotePrintEncode(buff, e.Html); err != nil {

			}
		}
		subWriter.Close()
	}
	//Create attachment part, if necessary
	if e.Attachments != nil {
		for _, a := range e.Attachments {
			ap, err := w.CreatePart(a.Header)
			if err != nil {
				return nil, err
			}
			//Write the base64Wrapped content to the part
			base64Wrap(ap, a.Content)
		}
	}
	w.Close()
	return buff.Bytes(), nil
}

//Send an email using the given host and SMTP auth (optional), returns any error thrown by smtp.SendMail
//This function merges the To, Cc, and Bcc fields and calls the smtp.SendMail function using the Email.Bytes() output as the message
func (e *Email) Send(addr string, a smtp.Auth) error {
	//Check to make sure there is at least one recipient and one "From" address
	if e.From == "" || (len(e.To) == 0 && len(e.Cc) == 0 && len(e.Bcc) == 0) {
		return errors.New("Must specify at least one From address and one To address")
	}
	// Merge the To, Cc, and Bcc fields
	to := append(append(e.To, e.Cc...), e.Bcc...)
	from, err := mail.ParseAddress(e.From)
	if err != nil {
		return err
	}
	raw, err := e.Bytes()
	if err != nil {
		return err
	}
	return smtp.SendMail(addr, a, from.Address, to, raw)
}

//Attachment is a struct representing an email attachment.
//Based on the mime/multipart.FileHeader struct, Attachment contains the name, MIMEHeader, and content of the attachment in question
type Attachment struct {
	Filename string
	Header   textproto.MIMEHeader
	Content  []byte
}

//quotePrintEncode writes the quoted-printable text to the IO Writer
func quotePrintEncode(w io.Writer, s string) error {
	mc := 0
	for _, c := range s {
		//Change these to a switch
		// If we've reached the line length limit
		// or we've found a special character whose encoding will surpass the limit
		//append a soft break
		if mc == 75 || (!((c >= '!' && c <= '<') || (c >= '>' && c <= '~') || (c == ' ' || c == '\n' || c == '\t')) && mc >= 72) {
			if _, err := fmt.Fprintf(w, "%s%s", "=\r\n", string(c)); err != nil {
				return err
			}
			mc = 0
			continue
		}
		//append the appropriate character
		if (c >= '!' && c <= '<') || (c >= '>' && c <= '~') || (c == ' ' || c == '\n' || c == '\t') {
			//Printable character
			if _, err := fmt.Fprintf(w, "%s", string(c)); err != nil {
				return err
			}
			// Reset the counter if we wrote a newline
			if c == '\n' {
				mc = 0
			}
			mc++
			continue
		} else {
			//non-printable.. encode it (TODO)
			if _, err := fmt.Fprintf(w, "%s%X", "=", c); err != nil {
				return err
			}
			mc++
		}
	}
	return nil
}

//base64Wrap encodeds the attachment content, and wraps it according to RFC 2045 standards (every 76 chars)
//The output is then written to the specified io.Writer
func base64Wrap(w io.Writer, b []byte) {
	encoded := base64.StdEncoding.EncodeToString(b)
	for i := 0; i < len(encoded); i += 76 {
		//Do we need to print 76 characters, or the rest of the string?
		if len(encoded)-i < 76 {
			fmt.Fprintf(w, "%s\r\n", encoded[i:])
		} else {
			fmt.Fprintf(w, "%s\r\n", encoded[i:i+76])
		}
	}
}

//headerToBytes enumerates the key and values in the header, and writes the results to the IO Writer
func headerToBytes(w io.Writer, t textproto.MIMEHeader) error {
	for k, v := range t {
		//Write the header key
		_, err := fmt.Fprintf(w, "%s:", k)
		if err != nil {
			return err
		}
		//Write each value in the header
		for _, c := range v {
			_, err := fmt.Fprintf(w, " %s\r\n", c)
			if err != nil {
				return err
			}
		}
	}
	return nil
}
<commit_msg>Encoding is now working. I believe it should (for the most part) be working.<commit_after>//Package email is designed to provide an "email interface for humans."
//Designed to be robust and flexible, the email package aims to make sending email easy without getting in the way.
package email

import (
	"bytes"
	"encoding/base64"
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"mime"
	"mime/multipart"
	"net/mail"
	"net/smtp"
	"net/textproto"
	"os"
	"path/filepath"
	"strings"
)

const (
	MAX_LINE_LENGTH = 76 //The maximum line length per RFC 2045
)

//Email is the type used for email messages
type Email struct {
	From        string
	To          []string
	Bcc         []string
	Cc          []string
	Subject     string
	Text        string //Plaintext message (optional)
	Html        string //Html message (optional)
	Headers     textproto.MIMEHeader
	Attachments map[string]*Attachment
	ReadReceipt []string
}

//NewEmail creates an Email, and returns the pointer to it.
func NewEmail() *Email {
	return &Email{Attachments: make(map[string]*Attachment), Headers: textproto.MIMEHeader{}}
}

//Attach is used to attach a file to the email.
//It attempts to open the file referenced by filename and, if successful, creates an Attachment.
//This Attachment is then appended to the slice of Email.Attachments.
//The function will then return the Attachment for reference, as well as nil for the error, if successful.
func (e *Email) Attach(filename string) (a *Attachment, err error) {
	//Check if the file exists, return any error
	if _, err := os.Stat(filename); os.IsNotExist(err) {
		return nil, err
	}
	//Read the file, and set the appropriate headers
	buffer, _ := ioutil.ReadFile(filename)
	e.Attachments[filename] = &Attachment{
		Filename: filename,
		Header:   textproto.MIMEHeader{},
		Content:  buffer}
	at := e.Attachments[filename]
	//Get the Content-Type to be used in the MIMEHeader
	ct := mime.TypeByExtension(filepath.Ext(filename))
	if ct != "" {
		at.Header.Set("Content-Type", ct)
	} else {
		//If the Content-Type is blank, set the Content-Type to "application/octet-stream"
		at.Header.Set("Content-Type", "application/octet-stream")
	}
	at.Header.Set("Content-Disposition", fmt.Sprintf("attachment;\r\n filename=\"%s\"", filename))
	at.Header.Set("Content-Transfer-Encoding", "base64")
	return e.Attachments[filename], nil
}

//Bytes converts the Email object to a []byte representation, including all needed MIMEHeaders, boundaries, etc.
func (e *Email) Bytes() ([]byte, error) {
	buff := &bytes.Buffer{}
	w := multipart.NewWriter(buff)
	//Set the appropriate headers (overwriting any conflicts)
	//Leave out Bcc (only included in envelope headers)
	e.Headers.Set("To", strings.Join(e.To, ","))
	if e.Cc != nil {
		e.Headers.Set("Cc", strings.Join(e.Cc, ","))
	}
	e.Headers.Set("From", e.From)
	e.Headers.Set("Subject", e.Subject)
	if len(e.ReadReceipt) != 0 {
		e.Headers.Set("Disposition-Notification-To", strings.Join(e.ReadReceipt, ","))
	}
	e.Headers.Set("MIME-Version", "1.0")
	e.Headers.Set("Content-Type", fmt.Sprintf("multipart/mixed;\r\n boundary=%s\r\n", w.Boundary()))

	//Write the envelope headers (including any custom headers)
	if err := headerToBytes(buff, e.Headers); err != nil {
	}
	//Start the multipart/mixed part
	fmt.Fprintf(buff, "--%s\r\n", w.Boundary())
	header := textproto.MIMEHeader{}
	//Check to see if there is a Text or HTML field
	if e.Text != "" || e.Html != "" {
		subWriter := multipart.NewWriter(buff)
		//Create the multipart alternative part
		header.Set("Content-Type", fmt.Sprintf("multipart/alternative;\r\n boundary=%s\r\n", subWriter.Boundary()))
		//Write the header
		if err := headerToBytes(buff, header); err != nil {

		}
		//Create the body sections
		if e.Text != "" {
			header.Set("Content-Type", fmt.Sprintf("text/plain; charset=UTF-8"))
			header.Set("Content-Transfer-Encoding", "quoted-printable")
			subWriter.CreatePart(header)
			// Write the text
			if err := quotePrintEncode(buff, e.Html); err != nil {
				return nil, err
			}
		}
		if e.Html != "" {
			header.Set("Content-Type", fmt.Sprintf("text/html; charset=UTF-8"))
			header.Set("Content-Transfer-Encoding", "quoted-printable")
			subWriter.CreatePart(header)
			// Write the text
			if err := quotePrintEncode(buff, e.Html); err != nil {
				return nil, err
			}
		}
		subWriter.Close()
	}
	//Create attachment part, if necessary
	if e.Attachments != nil {
		for _, a := range e.Attachments {
			ap, err := w.CreatePart(a.Header)
			if err != nil {
				return nil, err
			}
			//Write the base64Wrapped content to the part
			base64Wrap(ap, a.Content)
		}
	}
	w.Close()
	return buff.Bytes(), nil
}

//Send an email using the given host and SMTP auth (optional), returns any error thrown by smtp.SendMail
//This function merges the To, Cc, and Bcc fields and calls the smtp.SendMail function using the Email.Bytes() output as the message
func (e *Email) Send(addr string, a smtp.Auth) error {
	//Check to make sure there is at least one recipient and one "From" address
	if e.From == "" || (len(e.To) == 0 && len(e.Cc) == 0 && len(e.Bcc) == 0) {
		return errors.New("Must specify at least one From address and one To address")
	}
	// Merge the To, Cc, and Bcc fields
	to := append(append(e.To, e.Cc...), e.Bcc...)
	from, err := mail.ParseAddress(e.From)
	if err != nil {
		return err
	}
	raw, err := e.Bytes()
	if err != nil {
		return err
	}
	return smtp.SendMail(addr, a, from.Address, to, raw)
}

//Attachment is a struct representing an email attachment.
//Based on the mime/multipart.FileHeader struct, Attachment contains the name, MIMEHeader, and content of the attachment in question
type Attachment struct {
	Filename string
	Header   textproto.MIMEHeader
	Content  []byte
}

//quotePrintEncode writes the quoted-printable text to the IO Writer
func quotePrintEncode(w io.Writer, s string) error {
	mc := 0
	for _, c := range s {
		// Handle the soft break for the EOL, if needed
		if mc == 75 || (!isPrintable(c) && mc+len(fmt.Sprintf("%s%X", "=", c)) > 75) {
			if _, err := fmt.Fprintf(w, "%s", "=\r\n"); err != nil {
				return err
			}
			mc = 0
		}
		//append the appropriate character
		if isPrintable(c) {
			//Printable character
			if _, err := fmt.Fprintf(w, "%s", string(c)); err != nil {
				return err
			}
			// Reset the counter if we wrote a newline
			if c == '\n' {
				mc = 0
			}
			mc++
			continue
		} else {
			//non-printable.. encode it (TODO)
			es := fmt.Sprintf("%s%X", "=", c)
			if _, err := fmt.Fprintf(w, "%s", es); err != nil {
				return err
			}
			//todo - increment correctly
			mc += len(es)
		}
	}
	return nil
}

func isPrintable(c rune) bool {
	return (c >= '!' && c <= '<') || (c >= '>' && c <= '~') || (c == ' ' || c == '\n' || c == '\t')
}

//base64Wrap encodeds the attachment content, and wraps it according to RFC 2045 standards (every 76 chars)
//The output is then written to the specified io.Writer
func base64Wrap(w io.Writer, b []byte) {
	encoded := base64.StdEncoding.EncodeToString(b)
	for i := 0; i < len(encoded); i += 76 {
		//Do we need to print 76 characters, or the rest of the string?
		if len(encoded)-i < 76 {
			fmt.Fprintf(w, "%s\r\n", encoded[i:])
		} else {
			fmt.Fprintf(w, "%s\r\n", encoded[i:i+76])
		}
	}
}

//headerToBytes enumerates the key and values in the header, and writes the results to the IO Writer
func headerToBytes(w io.Writer, t textproto.MIMEHeader) error {
	for k, v := range t {
		//Write the header key
		_, err := fmt.Fprintf(w, "%s:", k)
		if err != nil {
			return err
		}
		//Write each value in the header
		for _, c := range v {
			_, err := fmt.Fprintf(w, " %s\r\n", c)
			if err != nil {
				return err
			}
		}
	}
	return nil
}
<|endoftext|>