<commit_before>/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package pkg

import (
	"bytes"
	"crypto/sha1"
	"fmt"
	"io/ioutil"
	"log"
	"os"
	"path/filepath"
	"strings"

	"mynewt.apache.org/newt/newt/interfaces"
	"mynewt.apache.org/newt/newt/newtutil"
	"mynewt.apache.org/newt/newt/repo"
	"mynewt.apache.org/newt/util"
	"mynewt.apache.org/newt/viper"
	"mynewt.apache.org/newt/yaml"
)

var PackageHashIgnoreDirs = map[string]bool{
	"obj": true,
	"bin": true,
	".":   true,
}

var LocalPackageSpecialNames = map[string]bool{
	"src":     true,
	"include": true,
	"bin":     true,
}

type LocalPackage struct {
	repo        *repo.Repo
	name        string
	basePath    string
	packageType interfaces.PackageType

	// General information about the package
	desc *PackageDesc
	// Dependencies for this package
	deps []*Dependency
	// APIs that this package exports
	apis []string
	// APIs that this package requires
	reqApis []string

	// Pointer to pkg.yml configuration structure
	Viper *viper.Viper

	// Names of all source yml files; used to determine if rebuild required.
	cfgFilenames []string
}

func NewLocalPackage(r *repo.Repo, pkgDir string) *LocalPackage {
	pkg := &LocalPackage{
		desc: &PackageDesc{},
		// XXX: Initialize viper object; clients should not need to check for
		// nil pointer.
	}
	pkg.Init(r, pkgDir)
	return pkg
}

func (pkg *LocalPackage) Name() string {
	return pkg.name
}

func (pkg *LocalPackage) FullName() string {
	r := pkg.Repo()
	if r.IsLocal() {
		return pkg.Name()
	} else {
		return newtutil.BuildPackageString(r.Name(), pkg.Name())
	}
}

func (pkg *LocalPackage) BasePath() string {
	return pkg.basePath
}

func (pkg *LocalPackage) Type() interfaces.PackageType {
	return pkg.packageType
}

func (pkg *LocalPackage) Repo() interfaces.RepoInterface {
	return pkg.repo
}

func (pkg *LocalPackage) Desc() *PackageDesc {
	return pkg.desc
}

func (pkg *LocalPackage) SetName(name string) {
	pkg.name = name
	// XXX: Also set "pkg.name" in viper object (possibly just remove cached
	// variable from code entirely).
}

func (pkg *LocalPackage) SetBasePath(basePath string) {
	pkg.basePath = basePath
}

func (pkg *LocalPackage) SetType(packageType interfaces.PackageType) {
	pkg.packageType = packageType
	// XXX: Also set "pkg.type" in viper object (possibly just remove cached
	// variable from code entirely).
}

func (pkg *LocalPackage) SetDesc(desc *PackageDesc) {
	pkg.desc = desc
	// XXX: Also set desc fields in viper object (possibly just remove cached
	// variable from code entirely).
}

func (pkg *LocalPackage) SetRepo(r *repo.Repo) {
	pkg.repo = r
}

func (pkg *LocalPackage) Hash() (string, error) {
	hash := sha1.New()

	err := filepath.Walk(pkg.basePath,
		func(path string, info os.FileInfo, err error) error {
			name := info.Name()
			if PackageHashIgnoreDirs[name] {
				return filepath.SkipDir
			}

			if info.IsDir() {
				// SHA the directory name into the hash
				hash.Write([]byte(name))
			} else {
				// SHA the file name & contents into the hash
				contents, err := ioutil.ReadFile(path)
				if err != nil {
					return err
				}
				hash.Write(contents)
			}
			return nil
		})
	if err != nil && err != filepath.SkipDir {
		return "", util.NewNewtError(err.Error())
	}

	hashStr := fmt.Sprintf("%x", hash.Sum(nil))

	return hashStr, nil
}

func (pkg *LocalPackage) CfgFilenames() []string {
	return pkg.cfgFilenames
}

func (pkg *LocalPackage) AddCfgFilename(cfgFilename string) {
	pkg.cfgFilenames = append(pkg.cfgFilenames, cfgFilename)
}

func (pkg *LocalPackage) HasDep(searchDep *Dependency) bool {
	for _, dep := range pkg.deps {
		if dep.String() == searchDep.String() {
			return true
		}
	}
	return false
}

func (pkg *LocalPackage) AddDep(dep *Dependency) {
	// Remember the name of the configuration file so that it can be specified
	// as a dependency to the compiler.
	pkg.deps = append(pkg.deps, dep)
}

func (pkg *LocalPackage) Deps() []*Dependency {
	return pkg.deps
}

func (pkg *LocalPackage) AddApi(api string) {
	pkg.apis = append(pkg.apis, api)
}

func (pkg *LocalPackage) Apis() []string {
	return pkg.apis
}

func (pkg *LocalPackage) AddReqApi(api string) {
	pkg.reqApis = append(pkg.reqApis, api)
}

func (pkg *LocalPackage) ReqApis() []string {
	return pkg.reqApis
}

func (pkg *LocalPackage) readDesc(v *viper.Viper) (*PackageDesc, error) {
	pdesc := &PackageDesc{}

	pdesc.Author = v.GetString("pkg.author")
	pdesc.Homepage = v.GetString("pkg.homepage")
	pdesc.Description = v.GetString("pkg.description")
	pdesc.Keywords = v.GetStringSlice("pkg.keywords")

	return pdesc, nil
}

func (pkg *LocalPackage) Init(repo *repo.Repo, pkgDir string) {
	pkg.repo = repo
	pkg.basePath = filepath.Clean(pkgDir) + "/"
}

func (pkg *LocalPackage) sequenceString(key string) string {
	var buffer bytes.Buffer

	if pkg.Viper != nil {
		for _, f := range pkg.Viper.GetStringSlice(key) {
			buffer.WriteString("    - " + yaml.EscapeString(f) + "\n")
		}
	}

	if buffer.Len() == 0 {
		return ""
	} else {
		return key + ":\n" + buffer.String()
	}
}

// Saves the package's pkg.yml file.
// NOTE: This does not save every field in the package.  Only the fields
// necessary for creating a new target get saved.
func (pkg *LocalPackage) Save() error {
	dirpath := pkg.BasePath()
	if err := os.MkdirAll(dirpath, 0755); err != nil {
		return util.NewNewtError(err.Error())
	}

	filepath := dirpath + "/" + PACKAGE_FILE_NAME
	file, err := os.Create(filepath)
	if err != nil {
		return util.NewNewtError(err.Error())
	}
	defer file.Close()

	file.WriteString("### Package: " + pkg.Name() + "\n")

	// XXX: Just iterate viper object's settings rather than calling out
	// cached settings individually.
	file.WriteString("pkg.name: " + yaml.EscapeString(pkg.Name()) + "\n")
	file.WriteString("pkg.type: " +
		yaml.EscapeString(PackageTypeNames[pkg.Type()]) + "\n")
	file.WriteString("pkg.description: " +
		yaml.EscapeString(pkg.Desc().Description) + "\n")
	file.WriteString("pkg.author: " +
		yaml.EscapeString(pkg.Desc().Author) + "\n")
	file.WriteString("pkg.homepage: " +
		yaml.EscapeString(pkg.Desc().Homepage) + "\n")
	file.WriteString("pkg.repository: " +
		yaml.EscapeString(pkg.Repo().Name()) + "\n")

	file.WriteString("\n")

	file.WriteString(pkg.sequenceString("pkg.aflags"))
	file.WriteString(pkg.sequenceString("pkg.cflags"))
	file.WriteString(pkg.sequenceString("pkg.features"))
	file.WriteString(pkg.sequenceString("pkg.lflags"))

	return nil
}

// Load reads everything that isn't identity specific into the
// package
func (pkg *LocalPackage) Load() error {
	// Load configuration
	log.Printf("[DEBUG] Loading configuration for package %s", pkg.basePath)

	v, err := util.ReadConfig(pkg.basePath,
		strings.TrimSuffix(PACKAGE_FILE_NAME, ".yml"))
	if err != nil {
		return err
	}
	pkg.Viper = v

	// Set package name from the package
	pkg.name = v.GetString("pkg.name")

	typeString := v.GetString("pkg.type")
	pkg.packageType = PACKAGE_TYPE_LIB
	for t, n := range PackageTypeNames {
		if typeString == n {
			pkg.packageType = t
			break
		}
	}

	// Read the package description from the file
	pkg.desc, err = pkg.readDesc(v)
	if err != nil {
		return err
	}

	pkg.AddCfgFilename(pkg.basePath + PACKAGE_FILE_NAME)

	return nil
}

func (pkg *LocalPackage) Clone(newRepo *repo.Repo,
	newName string) *LocalPackage {

	// XXX: Validate name.

	// Copy the package.
	newPkg := *pkg
	newPkg.repo = newRepo
	newPkg.name = newName
	newPkg.basePath = newRepo.Path() + "/" + newPkg.name

	// Insert the clone into the global package map.
	proj := interfaces.GetProject()
	pMap := proj.PackageList()
	(*pMap[newRepo.Name()])[newPkg.name] = &newPkg

	return &newPkg
}

func LoadLocalPackage(repo *repo.Repo, pkgDir string) (*LocalPackage, error) {
	pkg := &LocalPackage{}
	pkg.Init(repo, pkgDir)
	err := pkg.Load()
	return pkg, err
}

func LocalPackageSpecialName(dirName string) bool {
	_, ok := LocalPackageSpecialNames[dirName]
	return ok
}

func ReadLocalPackageRecursive(repo *repo.Repo,
	pkgList map[string]interfaces.PackageInterface, basePath string,
	pkgName string) error {

	dirList, err := ioutil.ReadDir(basePath + "/" + pkgName)
	if err != nil {
		return util.NewNewtError(err.Error())
	}

	for _, dirEnt := range dirList {
		if !dirEnt.IsDir() {
			continue
		}

		name := dirEnt.Name()
		if LocalPackageSpecialName(name) || strings.HasPrefix(name, ".") {
			continue
		}

		if err := ReadLocalPackageRecursive(repo, pkgList, basePath,
			pkgName+"/"+name); err != nil {
			return err
		}
	}

	if util.NodeNotExist(basePath + "/" + pkgName + "/" + PACKAGE_FILE_NAME) {
		return nil
	}

	pkg, err := LoadLocalPackage(repo, basePath+"/"+pkgName)
	if err != nil {
		return err
	}
	pkgList[pkg.Name()] = pkg

	return nil
}

func ReadLocalPackages(repo *repo.Repo, basePath string,
	searchPaths []string) (*map[string]interfaces.PackageInterface, error) {

	pkgList := map[string]interfaces.PackageInterface{}

	for _, path := range searchPaths {
		pkgDir := basePath + "/" + path

		if util.NodeNotExist(pkgDir) {
			continue
		}

		dirList, err := ioutil.ReadDir(pkgDir)
		if err != nil {
			return nil, util.NewNewtError(err.Error())
		}

		for _, subDir := range dirList {
			name := subDir.Name()
			if filepath.HasPrefix(name, ".") || filepath.HasPrefix(name, "..") {
				continue
			}

			if !subDir.IsDir() {
				continue
			}

			if err := ReadLocalPackageRecursive(repo, pkgList, pkgDir,
				name); err != nil {
				return nil, util.NewNewtError(err.Error())
			}
		}
	}

	return &pkgList, nil
}
<commit_msg>Don't write pkg.repository field.<commit_after>/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package pkg

import (
	"bytes"
	"crypto/sha1"
	"fmt"
	"io/ioutil"
	"log"
	"os"
	"path/filepath"
	"strings"

	"mynewt.apache.org/newt/newt/interfaces"
	"mynewt.apache.org/newt/newt/newtutil"
	"mynewt.apache.org/newt/newt/repo"
	"mynewt.apache.org/newt/util"
	"mynewt.apache.org/newt/viper"
	"mynewt.apache.org/newt/yaml"
)

var PackageHashIgnoreDirs = map[string]bool{
	"obj": true,
	"bin": true,
	".":   true,
}

var LocalPackageSpecialNames = map[string]bool{
	"src":     true,
	"include": true,
	"bin":     true,
}

type LocalPackage struct {
	repo        *repo.Repo
	name        string
	basePath    string
	packageType interfaces.PackageType

	// General information about the package
	desc *PackageDesc
	// Dependencies for this package
	deps []*Dependency
	// APIs that this package exports
	apis []string
	// APIs that this package requires
	reqApis []string

	// Pointer to pkg.yml configuration structure
	Viper *viper.Viper

	// Names of all source yml files; used to determine if rebuild required.
	cfgFilenames []string
}

func NewLocalPackage(r *repo.Repo, pkgDir string) *LocalPackage {
	pkg := &LocalPackage{
		desc: &PackageDesc{},
		// XXX: Initialize viper object; clients should not need to check for
		// nil pointer.
	}
	pkg.Init(r, pkgDir)
	return pkg
}

func (pkg *LocalPackage) Name() string {
	return pkg.name
}

func (pkg *LocalPackage) FullName() string {
	r := pkg.Repo()
	if r.IsLocal() {
		return pkg.Name()
	} else {
		return newtutil.BuildPackageString(r.Name(), pkg.Name())
	}
}

func (pkg *LocalPackage) BasePath() string {
	return pkg.basePath
}

func (pkg *LocalPackage) Type() interfaces.PackageType {
	return pkg.packageType
}

func (pkg *LocalPackage) Repo() interfaces.RepoInterface {
	return pkg.repo
}

func (pkg *LocalPackage) Desc() *PackageDesc {
	return pkg.desc
}

func (pkg *LocalPackage) SetName(name string) {
	pkg.name = name
	// XXX: Also set "pkg.name" in viper object (possibly just remove cached
	// variable from code entirely).
}

func (pkg *LocalPackage) SetBasePath(basePath string) {
	pkg.basePath = basePath
}

func (pkg *LocalPackage) SetType(packageType interfaces.PackageType) {
	pkg.packageType = packageType
	// XXX: Also set "pkg.type" in viper object (possibly just remove cached
	// variable from code entirely).
}

func (pkg *LocalPackage) SetDesc(desc *PackageDesc) {
	pkg.desc = desc
	// XXX: Also set desc fields in viper object (possibly just remove cached
	// variable from code entirely).
}

func (pkg *LocalPackage) SetRepo(r *repo.Repo) {
	pkg.repo = r
}

func (pkg *LocalPackage) Hash() (string, error) {
	hash := sha1.New()

	err := filepath.Walk(pkg.basePath,
		func(path string, info os.FileInfo, err error) error {
			name := info.Name()
			if PackageHashIgnoreDirs[name] {
				return filepath.SkipDir
			}

			if info.IsDir() {
				// SHA the directory name into the hash
				hash.Write([]byte(name))
			} else {
				// SHA the file name & contents into the hash
				contents, err := ioutil.ReadFile(path)
				if err != nil {
					return err
				}
				hash.Write(contents)
			}
			return nil
		})
	if err != nil && err != filepath.SkipDir {
		return "", util.NewNewtError(err.Error())
	}

	hashStr := fmt.Sprintf("%x", hash.Sum(nil))

	return hashStr, nil
}

func (pkg *LocalPackage) CfgFilenames() []string {
	return pkg.cfgFilenames
}

func (pkg *LocalPackage) AddCfgFilename(cfgFilename string) {
	pkg.cfgFilenames = append(pkg.cfgFilenames, cfgFilename)
}

func (pkg *LocalPackage) HasDep(searchDep *Dependency) bool {
	for _, dep := range pkg.deps {
		if dep.String() == searchDep.String() {
			return true
		}
	}
	return false
}

func (pkg *LocalPackage) AddDep(dep *Dependency) {
	// Remember the name of the configuration file so that it can be specified
	// as a dependency to the compiler.
	pkg.deps = append(pkg.deps, dep)
}

func (pkg *LocalPackage) Deps() []*Dependency {
	return pkg.deps
}

func (pkg *LocalPackage) AddApi(api string) {
	pkg.apis = append(pkg.apis, api)
}

func (pkg *LocalPackage) Apis() []string {
	return pkg.apis
}

func (pkg *LocalPackage) AddReqApi(api string) {
	pkg.reqApis = append(pkg.reqApis, api)
}

func (pkg *LocalPackage) ReqApis() []string {
	return pkg.reqApis
}

func (pkg *LocalPackage) readDesc(v *viper.Viper) (*PackageDesc, error) {
	pdesc := &PackageDesc{}

	pdesc.Author = v.GetString("pkg.author")
	pdesc.Homepage = v.GetString("pkg.homepage")
	pdesc.Description = v.GetString("pkg.description")
	pdesc.Keywords = v.GetStringSlice("pkg.keywords")

	return pdesc, nil
}

func (pkg *LocalPackage) Init(repo *repo.Repo, pkgDir string) {
	pkg.repo = repo
	pkg.basePath = filepath.Clean(pkgDir) + "/"
}

func (pkg *LocalPackage) sequenceString(key string) string {
	var buffer bytes.Buffer

	if pkg.Viper != nil {
		for _, f := range pkg.Viper.GetStringSlice(key) {
			buffer.WriteString("    - " + yaml.EscapeString(f) + "\n")
		}
	}

	if buffer.Len() == 0 {
		return ""
	} else {
		return key + ":\n" + buffer.String()
	}
}

// Saves the package's pkg.yml file.
// NOTE: This does not save every field in the package.  Only the fields
// necessary for creating a new target get saved.
func (pkg *LocalPackage) Save() error {
	dirpath := pkg.BasePath()
	if err := os.MkdirAll(dirpath, 0755); err != nil {
		return util.NewNewtError(err.Error())
	}

	filepath := dirpath + "/" + PACKAGE_FILE_NAME
	file, err := os.Create(filepath)
	if err != nil {
		return util.NewNewtError(err.Error())
	}
	defer file.Close()

	file.WriteString("### Package: " + pkg.Name() + "\n")

	// XXX: Just iterate viper object's settings rather than calling out
	// cached settings individually.
	file.WriteString("pkg.name: " + yaml.EscapeString(pkg.Name()) + "\n")
	file.WriteString("pkg.type: " +
		yaml.EscapeString(PackageTypeNames[pkg.Type()]) + "\n")
	file.WriteString("pkg.description: " +
		yaml.EscapeString(pkg.Desc().Description) + "\n")
	file.WriteString("pkg.author: " +
		yaml.EscapeString(pkg.Desc().Author) + "\n")
	file.WriteString("pkg.homepage: " +
		yaml.EscapeString(pkg.Desc().Homepage) + "\n")

	file.WriteString("\n")

	file.WriteString(pkg.sequenceString("pkg.aflags"))
	file.WriteString(pkg.sequenceString("pkg.cflags"))
	file.WriteString(pkg.sequenceString("pkg.features"))
	file.WriteString(pkg.sequenceString("pkg.lflags"))

	return nil
}

// Load reads everything that isn't identity specific into the
// package
func (pkg *LocalPackage) Load() error {
	// Load configuration
	log.Printf("[DEBUG] Loading configuration for package %s", pkg.basePath)

	v, err := util.ReadConfig(pkg.basePath,
		strings.TrimSuffix(PACKAGE_FILE_NAME, ".yml"))
	if err != nil {
		return err
	}
	pkg.Viper = v

	// Set package name from the package
	pkg.name = v.GetString("pkg.name")

	typeString := v.GetString("pkg.type")
	pkg.packageType = PACKAGE_TYPE_LIB
	for t, n := range PackageTypeNames {
		if typeString == n {
			pkg.packageType = t
			break
		}
	}

	// Read the package description from the file
	pkg.desc, err = pkg.readDesc(v)
	if err != nil {
		return err
	}

	pkg.AddCfgFilename(pkg.basePath + PACKAGE_FILE_NAME)

	return nil
}

func (pkg *LocalPackage) Clone(newRepo *repo.Repo,
	newName string) *LocalPackage {

	// XXX: Validate name.

	// Copy the package.
	newPkg := *pkg
	newPkg.repo = newRepo
	newPkg.name = newName
	newPkg.basePath = newRepo.Path() + "/" + newPkg.name

	// Insert the clone into the global package map.
	proj := interfaces.GetProject()
	pMap := proj.PackageList()
	(*pMap[newRepo.Name()])[newPkg.name] = &newPkg

	return &newPkg
}

func LoadLocalPackage(repo *repo.Repo, pkgDir string) (*LocalPackage, error) {
	pkg := &LocalPackage{}
	pkg.Init(repo, pkgDir)
	err := pkg.Load()
	return pkg, err
}

func LocalPackageSpecialName(dirName string) bool {
	_, ok := LocalPackageSpecialNames[dirName]
	return ok
}

func ReadLocalPackageRecursive(repo *repo.Repo,
	pkgList map[string]interfaces.PackageInterface, basePath string,
	pkgName string) error {

	dirList, err := ioutil.ReadDir(basePath + "/" + pkgName)
	if err != nil {
		return util.NewNewtError(err.Error())
	}

	for _, dirEnt := range dirList {
		if !dirEnt.IsDir() {
			continue
		}

		name := dirEnt.Name()
		if LocalPackageSpecialName(name) || strings.HasPrefix(name, ".") {
			continue
		}

		if err := ReadLocalPackageRecursive(repo, pkgList, basePath,
			pkgName+"/"+name); err != nil {
			return err
		}
	}

	if util.NodeNotExist(basePath + "/" + pkgName + "/" + PACKAGE_FILE_NAME) {
		return nil
	}

	pkg, err := LoadLocalPackage(repo, basePath+"/"+pkgName)
	if err != nil {
		return err
	}
	pkgList[pkg.Name()] = pkg

	return nil
}

func ReadLocalPackages(repo *repo.Repo, basePath string,
	searchPaths []string) (*map[string]interfaces.PackageInterface, error) {

	pkgList := map[string]interfaces.PackageInterface{}

	for _, path := range searchPaths {
		pkgDir := basePath + "/" + path

		if util.NodeNotExist(pkgDir) {
			continue
		}

		dirList, err := ioutil.ReadDir(pkgDir)
		if err != nil {
			return nil, util.NewNewtError(err.Error())
		}

		for _, subDir := range dirList {
			name := subDir.Name()
			if filepath.HasPrefix(name, ".") || filepath.HasPrefix(name, "..") {
				continue
			}

			if !subDir.IsDir() {
				continue
			}

			if err := ReadLocalPackageRecursive(repo, pkgList, pkgDir,
				name); err != nil {
				return nil, util.NewNewtError(err.Error())
			}
		}
	}

	return &pkgList, nil
}
<|endoftext|>


<commit_before>// meow_queue.go

package meow-data-structures

import "sync"

type meowQueueNode struct {
	meowData interface{}
	meowNxt *meowQueueNode
}

// go-routine safe FIFO
type meowQueue struct {
	meowHead *meowQueueNode
	meowTail *meowQueueNode
	meowCount int
	meowLock *sync.Mutex
}<commit_msg>add pointer to new meowQueue<commit_after>// meow_queue.go

package meow-data-structures

import "sync"

type meowQueueNode struct {
	meowData interface{}
	meowNxt *meowQueueNode
}

// go-routine safe FIFO
type meowQueue struct {
	meowHead *meowQueueNode
	meowTail *meowQueueNode
	meowCount int
	meowLock *sync.Mutex
}

// creating a new pointer to a new meowQueue
func meowNewQueue() *meowQueue {
	q := &meowQueue{}
	q.meowLock = &sync.Mutex{}
	return q
}<|endoftext|>


<commit_before>package uploader

import (
	"bytes"
	"errors"
	"fmt"
	"os"
	"path"
	"path/filepath"
	"reflect"
	"time"

	"koding/kites/keygen"
	"koding/klient/storage"
	"koding/logrotate"

	"github.com/boltdb/bolt"
	"github.com/koding/kite"
	"github.com/koding/logging"
)

var defaultLog = logging.NewCustom("uploader", false)

// Options represents arguments required to create a Uploader value.
type Options struct {
	KeygenURL string      // required
	Kite      *kite.Kite  // required
	Bucket    string      // required
	Region    string      // required
	DB        *bolt.DB    // optional; in-memory store if nil
	Log       kite.Logger // optional; defaultLog if nil
}

// Uploader is a kite handler for "log.upload" method.
type Uploader struct {
	cfg    *Options
	rotate *logrotate.Uploader
	req    chan *UploadRequest
	close  chan struct{}
}

// New gives new uploader built from the given options.
func New(cfg *Options) *Uploader {
	log := defaultLog
	if l, ok := cfg.Log.(logging.Logger); ok {
		log = l
	}

	up := &Uploader{
		cfg: cfg,
		rotate: &logrotate.Uploader{
			UserBucket: keygen.NewUserBucket(&keygen.Config{
				ServerURL: cfg.KeygenURL,
				Kite:      cfg.Kite,
				Bucket:    cfg.Bucket,
				Region:    cfg.Region,
				Log:       log,
			}),
			MetaStore: storage.NewEncodingStorage(cfg.DB, []byte("uploader.metadata")),
		},
		req:   make(chan *UploadRequest),
		close: make(chan struct{}),
	}

	go up.process()

	return up
}

// UploadRequest represents a request of the "log.upload" kite method.
type UploadRequest struct {
	// File is a path to a file to upload.
	// File path is also used as a S3 key.
	File string `json:"file"`

	// Interval makes the uploader stream the File at the given interval.
	// By default File is not streamed.
	//
	// Ignored when File is empty and Content is used instead.
	// Minimal interval is 15m.
	Interval time.Duration `json:"interval"`

	// Content is a one-time log content to upload, requires key to be non-empty.
	// Superseded by File, if non-empty.
	Content []byte `json:"content"`

	// Key is required when Content is set.
	Key string `json:"key"`
}

// Valid validates the request.
func (req *UploadRequest) Valid() error {
	if req.File == "" && len(req.Content) != 0 && req.Key == "" {
		return errors.New("missing key")
	}

	if req.File != "" {
		if _, err := os.Stat(req.File); err != nil {
			return err
		}

		req.File = filepath.ToSlash(filepath.Clean(req.File))
	}

	return nil
}

func (req *UploadRequest) key() string {
	if len(req.Content) != 0 && req.File == "" {
		return req.Key
	}

	return req.File
}

// UploadResponse represents a response of the "log.upload" kite method.
type UploadResponse struct {
	URL string `json:"url"`
}

// UploadFile uploads the given file to the S3.
//
// If interval is > 0 then the given file is uploaded to S3 at the given interval.
func (up *Uploader) UploadFile(file string, interval time.Duration) (string, error) {
	req := &UploadRequest{
		File:     file,
		Interval: interval,
	}

	return up.upload(req)
}

// Upload is a kite handler for the "log.upload" method.
func (up *Uploader) Upload(r *kite.Request) (interface{}, error) {
	if r.Args == nil {
		return nil, errors.New("missing argument")
	}

	var req UploadRequest
	if err := r.Args.One().Unmarshal(&req); err != nil {
		return nil, err
	}

	s, err := up.upload(&req)
	if err != nil {
		return nil, err
	}

	return &UploadResponse{
		URL: s,
	}, nil
}

// Close stops uploading logs to the S3.
func (up *Uploader) Close() error {
	if up.close != nil {
		close(up.close)
		up.close = nil
	}

	return nil
}

func (up *Uploader) upload(req *UploadRequest) (string, error) {
	if err := req.Valid(); err != nil {
		return "", err
	}

	up.req <- req

	return up.url(req), nil
}

func (up *Uploader) url(req *UploadRequest) string {
	return fmt.Sprintf("https://%s.s3.amazonaws.com/%s", up.cfg.Bucket, path.Clean(up.cfg.Kite.Config.Id+"/"+req.key()))
}

func (up *Uploader) log() kite.Logger {
	if up.cfg.Log != nil {
		return up.cfg.Log
	}

	return defaultLog
}

func (up *Uploader) process() {
	var (
		files   = make(map[int]string)
		tickers []*time.Ticker
		cases   = []reflect.SelectCase{
			0: {Dir: reflect.SelectRecv, Chan: reflect.ValueOf(up.close)},
			1: {Dir: reflect.SelectRecv, Chan: reflect.ValueOf(up.req)},
		}
		prefix  = up.cfg.Kite.Config.Id
		watched = func(file string) bool {
			for _, f := range files {
				if f == file {
					return true
				}
			}

			return false
		}
	)

	for {
		var req *UploadRequest

		switch n, v, _ := reflect.Select(cases); n {
		case 0:
			for _, t := range tickers {
				t.Stop()
			}
			return
		case 1:
			req = v.Interface().(*UploadRequest)

			if req.Interval > 0 && req.File != "" {
				if req.Interval < 15*time.Minute {
					req.Interval = 15 * time.Minute
				}

				if !watched(req.File) {
					t := time.NewTicker(req.Interval)

					tickers = append(tickers, t)
					files[len(cases)] = req.File
					cases = append(cases, reflect.SelectCase{
						Dir:  reflect.SelectRecv,
						Chan: reflect.ValueOf(t.C),
					})
				}
			}

			fallthrough

		default:
			var err error
			var key string

			if req == nil {
				err = up.rotate.UploadFile(prefix, files[n])
				key = prefix + "/" + files[n]
			} else if req.File != "" {
				err = up.rotate.UploadFile(prefix, req.File)
				key = prefix + "/" + req.File
			} else {
				err = up.rotate.Upload(key, bytes.NewReader(req.Content))
				key = prefix + "/" + req.Key
			}

			switch key = path.Clean(key); {
			case err == nil:
				up.log().Debug("%s: uploaded successfully", key)
			case logrotate.IsNop(err):
				up.log().Debug("%s: nothing to upload", key)
			case os.IsNotExist(err):
				up.log().Debug("%s: file does not exist", key)
			default:
				up.log().Error("%s: failed to upload: %s", key, err)
			}
		}
	}
}
<commit_msg>klient/uploader: rework up.process to not use reflect<commit_after>package uploader

import (
	"bytes"
	"errors"
	"fmt"
	"os"
	"path"
	"path/filepath"
	"time"

	"koding/kites/keygen"
	"koding/klient/storage"
	"koding/logrotate"

	"github.com/boltdb/bolt"
	"github.com/koding/kite"
	"github.com/koding/logging"
)

var defaultLog = logging.NewCustom("uploader", false)

// Options represents arguments required to create a Uploader value.
type Options struct {
	KeygenURL string      // required
	Kite      *kite.Kite  // required
	Bucket    string      // required
	Region    string      // required
	DB        *bolt.DB    // optional; in-memory store if nil
	Log       kite.Logger // optional; defaultLog if nil
}

// Uploader is a kite handler for "log.upload" method.
type Uploader struct {
	cfg    *Options
	rotate *logrotate.Uploader
	req    chan *UploadRequest
	close  chan struct{}
}

// New gives new uploader built from the given options.
func New(cfg *Options) *Uploader {
	log := defaultLog
	if l, ok := cfg.Log.(logging.Logger); ok {
		log = l
	}

	up := &Uploader{
		cfg: cfg,
		rotate: &logrotate.Uploader{
			UserBucket: keygen.NewUserBucket(&keygen.Config{
				ServerURL: cfg.KeygenURL,
				Kite:      cfg.Kite,
				Bucket:    cfg.Bucket,
				Region:    cfg.Region,
				Log:       log,
			}),
			MetaStore: storage.NewEncodingStorage(cfg.DB, []byte("uploader.metadata")),
		},
		req:   make(chan *UploadRequest),
		close: make(chan struct{}),
	}

	go up.process()

	return up
}

// UploadRequest represents a request of the "log.upload" kite method.
type UploadRequest struct {
	// File is a path to a file to upload.
	// File path is also used as a S3 key.
	File string `json:"file"`

	// Interval makes the uploader stream the File at the given interval.
	// By default File is not streamed.
	//
	// Ignored when File is empty and Content is used instead.
	// Minimal interval is 15m.
	Interval time.Duration `json:"interval"`

	// Content is a one-time log content to upload, requires key to be non-empty.
	// Superseded by File, if non-empty.
	Content []byte `json:"content"`

	// Key is required when Content is set.
	Key string `json:"key"`
}

// Valid validates the request.
func (req *UploadRequest) Valid() error {
	if req.File == "" && len(req.Content) != 0 && req.Key == "" {
		return errors.New("missing key")
	}

	if req.File != "" {
		if _, err := os.Stat(req.File); err != nil {
			return err
		}

		req.File = filepath.ToSlash(filepath.Clean(req.File))
	}

	return nil
}

func (req *UploadRequest) key() string {
	if len(req.Content) != 0 && req.File == "" {
		return req.Key
	}

	return req.File
}

// UploadResponse represents a response of the "log.upload" kite method.
type UploadResponse struct {
	URL string `json:"url"`
}

// UploadFile uploads the given file to the S3.
//
// If interval is > 0 then the given file is uploaded to S3 at the given interval.
func (up *Uploader) UploadFile(file string, interval time.Duration) (string, error) {
	req := &UploadRequest{
		File:     file,
		Interval: interval,
	}

	return up.upload(req)
}

// Upload is a kite handler for the "log.upload" method.
func (up *Uploader) Upload(r *kite.Request) (interface{}, error) {
	if r.Args == nil {
		return nil, errors.New("missing argument")
	}

	var req UploadRequest
	if err := r.Args.One().Unmarshal(&req); err != nil {
		return nil, err
	}

	s, err := up.upload(&req)
	if err != nil {
		return nil, err
	}

	return &UploadResponse{
		URL: s,
	}, nil
}

// Close stops uploading logs to the S3.
func (up *Uploader) Close() error {
	if up.close != nil {
		close(up.close)
		up.close = nil
	}

	return nil
}

func (up *Uploader) upload(req *UploadRequest) (string, error) {
	if err := req.Valid(); err != nil {
		return "", err
	}

	up.req <- req

	return up.url(req), nil
}

func (up *Uploader) url(req *UploadRequest) string {
	return fmt.Sprintf("https://%s.s3.amazonaws.com/%s", up.cfg.Bucket, path.Clean(up.cfg.Kite.Config.Id+"/"+req.key()))
}

func (up *Uploader) log() kite.Logger {
	if up.cfg.Log != nil {
		return up.cfg.Log
	}

	return defaultLog
}

func (up *Uploader) process() {
	watched := make(map[string]struct{})
	prefix := up.cfg.Kite.Config.Id

	for {
		select {
		case <-up.close:
			return

		case req := <-up.req:
			// Upload file at the given interval, if the file
			// is requested to be watched.
			if req.Interval > 0 && req.File != "" {
				if req.Interval < 15*time.Minute {
					req.Interval = 15 * time.Minute
				}

				if _, ok := watched[req.File]; !ok {
					go func(file string) {
						t := time.NewTicker(req.Interval)
						defer t.Stop()

						for {
							select {
							case <-up.close:
								return
							case <-t.C:
								up.req <- &UploadRequest{
									File: file,
								}
							}
						}
					}(req.File)
				}
			}

			var err error
			var key string

			if req.File != "" {
				err = up.rotate.UploadFile(prefix, req.File)
				key = prefix + "/" + req.File
			} else {
				err = up.rotate.Upload(key, bytes.NewReader(req.Content))
				key = prefix + "/" + req.Key
			}

			switch key = path.Clean(key); {
			case err == nil:
				up.log().Debug("%s: uploaded successfully", key)
			case logrotate.IsNop(err):
				up.log().Debug("%s: nothing to upload", key)
			case os.IsNotExist(err):
				up.log().Debug("%s: file does not exist", key)
			default:
				up.log().Error("%s: failed to upload: %s", key, err)
			}
		}
	}
}
<|endoftext|>


<commit_before>package models

import (
	"errors"
	"fmt"
	"socialapi/config"
	"socialapi/request"
	"time"

	"github.com/VerbalExpressions/GoVerbalExpressions"
	"github.com/jinzhu/gorm"
	"github.com/koding/bongo"
)

var mentionRegex = verbalexpressions.New().
	Find("@").
	BeginCapture().
	Word().
	EndCapture().
	Regex()

type ChannelMessage struct {
	// unique identifier of the channel message
	Id int64 `json:"id,string"`

	// Token holds the uuid for interoperability with the bongo-client
	Token string `json:"token"`

	// Body of the mesage
	Body string `json:"body"`

	// Generated Slug for body
	Slug string `json:"slug"                               sql:"NOT NULL;TYPE:VARCHAR(100);"`

	// type of the m essage
	TypeConstant string `json:"typeConstant"               sql:"NOT NULL;TYPE:VARCHAR(100);"`

	// Creator of the channel message
	AccountId int64 `json:"accountId,string"               sql:"NOT NULL"`

	// in which channel this message is created
	InitialChannelId int64 `json:"initialChannelId,string" sql:"NOT NULL"`

	// holds troll, unsafe, etc
	MetaBits MetaBits `json:"metaBits"`

	// Creation date of the message
	CreatedAt time.Time `json:"createdAt"                  sql:"DEFAULT:CURRENT_TIMESTAMP"`

	// Modification date of the message
	UpdatedAt time.Time `json:"updatedAt"                  sql:"DEFAULT:CURRENT_TIMESTAMP"`

	// Deletion date of the channel message
	DeletedAt time.Time `json:"deletedAt"`

	// Extra data storage
	Payload gorm.Hstore `json:"payload,omitempty"`
}

const (
	ChannelMessage_TYPE_POST            = "post"
	ChannelMessage_TYPE_REPLY           = "reply"
	ChannelMessage_TYPE_JOIN            = "join"
	ChannelMessage_TYPE_LEAVE           = "leave"
	ChannelMessage_TYPE_CHAT            = "chat"
	ChannelMessage_TYPE_PRIVATE_MESSAGE = "privatemessage"
)

func (c *ChannelMessage) MarkIfExempt() error {
	isExempt, err := c.isExempt()
	if err != nil {
		return err
	}

	if isExempt {
		c.MetaBits.Mark(Troll)
	}

	return nil
}

func (c *ChannelMessage) isExempt() (bool, error) {
	if c.MetaBits.Is(Troll) {
		return true, nil
	}

	accountId, err := c.getAccountId()
	if err != nil {
		return false, err
	}

	account, err := ResetAccountCache(accountId)
	if err != nil {
		return false, err
	}

	if account == nil {
		return false, fmt.Errorf("account is nil, accountId:%d", c.AccountId)
	}

	if account.IsTroll {
		return true, nil
	}

	return false, nil
}

func (c *ChannelMessage) getAccountId() (int64, error) {
	if c.AccountId != 0 {
		return c.AccountId, nil
	}

	if c.Id == 0 {
		return 0, fmt.Errorf("couldnt find accountId from content %+v", c)
	}

	cm := NewChannelMessage()
	if err := cm.ById(c.Id); err != nil {
		return 0, err
	}

	return cm.AccountId, nil
}

func bodyLenCheck(body string) error {
	if len(body) < config.MustGet().Limits.MessageBodyMinLen {
		return fmt.Errorf("message body length should be greater than %d, yours is %d ", config.MustGet().Limits.MessageBodyMinLen, len(body))
	}

	return nil
}

// todo create a new message while updating the channel_message and delete other
// cases, since deletion is a soft delete, old instances will still be there

// CreateRaw creates a new channel message without effected by auto generated createdAt
// and updatedAt values
func (c *ChannelMessage) CreateRaw() error {
	insertSql := "INSERT INTO " +
		c.TableName() +
		` ("body","slug","type_constant","account_id","initial_channel_id",` +
		`"created_at","updated_at","deleted_at","payload") ` +
		"VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9) " +
		"RETURNING ID"

	return bongo.B.DB.CommonDB().QueryRow(insertSql, c.Body, c.Slug, c.TypeConstant, c.AccountId, c.InitialChannelId,
		c.CreatedAt, c.UpdatedAt, c.DeletedAt, c.Payload).Scan(&c.Id)
}

// UpdateBodyRaw updates message body without effecting createdAt/UpdatedAt
// timestamps
func (c *ChannelMessage) UpdateBodyRaw() error {
	updateSql := fmt.Sprintf("UPDATE %s SET body=? WHERE id=?", c.TableName())

	return bongo.B.DB.Exec(updateSql, c.Body, c.Id).Error
}

func (c *ChannelMessage) BuildMessages(query *request.Query, messages []ChannelMessage) ([]*ChannelMessageContainer, error) {
	containers := make([]*ChannelMessageContainer, len(messages))
	if len(containers) == 0 {
		return containers, nil
	}

	for i, message := range messages {
		d := NewChannelMessage()
		*d = message
		data, err := d.BuildMessage(query)
		if err != nil {
			return containers, err
		}
		containers[i] = data
	}

	return containers, nil
}

func (c *ChannelMessage) BuildMessage(query *request.Query) (*ChannelMessageContainer, error) {
	cmc, err := c.FetchRelatives(query)
	if err != nil {
		return nil, err
	}

	mr := NewMessageReply()
	mr.MessageId = c.Id

	q := query.Clone()

	q.Limit = query.ReplyLimit
	q.Skip = query.ReplySkip

	replies, err := mr.List(q)
	if err != nil {
		return nil, err
	}

	repliesCount, err := mr.Count(q)
	if err != nil {
		return nil, err
	}
	cmc.RepliesCount = repliesCount

	cmc.IsFollowed, err = c.CheckIsMessageFollowed(q)
	if err != nil {
		return nil, err
	}

	populatedChannelMessagesReplies := make([]*ChannelMessageContainer, len(replies))
	for rl := 0; rl < len(replies); rl++ {
		cmrc, err := replies[rl].FetchRelatives(q)
		if err != nil {
			return nil, err
		}
		populatedChannelMessagesReplies[rl] = cmrc
	}

	cmc.Replies = populatedChannelMessagesReplies
	return cmc, nil
}

func (c *ChannelMessage) CheckIsMessageFollowed(query *request.Query) (bool, error) {
	channel := NewChannel()
	if err := channel.FetchPinnedActivityChannel(query.AccountId, query.GroupName); err != nil {
		if err == bongo.RecordNotFound {
			return false, nil
		}
		return false, err
	}

	cml := NewChannelMessageList()
	q := &bongo.Query{
		Selector: map[string]interface{}{
			"channel_id": channel.Id,
			"message_id": c.Id,
		},
	}
	if err := cml.One(q); err != nil {
		if err == bongo.RecordNotFound {
			return false, nil
		}

		return false, err
	}

	return true, nil
}

func (c *ChannelMessage) BuildEmptyMessageContainer() (*ChannelMessageContainer, error) {
	if c.Id == 0 {
		return nil, errors.New("Channel message id is not set")
	}
	container := NewChannelMessageContainer()
	container.Message = c

	oldId, err := FetchAccountOldIdByIdFromCache(c.AccountId)
	if err != nil {
		return nil, err
	}

	container.AccountOldId = oldId

	interactionContainer := NewInteractionContainer()
	interactionContainer.ActorsPreview = make([]string, 0)
	interactionContainer.IsInteracted = false
	interactionContainer.ActorsCount = 0

	container.Interactions = make(map[string]*InteractionContainer)
	container.Interactions["like"] = interactionContainer

	return container, nil
}

func (c *ChannelMessage) FetchRelatives(query *request.Query) (*ChannelMessageContainer, error) {
	container, err := c.BuildEmptyMessageContainer()
	if err != nil {
		return nil, err
	}

	i := NewInteraction()
	i.MessageId = c.Id
	// get preview
	query.Type = "like"
	query.Limit = 3

	interactionContainer, err := i.FetchInteractionContainer(query)
	if err != nil {
		return nil, err
	}

	container.Interactions[query.Type] = interactionContainer

	return container, nil
}

func generateMessageListQuery(channelId int64, q *request.Query) *bongo.Query {
	messageType := q.Type
	if messageType == "" {
		messageType = ChannelMessage_TYPE_POST
	}

	return &bongo.Query{
		Selector: map[string]interface{}{
			"account_id":         q.AccountId,
			"initial_channel_id": channelId,
			"type_constant":      messageType,
		},
		Pagination: *bongo.NewPagination(q.Limit, q.Skip),
		Sort: map[string]string{
			"created_at": "DESC",
		},
	}
}

func (c *ChannelMessage) FetchMessagesByChannelId(channelId int64, q *request.Query) ([]ChannelMessage, error) {
	query := generateMessageListQuery(channelId, q)

	var messages []ChannelMessage
	if err := c.Some(&messages, query); err != nil {
		return nil, err
	}

	if messages == nil {
		return make([]ChannelMessage, 0), nil
	}
	return messages, nil
}

func (c *ChannelMessage) GetMentionedUsernames() []string {
	flattened := make([]string, 0)

	res := mentionRegex.FindAllStringSubmatch(c.Body, -1)
	if len(res) == 0 {
		return flattened
	}

	participants := map[string]struct{}{}
	// remove duplicate mentions
	for _, ele := range res {
		participants[ele[1]] = struct{}{}
	}

	for participant := range participants {
		flattened = append(flattened, participant)
	}

	return flattened
}

func (c *ChannelMessage) FetchTotalMessageCount(q *request.Query) (int, error) {
	query := &bongo.Query{
		Selector: map[string]interface{}{
			"account_id":    q.AccountId,
			"type_constant": q.Type,
		},
		Pagination: *bongo.NewPagination(q.Limit, q.Skip),
	}

	query.AddScope(RemoveTrollContent(
		c, q.ShowExempt,
	))

	return c.CountWithQuery(query)
}

func (c *ChannelMessage) FetchMessageIds(q *request.Query) ([]int64, error) {
	query := &bongo.Query{
		Selector: map[string]interface{}{
			"account_id":    q.AccountId,
			"type_constant": q.Type,
		},
		Pluck:      "id",
		Pagination: *bongo.NewPagination(q.Limit, q.Skip),
		Sort: map[string]string{
			"created_at": "DESC",
		},
	}

	query.AddScope(RemoveTrollContent(c, q.ShowExempt))

	var messageIds []int64
	if err := c.Some(&messageIds, query); err != nil {
		return nil, err
	}

	if messageIds == nil {
		return make([]int64, 0), nil
	}

	return messageIds, nil
}

// BySlug fetchs channel message by its slug
// checks if message is in the channel or not
func (c *ChannelMessage) BySlug(query *request.Query) error {
	if query.Slug == "" {
		return errors.New("slug is not set")
	}

	// fetch message itself
	q := &bongo.Query{
		Selector: map[string]interface{}{
			"slug": query.Slug,
		},
	}

	q.AddScope(RemoveTrollContent(
		c, query.ShowExempt,
	))

	if err := c.One(q); err != nil {
		return err
	}

	// fetch channel by group name
	query.Name = query.GroupName
	query.Type = Channel_TYPE_GROUP
	ch := NewChannel()
	channel, err := ch.ByName(query)
	if err != nil {
		return err
	}

	if channel.Id == 0 {
		return errors.New("channel is not found")
	}

	// check if message is in the channel
	cml := NewChannelMessageList()
	res, err := cml.IsInChannel(c.Id, channel.Id)
	if err != nil {
		return err
	}

	// if message is not in the channel
	if !res {
		return bongo.RecordNotFound
	}

	return nil
}
<commit_msg>Social: clear BuildMessage function<commit_after>package models

import (
	"errors"
	"fmt"
	"socialapi/config"
	"socialapi/request"
	"time"

	"github.com/VerbalExpressions/GoVerbalExpressions"
	"github.com/jinzhu/gorm"
	"github.com/koding/bongo"
)

var mentionRegex = verbalexpressions.New().
	Find("@").
	BeginCapture().
	Word().
	EndCapture().
	Regex()

type ChannelMessage struct {
	// unique identifier of the channel message
	Id int64 `json:"id,string"`

	// Token holds the uuid for interoperability with the bongo-client
	Token string `json:"token"`

	// Body of the mesage
	Body string `json:"body"`

	// Generated Slug for body
	Slug string `json:"slug"                               sql:"NOT NULL;TYPE:VARCHAR(100);"`

	// type of the m essage
	TypeConstant string `json:"typeConstant"               sql:"NOT NULL;TYPE:VARCHAR(100);"`

	// Creator of the channel message
	AccountId int64 `json:"accountId,string"               sql:"NOT NULL"`

	// in which channel this message is created
	InitialChannelId int64 `json:"initialChannelId,string" sql:"NOT NULL"`

	// holds troll, unsafe, etc
	MetaBits MetaBits `json:"metaBits"`

	// Creation date of the message
	CreatedAt time.Time `json:"createdAt"                  sql:"DEFAULT:CURRENT_TIMESTAMP"`

	// Modification date of the message
	UpdatedAt time.Time `json:"updatedAt"                  sql:"DEFAULT:CURRENT_TIMESTAMP"`

	// Deletion date of the channel message
	DeletedAt time.Time `json:"deletedAt"`

	// Extra data storage
	Payload gorm.Hstore `json:"payload,omitempty"`
}

const (
	ChannelMessage_TYPE_POST            = "post"
	ChannelMessage_TYPE_REPLY           = "reply"
	ChannelMessage_TYPE_JOIN            = "join"
	ChannelMessage_TYPE_LEAVE           = "leave"
	ChannelMessage_TYPE_CHAT            = "chat"
	ChannelMessage_TYPE_PRIVATE_MESSAGE = "privatemessage"
)

func (c *ChannelMessage) MarkIfExempt() error {
	isExempt, err := c.isExempt()
	if err != nil {
		return err
	}

	if isExempt {
		c.MetaBits.Mark(Troll)
	}

	return nil
}

func (c *ChannelMessage) isExempt() (bool, error) {
	if c.MetaBits.Is(Troll) {
		return true, nil
	}

	accountId, err := c.getAccountId()
	if err != nil {
		return false, err
	}

	account, err := ResetAccountCache(accountId)
	if err != nil {
		return false, err
	}

	if account == nil {
		return false, fmt.Errorf("account is nil, accountId:%d", c.AccountId)
	}

	if account.IsTroll {
		return true, nil
	}

	return false, nil
}

func (c *ChannelMessage) getAccountId() (int64, error) {
	if c.AccountId != 0 {
		return c.AccountId, nil
	}

	if c.Id == 0 {
		return 0, fmt.Errorf("couldnt find accountId from content %+v", c)
	}

	cm := NewChannelMessage()
	if err := cm.ById(c.Id); err != nil {
		return 0, err
	}

	return cm.AccountId, nil
}

func bodyLenCheck(body string) error {
	if len(body) < config.MustGet().Limits.MessageBodyMinLen {
		return fmt.Errorf("message body length should be greater than %d, yours is %d ", config.MustGet().Limits.MessageBodyMinLen, len(body))
	}

	return nil
}

// todo create a new message while updating the channel_message and delete other
// cases, since deletion is a soft delete, old instances will still be there

// CreateRaw creates a new channel message without effected by auto generated createdAt
// and updatedAt values
func (c *ChannelMessage) CreateRaw() error {
	insertSql := "INSERT INTO " +
		c.TableName() +
		` ("body","slug","type_constant","account_id","initial_channel_id",` +
		`"created_at","updated_at","deleted_at","payload") ` +
		"VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9) " +
		"RETURNING ID"

	return bongo.B.DB.CommonDB().QueryRow(insertSql, c.Body, c.Slug, c.TypeConstant, c.AccountId, c.InitialChannelId,
		c.CreatedAt, c.UpdatedAt, c.DeletedAt, c.Payload).Scan(&c.Id)
}

// UpdateBodyRaw updates message body without effecting createdAt/UpdatedAt
// timestamps
func (c *ChannelMessage) UpdateBodyRaw() error {
	updateSql := fmt.Sprintf("UPDATE %s SET body=? WHERE id=?", c.TableName())

	return bongo.B.DB.Exec(updateSql, c.Body, c.Id).Error
}

func (c *ChannelMessage) BuildMessages(query *request.Query, messages []ChannelMessage) ([]*ChannelMessageContainer, error) {
	containers := make([]*ChannelMessageContainer, len(messages))
	if len(containers) == 0 {
		return containers, nil
	}

	for i, message := range messages {
		d := NewChannelMessage()
		*d = message
		data, err := d.BuildMessage(query)
		if err != nil {
			return containers, err
		}
		containers[i] = data
	}

	return containers, nil
}

func (c *ChannelMessage) BuildMessage(query *request.Query) (*ChannelMessageContainer, error) {
	cmc := NewChannelMessageContainer()
	return cmc.PopulateWith(c).AddIsFollowed(query), nil
}

func (c *ChannelMessage) CheckIsMessageFollowed(query *request.Query) (bool, error) {
	channel := NewChannel()
	if err := channel.FetchPinnedActivityChannel(query.AccountId, query.GroupName); err != nil {
		if err == bongo.RecordNotFound {
			return false, nil
		}
		return false, err
	}

	cml := NewChannelMessageList()
	q := &bongo.Query{
		Selector: map[string]interface{}{
			"channel_id": channel.Id,
			"message_id": c.Id,
		},
	}
	if err := cml.One(q); err != nil {
		if err == bongo.RecordNotFound {
			return false, nil
		}

		return false, err
	}

	return true, nil
}

func (c *ChannelMessage) BuildEmptyMessageContainer() (*ChannelMessageContainer, error) {
	if c.Id == 0 {
		return nil, errors.New("Channel message id is not set")
	}
	container := NewChannelMessageContainer()
	container.Message = c

	oldId, err := FetchAccountOldIdByIdFromCache(c.AccountId)
	if err != nil {
		return nil, err
	}

	container.AccountOldId = oldId

	interactionContainer := NewInteractionContainer()
	interactionContainer.ActorsPreview = make([]string, 0)
	interactionContainer.IsInteracted = false
	interactionContainer.ActorsCount = 0

	container.Interactions = make(map[string]*InteractionContainer)
	container.Interactions["like"] = interactionContainer

	return container, nil
}

func (c *ChannelMessage) FetchRelatives(query *request.Query) (*ChannelMessageContainer, error) {
	container, err := c.BuildEmptyMessageContainer()
	if err != nil {
		return nil, err
	}

	i := NewInteraction()
	i.MessageId = c.Id
	// get preview
	query.Type = "like"
	query.Limit = 3

	interactionContainer, err := i.FetchInteractionContainer(query)
	if err != nil {
		return nil, err
	}

	container.Interactions[query.Type] = interactionContainer

	return container, nil
}

func generateMessageListQuery(channelId int64, q *request.Query) *bongo.Query {
	messageType := q.Type
	if messageType == "" {
		messageType = ChannelMessage_TYPE_POST
	}

	return &bongo.Query{
		Selector: map[string]interface{}{
			"account_id":         q.AccountId,
			"initial_channel_id": channelId,
			"type_constant":      messageType,
		},
		Pagination: *bongo.NewPagination(q.Limit, q.Skip),
		Sort: map[string]string{
			"created_at": "DESC",
		},
	}
}

func (c *ChannelMessage) FetchMessagesByChannelId(channelId int64, q *request.Query) ([]ChannelMessage, error) {
	query := generateMessageListQuery(channelId, q)

	var messages []ChannelMessage
	if err := c.Some(&messages, query); err != nil {
		return nil, err
	}

	if messages == nil {
		return make([]ChannelMessage, 0), nil
	}
	return messages, nil
}

func (c *ChannelMessage) GetMentionedUsernames() []string {
	flattened := make([]string, 0)

	res := mentionRegex.FindAllStringSubmatch(c.Body, -1)
	if len(res) == 0 {
		return flattened
	}

	participants := map[string]struct{}{}
	// remove duplicate mentions
	for _, ele := range res {
		participants[ele[1]] = struct{}{}
	}

	for participant := range participants {
		flattened = append(flattened, participant)
	}

	return flattened
}

func (c *ChannelMessage) FetchTotalMessageCount(q *request.Query) (int, error) {
	query := &bongo.Query{
		Selector: map[string]interface{}{
			"account_id":    q.AccountId,
			"type_constant": q.Type,
		},
		Pagination: *bongo.NewPagination(q.Limit, q.Skip),
	}

	query.AddScope(RemoveTrollContent(
		c, q.ShowExempt,
	))

	return c.CountWithQuery(query)
}

func (c *ChannelMessage) FetchMessageIds(q *request.Query) ([]int64, error) {
	query := &bongo.Query{
		Selector: map[string]interface{}{
			"account_id":    q.AccountId,
			"type_constant": q.Type,
		},
		Pluck:      "id",
		Pagination: *bongo.NewPagination(q.Limit, q.Skip),
		Sort: map[string]string{
			"created_at": "DESC",
		},
	}

	query.AddScope(RemoveTrollContent(c, q.ShowExempt))

	var messageIds []int64
	if err := c.Some(&messageIds, query); err != nil {
		return nil, err
	}

	if messageIds == nil {
		return make([]int64, 0), nil
	}

	return messageIds, nil
}

// BySlug fetchs channel message by its slug
// checks if message is in the channel or not
func (c *ChannelMessage) BySlug(query *request.Query) error {
	if query.Slug == "" {
		return errors.New("slug is not set")
	}

	// fetch message itself
	q := &bongo.Query{
		Selector: map[string]interface{}{
			"slug": query.Slug,
		},
	}

	q.AddScope(RemoveTrollContent(
		c, query.ShowExempt,
	))

	if err := c.One(q); err != nil {
		return err
	}

	// fetch channel by group name
	query.Name = query.GroupName
	query.Type = Channel_TYPE_GROUP
	ch := NewChannel()
	channel, err := ch.ByName(query)
	if err != nil {
		return err
	}

	if channel.Id == 0 {
		return errors.New("channel is not found")
	}

	// check if message is in the channel
	cml := NewChannelMessageList()
	res, err := cml.IsInChannel(c.Id, channel.Id)
	if err != nil {
		return err
	}

	// if message is not in the channel
	if !res {
		return bongo.RecordNotFound
	}

	return nil
}
<|endoftext|>


<commit_before>package models

import (
	"errors"
	"time"

	"github.com/koding/bongo"
)

type ChannelMessage struct {
	// unique identifier of the channel message
	Id int64 `json:"id"`

	// Body of the mesage
	Body string `json:"body"`

	// Generated Slug for body
	Slug string `json:"slug"                       sql:"NOT NULL;TYPE:VARCHAR(100);"`

	// type of the message
	TypeConstant string `json:"typeConstant"        sql:"NOT NULL"`

	// Creator of the channel message
	AccountId int64 `json:"accountId"               sql:"NOT NULL"`

	// in which channel this message is created
	InitialChannelId int64 `json:"initialChannelId" sql:"NOT NULL"`

	// Creation date of the message
	CreatedAt time.Time `json:"createdAt"           sql:"DEFAULT:CURRENT_TIMESTAMP"`

	// Modification date of the message
	UpdatedAt time.Time `json:"updatedAt"           sql:"DEFAULT:CURRENT_TIMESTAMP"`
}

func (c *ChannelMessage) AfterCreate() {
	bongo.B.AfterCreate(c)
}

func (c *ChannelMessage) AfterUpdate() {
	bongo.B.AfterUpdate(c)
}

func (c *ChannelMessage) AfterDelete() {
	bongo.B.AfterDelete(c)
}

func (c *ChannelMessage) GetId() int64 {
	return c.Id
}

func (c *ChannelMessage) TableName() string {
	return "channel_message"
}

const (
	ChannelMessage_TYPE_POST  = "post"
	ChannelMessage_TYPE_REPLY = "reply"
	ChannelMessage_TYPE_JOIN  = "join"
	ChannelMessage_TYPE_LEAVE = "leave"
	ChannelMessage_TYPE_CHAT  = "chat"
)

func NewChannelMessage() *ChannelMessage {
	return &ChannelMessage{}
}

func (c *ChannelMessage) Fetch() error {
	return bongo.B.Fetch(c)
}

func (c *ChannelMessage) Update() error {
	// only update body
	err := bongo.B.UpdatePartial(c,
		map[string]interface{}{
			"body": c.Body,
		},
	)
	return err
}

func (c *ChannelMessage) Create() error {
	var err error
	c, err = Slugify(c)
	if err != nil {
		return err
	}

	return bongo.B.Create(c)
}

func (c *ChannelMessage) Delete() error {
	return bongo.B.Delete(c)
}

func (c *ChannelMessage) FetchByIds(ids []int64) ([]ChannelMessage, error) {
	var messages []ChannelMessage

	if len(ids) == 0 {
		return messages, nil
	}

	if err := bongo.B.FetchByIds(c, &messages, ids); err != nil {
		return nil, err
	}
	return messages, nil
}

func (c *ChannelMessage) FetchRelatives() (*ChannelMessageContainer, error) {
	if c.Id == 0 {
		return nil, errors.New("Channel message id is not set")
	}
	container := NewChannelMessageContainer()
	container.Message = c

	i := NewInteraction()
	i.MessageId = c.Id

	interactions, err := i.List("like")
	if err != nil {
		return nil, err
	}

	interactionContainer := NewInteractionContainer()
	interactionContainer.Actors = interactions
	// check this from database
	interactionContainer.IsInteracted = true

	if container.Interactions == nil {
		container.Interactions = make(map[string]*InteractionContainer)
	}
	if _, ok := container.Interactions["like"]; !ok {
		container.Interactions["like"] = NewInteractionContainer()
	}
	container.Interactions["like"] = interactionContainer
	return container, nil
}
<commit_msg>Social: add sql tag<commit_after>package models

import (
	"errors"
	"time"

	"github.com/koding/bongo"
)

type ChannelMessage struct {
	// unique identifier of the channel message
	Id int64 `json:"id"`

	// Body of the mesage
	Body string `json:"body"`

	// Generated Slug for body
	Slug string `json:"slug"                        sql:"NOT NULL;TYPE:VARCHAR(100);"`

	// type of the message
	TypeConstant string `json:"typeConstant"        sql:"NOT NULL;TYPE:VARCHAR(100);"`

	// Creator of the channel message
	AccountId int64 `json:"accountId"               sql:"NOT NULL"`

	// in which channel this message is created
	InitialChannelId int64 `json:"initialChannelId" sql:"NOT NULL"`

	// Creation date of the message
	CreatedAt time.Time `json:"createdAt"           sql:"DEFAULT:CURRENT_TIMESTAMP"`

	// Modification date of the message
	UpdatedAt time.Time `json:"updatedAt"           sql:"DEFAULT:CURRENT_TIMESTAMP"`
}

func (c *ChannelMessage) AfterCreate() {
	bongo.B.AfterCreate(c)
}

func (c *ChannelMessage) AfterUpdate() {
	bongo.B.AfterUpdate(c)
}

func (c *ChannelMessage) AfterDelete() {
	bongo.B.AfterDelete(c)
}

func (c *ChannelMessage) GetId() int64 {
	return c.Id
}

func (c *ChannelMessage) TableName() string {
	return "channel_message"
}

const (
	ChannelMessage_TYPE_POST  = "post"
	ChannelMessage_TYPE_REPLY = "reply"
	ChannelMessage_TYPE_JOIN  = "join"
	ChannelMessage_TYPE_LEAVE = "leave"
	ChannelMessage_TYPE_CHAT  = "chat"
)

func NewChannelMessage() *ChannelMessage {
	return &ChannelMessage{}
}

func (c *ChannelMessage) Fetch() error {
	return bongo.B.Fetch(c)
}

func (c *ChannelMessage) Update() error {
	// only update body
	err := bongo.B.UpdatePartial(c,
		map[string]interface{}{
			"body": c.Body,
		},
	)
	return err
}

func (c *ChannelMessage) Create() error {
	var err error
	c, err = Slugify(c)
	if err != nil {
		return err
	}

	return bongo.B.Create(c)
}

func (c *ChannelMessage) Delete() error {
	return bongo.B.Delete(c)
}

func (c *ChannelMessage) FetchByIds(ids []int64) ([]ChannelMessage, error) {
	var messages []ChannelMessage

	if len(ids) == 0 {
		return messages, nil
	}

	if err := bongo.B.FetchByIds(c, &messages, ids); err != nil {
		return nil, err
	}
	return messages, nil
}

func (c *ChannelMessage) FetchRelatives() (*ChannelMessageContainer, error) {
	if c.Id == 0 {
		return nil, errors.New("Channel message id is not set")
	}
	container := NewChannelMessageContainer()
	container.Message = c

	i := NewInteraction()
	i.MessageId = c.Id

	interactions, err := i.List("like")
	if err != nil {
		return nil, err
	}

	interactionContainer := NewInteractionContainer()
	interactionContainer.Actors = interactions
	// check this from database
	interactionContainer.IsInteracted = true

	if container.Interactions == nil {
		container.Interactions = make(map[string]*InteractionContainer)
	}
	if _, ok := container.Interactions["like"]; !ok {
		container.Interactions["like"] = NewInteractionContainer()
	}
	container.Interactions["like"] = interactionContainer
	return container, nil
}
<|endoftext|>


<commit_before>// Package collect throughput and response times using Go-Kit Metrics
package collect

import (
	"fmt"
	. "github.com/adrianco/goguesstimate/guesstimate"
	"github.com/adrianco/spigo/tooling/archaius"
	"github.com/adrianco/spigo/tooling/names"
	//"github.com/go-kit/kit/metrics"
	//"github.com/go-kit/kit/metrics/expvar"
	"github.com/go-kit/kit/metrics/generic"
	"log"
	"net"
	"net/http"
	"os"
	"sync"
	"time"
	"io/ioutil"
	"strings"
	"encoding/json"
)

const (
	maxHistObservable = 1000000 // one millisecond
	sampleCount       = 1000    // data points will be sampled 5000 times to build a distribution by guesstimate
)

type ArchObject struct {
	Arch string `json:"arch"`
	Version string `json:"version"`
	Args string `json:"args"`
	Services []struct {
		Name string `json:"name"`
		Package string `json:"package"`
		Regions int `json:"regions"`
		Count int `json:"count"`
		Dependencies []string `json:"dependencies"`
		UseCustomGuesstimate bool `json:"useCustomGuesstimate,omitempty"`
		GuesstimateType string `json:"guesstimateType,omitempty"`
		GuesstimateValue string `json:"guesstimateValue,omitempty"`
	} `json:"services"`
}
//save a sample of the actual data for use by guesstimate
var sampleMap map[*generic.Histogram][]int64
var sampleLock sync.Mutex

// NewHist creates a new histogram
func NewHist(name string) *generic.Histogram {
	var h *generic.Histogram
	if name != "" && archaius.Conf.Collect {
		h = generic.NewHistogram(name, 100) // 1000, maxHistObservable, 1, []int{50, 99}...)
		sampleLock.Lock()
		if sampleMap == nil {
			sampleMap = make(map[*generic.Histogram][]int64)
		}
		sampleMap[h] = make([]int64, 0, sampleCount)
		sampleLock.Unlock()
		return h
	}
	return nil
}

// Measure adds a measurement to a histogram collection
func Measure(h *generic.Histogram, d time.Duration) {
	if h != nil && archaius.Conf.Collect {
		if d > maxHistObservable {
			h.Observe(float64(maxHistObservable))
		} else {
			h.Observe(float64(d))
		}
		sampleLock.Lock()
		s := sampleMap[h]
		if s != nil && len(s) < sampleCount {
			sampleMap[h] = append(s, int64(d))
			sampleLock.Unlock()
		}
	}
}

// SaveHist passes in name because metrics.Histogram blocks expvar.Histogram.Name()
func SaveHist(h *generic.Histogram, name, suffix string) {
	if archaius.Conf.Collect {
		file, err := os.Create("csv_metrics/" + names.Arch(name) + "_" + names.Instance(name) + suffix + ".csv")
		if err != nil {
			log.Fatal("Save histogram %v: %v\n", name, err)
		}
		//metrics.PrintDistribution(file, h)
		h.Print(file)
		file.Close()
	}
}

// SaveAllGuesses writes guesses to a file
func SaveAllGuesses(name string) {
	if len(sampleMap) == 0 {
		return
	}
	log.Printf("Saving %v histograms for Guesstimate\n", len(sampleMap))
	var g Guess
	g = Guess{
		Space: GuessModel{
			Name:        names.Arch(name),
			Description: "Guesstimate generated by github.com/adrianco/spigo",
			IsPrivate:   "true",
			Graph: GuessGraph{
				Metrics:      make([]GuessMetric, 0, len(sampleMap)),
				Guesstimates: make([]Guesstimate, 0, len(sampleMap)),
			},
		},
	}

	var archObject ArchObject

	file, e := ioutil.ReadFile("./json_arch/" + names.Arch(name) + "_arch.json")
	if e != nil {
		log.Printf("File error: %v\n", e)
		os.Exit(1)
	}

	if file != nil {
		json.Unmarshal(file, &archObject)
	}
	file.Close()

	row := 1
	col := 1
	seq := []string{"", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"}
	for h, data := range sampleMap {

		UseCustomGuesstimate := false
		GuesstimateType := "DATA"
		GuesstimateValue := ""

		if(archObject.Arch != ""){
			for _, e := range archObject.Services {
				if(strings.Contains(h.Name, e.Name)){
					UseCustomGuesstimate = e.UseCustomGuesstimate
					GuesstimateType = e.GuesstimateType
					GuesstimateValue = e.GuesstimateValue
				}
			}
		}

		if UseCustomGuesstimate {
			data = nil
		}

		g.Space.Graph.Metrics = append(g.Space.Graph.Metrics, GuessMetric{
			ID:         seq[row] + seq[col],
			ReadableID: seq[row] + seq[col],
			Name:       h.Name,
			Location:   GuessMetricLocation{row, col},
		})

		g.Space.Graph.Guesstimates = append(g.Space.Graph.Guesstimates, Guesstimate{
			Metric:          seq[row] + seq[col],
			Expression:           GuesstimateValue,
			GuesstimateType: GuesstimateType,
			Data:            data,
		})
		row++
		if row >= len(seq) {
			row = 1
			col++
			if col >= len(seq) {
				break
			}
		}
	}
	SaveGuess(g, "json_metrics/"+names.Arch(name))
}

// Save currently does nothing
func Save() {
	//	if archaius.Conf.Collect {
	//		file, _ := os.Create("csv_metrics/" + archaius.Conf.Arch + "_metrics.csv")
	//		counters, gauges := metrics.Snapshot()
	//		cj, _ := json.Marshal(counters)
	//		gj, _ := json.Marshal(gauges)
	//		file.WriteString(fmt.Sprintf("{\n\"counters\":%v\n\"gauges\":%v\n}\n", string(cj), string(gj)))
	//		file.Close()
	//	}
}

// Serve on a port
func Serve(port int) {
	sock, err := net.Listen("tcp", fmt.Sprintf("localhost:%v", port))
	if err != nil {
		log.Fatal(err)
	}
	go func() {
		log.Printf("HTTP metrics now available at localhost:%v/debug/vars", port)
		http.Serve(sock, nil)
	}()
}
<commit_msg>Remove unneccesary Close().<commit_after>// Package collect throughput and response times using Go-Kit Metrics
package collect

import (
	"fmt"
	. "github.com/adrianco/goguesstimate/guesstimate"
	"github.com/adrianco/spigo/tooling/archaius"
	"github.com/adrianco/spigo/tooling/names"
	//"github.com/go-kit/kit/metrics"
	//"github.com/go-kit/kit/metrics/expvar"
	"github.com/go-kit/kit/metrics/generic"
	"log"
	"net"
	"net/http"
	"os"
	"sync"
	"time"
	"io/ioutil"
	"strings"
	"encoding/json"
)

const (
	maxHistObservable = 1000000 // one millisecond
	sampleCount       = 1000    // data points will be sampled 5000 times to build a distribution by guesstimate
)

type ArchObject struct {
	Arch string `json:"arch"`
	Version string `json:"version"`
	Args string `json:"args"`
	Services []struct {
		Name string `json:"name"`
		Package string `json:"package"`
		Regions int `json:"regions"`
		Count int `json:"count"`
		Dependencies []string `json:"dependencies"`
		UseCustomGuesstimate bool `json:"useCustomGuesstimate,omitempty"`
		GuesstimateType string `json:"guesstimateType,omitempty"`
		GuesstimateValue string `json:"guesstimateValue,omitempty"`
	} `json:"services"`
}
//save a sample of the actual data for use by guesstimate
var sampleMap map[*generic.Histogram][]int64
var sampleLock sync.Mutex

// NewHist creates a new histogram
func NewHist(name string) *generic.Histogram {
	var h *generic.Histogram
	if name != "" && archaius.Conf.Collect {
		h = generic.NewHistogram(name, 100) // 1000, maxHistObservable, 1, []int{50, 99}...)
		sampleLock.Lock()
		if sampleMap == nil {
			sampleMap = make(map[*generic.Histogram][]int64)
		}
		sampleMap[h] = make([]int64, 0, sampleCount)
		sampleLock.Unlock()
		return h
	}
	return nil
}

// Measure adds a measurement to a histogram collection
func Measure(h *generic.Histogram, d time.Duration) {
	if h != nil && archaius.Conf.Collect {
		if d > maxHistObservable {
			h.Observe(float64(maxHistObservable))
		} else {
			h.Observe(float64(d))
		}
		sampleLock.Lock()
		s := sampleMap[h]
		if s != nil && len(s) < sampleCount {
			sampleMap[h] = append(s, int64(d))
			sampleLock.Unlock()
		}
	}
}

// SaveHist passes in name because metrics.Histogram blocks expvar.Histogram.Name()
func SaveHist(h *generic.Histogram, name, suffix string) {
	if archaius.Conf.Collect {
		file, err := os.Create("csv_metrics/" + names.Arch(name) + "_" + names.Instance(name) + suffix + ".csv")
		if err != nil {
			log.Fatal("Save histogram %v: %v\n", name, err)
		}
		//metrics.PrintDistribution(file, h)
		h.Print(file)
		file.Close()
	}
}

// SaveAllGuesses writes guesses to a file
func SaveAllGuesses(name string) {
	if len(sampleMap) == 0 {
		return
	}
	log.Printf("Saving %v histograms for Guesstimate\n", len(sampleMap))
	var g Guess
	g = Guess{
		Space: GuessModel{
			Name:        names.Arch(name),
			Description: "Guesstimate generated by github.com/adrianco/spigo",
			IsPrivate:   "true",
			Graph: GuessGraph{
				Metrics:      make([]GuessMetric, 0, len(sampleMap)),
				Guesstimates: make([]Guesstimate, 0, len(sampleMap)),
			},
		},
	}

	var archObject ArchObject

	file, e := ioutil.ReadFile("./json_arch/" + names.Arch(name) + "_arch.json")
	if e != nil {
		log.Printf("File error: %v\n", e)
		os.Exit(1)
	}

	if file != nil {
		json.Unmarshal(file, &archObject)
	}

	row := 1
	col := 1
	seq := []string{"", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"}
	for h, data := range sampleMap {

		UseCustomGuesstimate := false
		GuesstimateType := "DATA"
		GuesstimateValue := ""

		if(archObject.Arch != ""){
			for _, e := range archObject.Services {
				if(strings.Contains(h.Name, e.Name)){
					UseCustomGuesstimate = e.UseCustomGuesstimate
					GuesstimateType = e.GuesstimateType
					GuesstimateValue = e.GuesstimateValue
				}
			}
		}

		if UseCustomGuesstimate {
			data = nil
		}

		g.Space.Graph.Metrics = append(g.Space.Graph.Metrics, GuessMetric{
			ID:         seq[row] + seq[col],
			ReadableID: seq[row] + seq[col],
			Name:       h.Name,
			Location:   GuessMetricLocation{row, col},
		})

		g.Space.Graph.Guesstimates = append(g.Space.Graph.Guesstimates, Guesstimate{
			Metric:          seq[row] + seq[col],
			Expression:           GuesstimateValue,
			GuesstimateType: GuesstimateType,
			Data:            data,
		})
		row++
		if row >= len(seq) {
			row = 1
			col++
			if col >= len(seq) {
				break
			}
		}
	}
	SaveGuess(g, "json_metrics/"+names.Arch(name))
}

// Save currently does nothing
func Save() {
	//	if archaius.Conf.Collect {
	//		file, _ := os.Create("csv_metrics/" + archaius.Conf.Arch + "_metrics.csv")
	//		counters, gauges := metrics.Snapshot()
	//		cj, _ := json.Marshal(counters)
	//		gj, _ := json.Marshal(gauges)
	//		file.WriteString(fmt.Sprintf("{\n\"counters\":%v\n\"gauges\":%v\n}\n", string(cj), string(gj)))
	//		file.Close()
	//	}
}

// Serve on a port
func Serve(port int) {
	sock, err := net.Listen("tcp", fmt.Sprintf("localhost:%v", port))
	if err != nil {
		log.Fatal(err)
	}
	go func() {
		log.Printf("HTTP metrics now available at localhost:%v/debug/vars", port)
		http.Serve(sock, nil)
	}()
}
<|endoftext|>


<commit_before>package prompt

import (
	// Stdlib
	"bufio"
	"bytes"
	"errors"
	"fmt"
	"io"
	"os"
	"strconv"
	"strings"
	"text/tabwriter"

	// Internal
	"github.com/salsaflow/salsaflow/errs"
	"github.com/salsaflow/salsaflow/modules/common"
)

// maxStoryTitleColumnWidth specifies the width of the story title column for story listing.
// The story title is truncated to this width in case it is too long.
const maxStoryTitleColumnWidth = 80

type InvalidInputError struct {
	input string
}

func (i *InvalidInputError) Error() string {
	return "Invalid input: " + i.input
}

type OutOfBoundsError struct {
	input string
}

func (i *OutOfBoundsError) Error() string {
	return "Index out of bounds: " + i.input
}

func Confirm(question string) (bool, error) {
	printQuestion := func() {
		fmt.Print(question)
		fmt.Print(" [y/N]: ")
	}
	printQuestion()

	var line string
	scanner := bufio.NewScanner(os.Stdin)
	for scanner.Scan() {
		line = strings.ToLower(scanner.Text())
		switch line {
		case "":
			line = "n"
		case "y":
		case "n":
		default:
			printQuestion()
			continue
		}
		break
	}
	if err := scanner.Err(); err != nil {
		return false, err
	}

	return line == "y", nil
}

func Prompt(msg string) (string, error) {
	fmt.Print(msg)
	scanner := bufio.NewScanner(os.Stdin)
	scanner.Scan()
	if err := scanner.Err(); err != nil {
		return "", err
	}
	return scanner.Text(), nil
}

func PromptIndex(msg string, min, max int) (int, error) {
	line, err := Prompt(msg)
	if err != nil {
		return 0, err
	}
	if line == "" {
		return 0, ErrCanceled
	}

	index, err := strconv.Atoi(line)
	if err != nil {
		return 0, &InvalidInputError{line}
	}

	if index < min || index > max {
		return 0, &OutOfBoundsError{line}
	}

	return index, nil
}

func PromptStory(msg string, stories []common.Story) (common.Story, error) {
	var task = "Prompt the user to select a story"

	// Make sure there are actually some stories to be printed.
	if len(stories) == 0 {
		return nil, errs.NewError(task, errors.New("no stories to choose from"),
			bytes.NewBufferString(`
There are no stories that the unassigned commits can be assigned to.
In other words, there are no stories in the right state for that.

`))
	}

	// Print the intro message.
	fmt.Println(msg)
	fmt.Println()

	// Present the stories to the user.
	if err := ListStories(stories, os.Stdout); err != nil {
		return nil, err
	}
	fmt.Println()

	// Prompt the user to select a story to assign the commit with.
	index, err := PromptIndex(
		"Choose a story by inserting its index. Just press Enter to abort: ", 0, len(stories)-1)
	if err != nil {
		if err == ErrCanceled {
			return nil, ErrCanceled
		}
		return nil, errs.NewError(task, err, nil)
	}
	return stories[index], nil
}

type writeError struct {
	err error
}

func ListStories(stories []common.Story, w io.Writer) (err error) {
	must := func(n int, err error) {
		if err != nil {
			panic(&writeError{err})
		}
	}

	defer func() {
		if r := recover(); r != nil {
			if we, ok := r.(*writeError); ok {
				err = we.err
			} else {
				panic(r)
			}
		}
	}()

	tw := tabwriter.NewWriter(w, 0, 8, 4, '\t', 0)
	must(io.WriteString(tw, "  Index\tStory ID\tStory Title\n"))
	must(io.WriteString(tw, "  =====\t========\t===========\n"))
	for i, story := range stories {
		must(fmt.Fprintf(
			tw, "  %v\t%v\t%v\n", i, story.ReadableId(), formatStoryTitle(story.Title())))
	}
	must(0, tw.Flush())

	return nil
}

func formatStoryTitle(title string) string {
	if len(title) < maxStoryTitleColumnWidth {
		return title
	}

	// maxStoryTitleColumnWidth incorporates the trailing " ...",
	// so that is why we subtract len(" ...") when truncating.
	truncatedTitle := title[:maxStoryTitleColumnWidth-4]
	if title[maxStoryTitleColumnWidth-4] != ' ' {
		if i := strings.LastIndex(truncatedTitle, " "); i != -1 {
			truncatedTitle = truncatedTitle[:i]
		}
	}
	return truncatedTitle + " ..."
}
<commit_msg>prompt: Fix review issues<commit_after>package prompt

import (
	// Stdlib
	"bufio"
	"bytes"
	"errors"
	"fmt"
	"io"
	"os"
	"strconv"
	"strings"
	"text/tabwriter"

	// Internal
	"github.com/salsaflow/salsaflow/errs"
	"github.com/salsaflow/salsaflow/modules/common"
)

// maxStoryTitleColumnWidth specifies the width of the story title column for story listing.
// The story title is truncated to this width in case it is too long.
const maxStoryTitleColumnWidth = 80

type InvalidInputError struct {
	input string
}

func (i *InvalidInputError) Error() string {
	return "Invalid input: " + i.input
}

type OutOfBoundsError struct {
	input string
}

func (i *OutOfBoundsError) Error() string {
	return "Index out of bounds: " + i.input
}

func Confirm(question string) (bool, error) {
	printQuestion := func() {
		fmt.Print(question)
		fmt.Print(" [y/N]: ")
	}
	printQuestion()

	var line string
	scanner := bufio.NewScanner(os.Stdin)
	for scanner.Scan() {
		line = strings.ToLower(scanner.Text())
		switch line {
		case "":
			line = "n"
		case "y":
		case "n":
		default:
			printQuestion()
			continue
		}
		break
	}
	if err := scanner.Err(); err != nil {
		return false, err
	}

	return line == "y", nil
}

func Prompt(msg string) (string, error) {
	fmt.Print(msg)
	scanner := bufio.NewScanner(os.Stdin)
	scanner.Scan()
	if err := scanner.Err(); err != nil {
		return "", err
	}
	return scanner.Text(), nil
}

func PromptIndex(msg string, min, max int) (int, error) {
	line, err := Prompt(msg)
	if err != nil {
		return 0, err
	}
	if line == "" {
		return 0, ErrCanceled
	}

	index, err := strconv.Atoi(line)
	if err != nil {
		return 0, &InvalidInputError{line}
	}

	if index < min || index > max {
		return 0, &OutOfBoundsError{line}
	}

	return index, nil
}

func PromptStory(msg string, stories []common.Story) (common.Story, error) {
	var task = "Prompt the user to select a story"

	// Make sure there are actually some stories to be printed.
	if len(stories) == 0 {
		return nil, errs.NewError(task, errors.New("no stories to choose from"),
			bytes.NewBufferString(`
There are no stories that the unassigned commits can be assigned to.
In other words, there are no stories in the right state for that.

`))
	}

	// Print the intro message.
	fmt.Println(msg)
	fmt.Println()

	// Present the stories to the user.
	if err := ListStories(stories, os.Stdout); err != nil {
		return nil, err
	}
	fmt.Println()

	// Prompt the user to select a story to assign the commit with.
	index, err := PromptIndex(
		"Choose a story by inserting its index. Or just press Enter to abort: ", 0, len(stories)-1)
	if err != nil {
		if err == ErrCanceled {
			return nil, ErrCanceled
		}
		return nil, errs.NewError(task, err, nil)
	}
	return stories[index], nil
}

type writeError struct {
	err error
}

func ListStories(stories []common.Story, w io.Writer) (err error) {
	must := func(n int, err error) {
		if err != nil {
			panic(&writeError{err})
		}
	}

	defer func() {
		if r := recover(); r != nil {
			if we, ok := r.(*writeError); ok {
				err = we.err
			} else {
				panic(r)
			}
		}
	}()

	tw := tabwriter.NewWriter(w, 0, 8, 4, '\t', 0)
	must(io.WriteString(tw, "  Index\tStory ID\tStory Title\n"))
	must(io.WriteString(tw, "  =====\t========\t===========\n"))
	for i, story := range stories {
		must(fmt.Fprintf(
			tw, "  %v\t%v\t%v\n", i, story.ReadableId(), formatStoryTitle(story.Title())))
	}
	must(0, tw.Flush())

	return nil
}

func formatStoryTitle(title string) string {
	if len(title) < maxStoryTitleColumnWidth {
		return title
	}

	// maxStoryTitleColumnWidth incorporates the trailing " ...",
	// so that is why we subtract len(" ...") when truncating.
	truncatedTitle := title[:maxStoryTitleColumnWidth-4]
	if title[maxStoryTitleColumnWidth-4] != ' ' {
		if i := strings.LastIndex(truncatedTitle, " "); i != -1 {
			truncatedTitle = truncatedTitle[:i]
		}
	}
	return truncatedTitle + " ..."
}
<|endoftext|>


<commit_before>package prompt

import (
	// Stdlib
	"bufio"
	"errors"
	"fmt"
	"io"
	"os"
	"strconv"
	"strings"
	"text/tabwriter"

	// Internal
	"github.com/salsita/salsaflow/errs"
	"github.com/salsita/salsaflow/modules/common"

	// Other
	"gopkg.in/salsita/go-pivotaltracker.v0/v5/pivotal"
)

// maxStoryTitleColumnWidth specifies the width of the story title column for story listing.
// The story title is truncated to this width in case it is too long.
const maxStoryTitleColumnWidth = 80

type InvalidInputError struct {
	input string
}

func (i *InvalidInputError) Error() string {
	return "Invalid input: " + i.input
}

type OutOfBoundsError struct {
	input string
}

func (i *OutOfBoundsError) Error() string {
	return "Index out of bounds: " + i.input
}

func Confirm(question string) (bool, error) {
	printQuestion := func() {
		fmt.Print(question)
		fmt.Print(" [y/N]: ")
	}
	printQuestion()

	var line string
	scanner := bufio.NewScanner(os.Stdin)
	for scanner.Scan() {
		line = strings.ToLower(scanner.Text())
		switch line {
		case "":
			line = "n"
		case "y":
		case "n":
		default:
			printQuestion()
			continue
		}
		break
	}
	if err := scanner.Err(); err != nil {
		return false, err
	}

	return line == "y", nil
}

func Prompt(msg string) (string, error) {
	fmt.Print(msg)
	scanner := bufio.NewScanner(os.Stdin)
	scanner.Scan()
	if err := scanner.Err(); err != nil {
		return "", err
	}
	return scanner.Text(), nil
}

func PromptIndex(msg string, min, max int) (int, error) {
	line, err := Prompt(msg)
	if err != nil {
		return 0, err
	}
	if line == "" {
		return 0, ErrCanceled
	}

	index, err := strconv.Atoi(line)
	if err != nil {
		return 0, &InvalidInputError{line}
	}

	if index < min || index > max {
		return 0, &OutOfBoundsError{line}
	}

	return index, nil
}

func PromptStory(msg string, stories []common.Story) (common.Story, error) {
	var task = "Prompt the user to select a story"

	// Make sure there are actually some stories to be printed.
	if len(stories) == 0 {
		fmt.Println("There are no stories to choose from!")
		return nil, errs.NewError(task, errors.New("no stories to be offered"), nil)
	}

	// Print the intro message.
	fmt.Println(msg)
	fmt.Println()

	// Present the stories to the user.
	if err := ListStories(stories, os.Stdout); err != nil {
		return nil, err
	}
	fmt.Println()

	// Prompt the user to select a story to assign the commit with.
	index, err := PromptIndex(
		"Choose a story by inserting its index. Just press Enter to abort: ", 0, len(stories)-1)
	if err != nil {
		if err == ErrCanceled {
			return nil, ErrCanceled
		}
		return nil, errs.NewError(task, err, nil)
	}
	return stories[index], nil
}

func ConfirmStories(headerLine string, stories []*pivotal.Story) (bool, error) {
	printStoriesConfirmationDialog(headerLine, stories)

	var line string
	scanner := bufio.NewScanner(os.Stdin)
	for scanner.Scan() {
		line = strings.ToLower(scanner.Text())
		switch line {
		case "":
			line = "n"
		case "y":
		case "n":
		default:
			printStoriesConfirmationDialog(headerLine, stories)
			continue
		}
		break
	}
	if err := scanner.Err(); err != nil {
		return false, err
	}

	return line == "y", nil
}

type writeError struct {
	err error
}

func ListStories(stories []common.Story, w io.Writer) (err error) {
	must := func(n int, err error) {
		if err != nil {
			panic(&writeError{err})
		}
	}

	defer func() {
		if r := recover(); r != nil {
			if we, ok := r.(*writeError); ok {
				err = we.err
			} else {
				panic(r)
			}
		}
	}()

	tw := tabwriter.NewWriter(w, 0, 8, 4, '\t', 0)
	must(io.WriteString(tw, "  Index\tStory ID\tStory Title\n"))
	must(io.WriteString(tw, "  =====\t========\t===========\n"))
	for i, story := range stories {
		must(fmt.Fprintf(
			tw, "  %v\t%v\t%v\n", i, story.ReadableId(), formatStoryTitle(story.Title())))
	}
	must(0, tw.Flush())

	return nil
}

func printStoriesConfirmationDialog(headerLine string, stories []*pivotal.Story) {
	tw := tabwriter.NewWriter(os.Stdout, 0, 8, 2, '\t', 0)

	io.WriteString(tw, "\n")
	io.WriteString(tw, headerLine)
	io.WriteString(tw, "\n\n")
	io.WriteString(tw, "Story Name\tStory URL\n")
	io.WriteString(tw, "==========\t=========\n")

	for _, story := range stories {
		fmt.Fprintf(tw, "%v\t%v\n", story.Name, story.URL)
	}

	io.WriteString(tw, "\nDo you want to proceed? [y/N]:")
	tw.Flush()
}

func formatStoryTitle(title string) string {
	if len(title) < maxStoryTitleColumnWidth {
		return title
	}

	// maxStoryTitleColumnWidth incorporates the trailing " ...",
	// so that is why we subtract len(" ...") when truncating.
	truncatedTitle := title[:maxStoryTitleColumnWidth-4]
	if title[maxStoryTitleColumnWidth-4] != ' ' {
		if i := strings.LastIndex(truncatedTitle, " "); i != -1 {
			truncatedTitle = truncatedTitle[:i]
		}
	}
	return truncatedTitle + " ..."
}
<commit_msg>review post: Format error message better<commit_after>package prompt

import (
	// Stdlib
	"bufio"
	"bytes"
	"errors"
	"fmt"
	"io"
	"os"
	"strconv"
	"strings"
	"text/tabwriter"

	// Internal
	"github.com/salsita/salsaflow/errs"
	"github.com/salsita/salsaflow/modules/common"

	// Other
	"gopkg.in/salsita/go-pivotaltracker.v0/v5/pivotal"
)

// maxStoryTitleColumnWidth specifies the width of the story title column for story listing.
// The story title is truncated to this width in case it is too long.
const maxStoryTitleColumnWidth = 80

type InvalidInputError struct {
	input string
}

func (i *InvalidInputError) Error() string {
	return "Invalid input: " + i.input
}

type OutOfBoundsError struct {
	input string
}

func (i *OutOfBoundsError) Error() string {
	return "Index out of bounds: " + i.input
}

func Confirm(question string) (bool, error) {
	printQuestion := func() {
		fmt.Print(question)
		fmt.Print(" [y/N]: ")
	}
	printQuestion()

	var line string
	scanner := bufio.NewScanner(os.Stdin)
	for scanner.Scan() {
		line = strings.ToLower(scanner.Text())
		switch line {
		case "":
			line = "n"
		case "y":
		case "n":
		default:
			printQuestion()
			continue
		}
		break
	}
	if err := scanner.Err(); err != nil {
		return false, err
	}

	return line == "y", nil
}

func Prompt(msg string) (string, error) {
	fmt.Print(msg)
	scanner := bufio.NewScanner(os.Stdin)
	scanner.Scan()
	if err := scanner.Err(); err != nil {
		return "", err
	}
	return scanner.Text(), nil
}

func PromptIndex(msg string, min, max int) (int, error) {
	line, err := Prompt(msg)
	if err != nil {
		return 0, err
	}
	if line == "" {
		return 0, ErrCanceled
	}

	index, err := strconv.Atoi(line)
	if err != nil {
		return 0, &InvalidInputError{line}
	}

	if index < min || index > max {
		return 0, &OutOfBoundsError{line}
	}

	return index, nil
}

func PromptStory(msg string, stories []common.Story) (common.Story, error) {
	var task = "Prompt the user to select a story"

	// Make sure there are actually some stories to be printed.
	if len(stories) == 0 {
		return nil, errs.NewError(task, errors.New("no stories to choose from"),
			bytes.NewBufferString(`
There are no stories that the unassigned commits can be assigned to.
In other words, there are no stories in the right state for that.

`))
	}

	// Print the intro message.
	fmt.Println(msg)
	fmt.Println()

	// Present the stories to the user.
	if err := ListStories(stories, os.Stdout); err != nil {
		return nil, err
	}
	fmt.Println()

	// Prompt the user to select a story to assign the commit with.
	index, err := PromptIndex(
		"Choose a story by inserting its index. Just press Enter to abort: ", 0, len(stories)-1)
	if err != nil {
		if err == ErrCanceled {
			return nil, ErrCanceled
		}
		return nil, errs.NewError(task, err, nil)
	}
	return stories[index], nil
}

func ConfirmStories(headerLine string, stories []*pivotal.Story) (bool, error) {
	printStoriesConfirmationDialog(headerLine, stories)

	var line string
	scanner := bufio.NewScanner(os.Stdin)
	for scanner.Scan() {
		line = strings.ToLower(scanner.Text())
		switch line {
		case "":
			line = "n"
		case "y":
		case "n":
		default:
			printStoriesConfirmationDialog(headerLine, stories)
			continue
		}
		break
	}
	if err := scanner.Err(); err != nil {
		return false, err
	}

	return line == "y", nil
}

type writeError struct {
	err error
}

func ListStories(stories []common.Story, w io.Writer) (err error) {
	must := func(n int, err error) {
		if err != nil {
			panic(&writeError{err})
		}
	}

	defer func() {
		if r := recover(); r != nil {
			if we, ok := r.(*writeError); ok {
				err = we.err
			} else {
				panic(r)
			}
		}
	}()

	tw := tabwriter.NewWriter(w, 0, 8, 4, '\t', 0)
	must(io.WriteString(tw, "  Index\tStory ID\tStory Title\n"))
	must(io.WriteString(tw, "  =====\t========\t===========\n"))
	for i, story := range stories {
		must(fmt.Fprintf(
			tw, "  %v\t%v\t%v\n", i, story.ReadableId(), formatStoryTitle(story.Title())))
	}
	must(0, tw.Flush())

	return nil
}

func printStoriesConfirmationDialog(headerLine string, stories []*pivotal.Story) {
	tw := tabwriter.NewWriter(os.Stdout, 0, 8, 2, '\t', 0)

	io.WriteString(tw, "\n")
	io.WriteString(tw, headerLine)
	io.WriteString(tw, "\n\n")
	io.WriteString(tw, "Story Name\tStory URL\n")
	io.WriteString(tw, "==========\t=========\n")

	for _, story := range stories {
		fmt.Fprintf(tw, "%v\t%v\n", story.Name, story.URL)
	}

	io.WriteString(tw, "\nDo you want to proceed? [y/N]:")
	tw.Flush()
}

func formatStoryTitle(title string) string {
	if len(title) < maxStoryTitleColumnWidth {
		return title
	}

	// maxStoryTitleColumnWidth incorporates the trailing " ...",
	// so that is why we subtract len(" ...") when truncating.
	truncatedTitle := title[:maxStoryTitleColumnWidth-4]
	if title[maxStoryTitleColumnWidth-4] != ' ' {
		if i := strings.LastIndex(truncatedTitle, " "); i != -1 {
			truncatedTitle = truncatedTitle[:i]
		}
	}
	return truncatedTitle + " ..."
}
<|endoftext|>


<commit_before>// Copyright 2018 by caixw, All rights reserved.
// Use of this source code is governed by a MIT
// license that can be found in the LICENSE file.

// Package prompt 简单的终端交互界面
package prompt

import (
	"bufio"
	"io"
	"strconv"
	"strings"

	"github.com/issue9/term/colors"
)

// Prompt 终端交互对象
type Prompt struct {
	reader       *bufio.Reader
	output       io.Writer
	delim        byte
	defaultColor colors.Color
	err          error
}

// New 声明 Prompt 变量
//
// delim 从 input 读取内容时的分隔符，如果为空，则采用 \n；
// defaultColor 默认值的颜色，如果该值无效，则会 panic。
func New(delim byte, input io.Reader, output io.Writer, defaultColor colors.Color) *Prompt {
	if delim == 0 {
		delim = '\n'
	}

	if !defaultColor.IsValid() {
		panic("无效的颜色值 defaultColor")
	}

	return &Prompt{
		reader:       bufio.NewReader(input),
		output:       output,
		delim:        delim,
		defaultColor: defaultColor,
	}
}

func (p *Prompt) println(c colors.Color, v ...interface{}) {
	if p.err == nil {
		_, p.err = colors.Fprintln(p.output, c, colors.Default, v...)
	}
}

func (p *Prompt) print(c colors.Color, v ...interface{}) {
	if p.err == nil {
		_, p.err = colors.Fprint(p.output, c, colors.Default, v...)
	}
}

func (p *Prompt) printf(c colors.Color, format string, v ...interface{}) {
	if p.err == nil {
		_, p.err = colors.Fprintf(p.output, c, colors.Default, format, v...)
	}
}

// 从输入端读取一行内容
func (p *Prompt) read() (v string) {
	if p.err != nil {
		return
	}

	if v, p.err = p.reader.ReadString(p.delim); p.err == nil {
		return v[:len(v)-1]
	}
	return
}

// String 输出问题，并获取用户的回答内容
//
// q 显示的问题内容；
// def 表示默认值。
func (p *Prompt) String(q, def string) (string, error) {
	p.print(colors.Default, q)
	if def != "" {
		p.print(p.defaultColor, "（", def, "）")
	}
	p.print(colors.Default, "：")

	v := p.read()

	if p.err != nil {
		return "", p.err
	}

	if v == "" {
		v = def
	}
	return v, nil
}

// Bool 输出 bool 问题，并获取用户的回答内容
func (p *Prompt) Bool(q string, def bool) (bool, error) {
	p.print(colors.Default, q)
	str := "Y"
	if !def {
		str = "N"
	}
	p.print(p.defaultColor, "（", str, "）：")

	val := p.read()

	if p.err != nil {
		return false, p.err
	}

	switch strings.ToLower(val) {
	case "yes", "y":
		return true, nil
	case "no", "n":
		return false, nil
	default:
		return def, nil
	}
}

// Slice 输出一个选择性问题，并获取用户的选择项
//
// q 表示问题内容；
// slice 表示可选的问题列表；
// def 表示默认项的索引，必须在 slice 之内。
func (p *Prompt) Slice(q string, slice []string, def ...int) (selected []int, err error) {
	p.println(colors.Default, q)
	for i, v := range slice {
		c := colors.Default
		if inIntSlice(i, def) {
			c = p.defaultColor
		}
		p.printf(c, "(%d) %s\n", i, v)
	}
	p.print(colors.Default, "请输入你的选择项，多项请用半角逗号（,）分隔：")

	val := p.read()

	if p.err != nil {
		return nil, p.err
	}

	if val == "" {
		return def, nil
	}

	for _, v := range strings.Split(val, ",") {
		vv, err := strconv.Atoi(v)
		if err != nil {
			return nil, err
		}
		selected = append(selected, vv)
	}
	return selected, nil
}

// Map 输出一个单选问题，并获取用户的选择项
//
// q 表示问题内容；
// maps 表示可选的问题列表；
// def 表示默认项的索引，必须在 maps 之内。
func (p *Prompt) Map(q string, maps map[string]string, def ...string) (selected []string, err error) {
	p.println(colors.Default, q)
	for k, v := range maps {
		c := colors.Default
		if inStringSlice(k, def) {
			c = p.defaultColor
		}
		p.printf(c, "(%s) %s\n", k, v)
	}
	p.print(colors.Default, "请输入你的选择项，多项请用半角逗号（,）分隔：")

	val := p.read()

	if p.err != nil {
		return nil, p.err
	}

	if val == "" {
		return def, nil
	}
	return strings.Split(val, ","), nil
}

func inIntSlice(v int, vals []int) bool {
	for _, val := range vals {
		if val == v {
			return true
		}
	}

	return false
}

func inStringSlice(v string, vals []string) bool {
	for _, val := range vals {
		if val == v {
			return true
		}
	}

	return false
}
<commit_msg>[prompt] 统一输出的颜色格式<commit_after>// Copyright 2018 by caixw, All rights reserved.
// Use of this source code is governed by a MIT
// license that can be found in the LICENSE file.

// Package prompt 简单的终端交互界面
package prompt

import (
	"bufio"
	"io"
	"strconv"
	"strings"

	"github.com/issue9/term/colors"
)

// Prompt 终端交互对象
type Prompt struct {
	reader       *bufio.Reader
	output       io.Writer
	delim        byte
	defaultColor colors.Color
	err          error
}

// New 声明 Prompt 变量
//
// delim 从 input 读取内容时的分隔符，如果为空，则采用 \n；
// defaultColor 默认值的颜色，如果该值无效，则会 panic。
func New(delim byte, input io.Reader, output io.Writer, defaultColor colors.Color) *Prompt {
	if delim == 0 {
		delim = '\n'
	}

	if !defaultColor.IsValid() {
		panic("无效的颜色值 defaultColor")
	}

	return &Prompt{
		reader:       bufio.NewReader(input),
		output:       output,
		delim:        delim,
		defaultColor: defaultColor,
	}
}

func (p *Prompt) println(c colors.Color, v ...interface{}) {
	if p.err == nil {
		_, p.err = colors.Fprintln(p.output, c, colors.Default, v...)
	}
}

func (p *Prompt) print(c colors.Color, v ...interface{}) {
	if p.err == nil {
		_, p.err = colors.Fprint(p.output, c, colors.Default, v...)
	}
}

func (p *Prompt) printf(c colors.Color, format string, v ...interface{}) {
	if p.err == nil {
		_, p.err = colors.Fprintf(p.output, c, colors.Default, format, v...)
	}
}

// 从输入端读取一行内容
func (p *Prompt) read() (v string) {
	if p.err != nil {
		return
	}

	if v, p.err = p.reader.ReadString(p.delim); p.err == nil {
		return v[:len(v)-1]
	}
	return
}

// String 输出问题，并获取用户的回答内容
//
// q 显示的问题内容；
// def 表示默认值。
func (p *Prompt) String(q, def string) (string, error) {
	p.print(colors.Default, q)
	if def != "" {
		p.print(p.defaultColor, "（", def, "）")
	}
	p.print(colors.Default, "：")

	v := p.read()

	if p.err != nil {
		return "", p.err
	}

	if v == "" {
		v = def
	}
	return v, nil
}

// Bool 输出 bool 问题，并获取用户的回答内容
func (p *Prompt) Bool(q string, def bool) (bool, error) {
	p.print(colors.Default, q)
	str := "Y"
	if !def {
		str = "N"
	}
	p.print(p.defaultColor, "（", str, "）：")
	p.print(colors.Default, "：")

	val := p.read()

	if p.err != nil {
		return false, p.err
	}

	switch strings.ToLower(val) {
	case "yes", "y":
		return true, nil
	case "no", "n":
		return false, nil
	default:
		return def, nil
	}
}

// Slice 输出一个选择性问题，并获取用户的选择项
//
// q 表示问题内容；
// slice 表示可选的问题列表；
// def 表示默认项的索引，必须在 slice 之内。
func (p *Prompt) Slice(q string, slice []string, def ...int) (selected []int, err error) {
	p.println(colors.Default, q)
	for i, v := range slice {
		c := colors.Default
		if inIntSlice(i, def) {
			c = p.defaultColor
		}
		p.printf(c, "（%d）", i)
		p.printf(colors.Default, "%s\n", v)
	}
	p.print(colors.Default, "请输入你的选择项，多项请用半角逗号（,）分隔：")

	val := p.read()

	if p.err != nil {
		return nil, p.err
	}

	if val == "" {
		return def, nil
	}

	for _, v := range strings.Split(val, ",") {
		vv, err := strconv.Atoi(v)
		if err != nil {
			return nil, err
		}
		selected = append(selected, vv)
	}
	return selected, nil
}

// Map 输出一个单选问题，并获取用户的选择项
//
// q 表示问题内容；
// maps 表示可选的问题列表；
// def 表示默认项的索引，必须在 maps 之内。
func (p *Prompt) Map(q string, maps map[string]string, def ...string) (selected []string, err error) {
	p.println(colors.Default, q)
	for k, v := range maps {
		c := colors.Default
		if inStringSlice(k, def) {
			c = p.defaultColor
		}
		p.printf(c, "（%s）", k)
		p.printf(colors.Default, "%s\n", v)
	}
	p.print(colors.Default, "请输入你的选择项，多项请用半角逗号（,）分隔：")

	val := p.read()

	if p.err != nil {
		return nil, p.err
	}

	if val == "" {
		return def, nil
	}
	return strings.Split(val, ","), nil
}

func inIntSlice(v int, vals []int) bool {
	for _, val := range vals {
		if val == v {
			return true
		}
	}

	return false
}

func inStringSlice(v string, vals []string) bool {
	for _, val := range vals {
		if val == v {
			return true
		}
	}

	return false
}
<|endoftext|>