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

// Package socket implements an WebSocket-based playground backend.
// Clients connect to a websocket handler and send run/kill commands, and
// the server sends the output and exit status of the running processes.
// Multiple clients running multiple processes may be served concurrently.
// The wire format is JSON and is described by the Message type.
//
// This will not run on App Engine as WebSockets are not supported there.
package socket

import (
	"encoding/json"
	"io"
	"io/ioutil"
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"strconv"

	"code.google.com/p/go.net/websocket"
)

// Handler implements a WebSocket handler for a client connection.
var Handler = websocket.Handler(socketHandler)

const msgLimit = 1000 // max number of messages to send per session

// Message is the wire format for the websocket connection to the browser.
// It is used for both sending output messages and receiving commands, as
// distinguished by the Kind field.
type Message struct {
	Id   string // client-provided unique id for the process
	Kind string // in: "run", "kill" out: "stdout", "stderr", "end"
	Body string
}

// socketHandler handles the websocket connection for a given present session.
// It handles transcoding Messages to and from JSON format, and starting
// and killing processes.
func socketHandler(c *websocket.Conn) {
	in, out := make(chan *Message), make(chan *Message)
	errc := make(chan error, 1)

	// Decode messages from client and send to the in channel.
	go func() {
		dec := json.NewDecoder(c)
		for {
			var m Message
			if err := dec.Decode(&m); err != nil {
				errc <- err
				return
			}
			in <- &m
		}
	}()

	// Receive messages from the out channel and encode to the client.
	go func() {
		enc := json.NewEncoder(c)
		for m := range out {
			if err := enc.Encode(m); err != nil {
				errc <- err
				return
			}
		}
	}()

	// Start and kill processes and handle errors.
	proc := make(map[string]*process)
	for {
		select {
		case m := <-in:
			switch m.Kind {
			case "run":
				proc[m.Id].Kill()
				lOut := limiter(in, out)
				proc[m.Id] = startProcess(m.Id, m.Body, lOut)
			case "kill":
				proc[m.Id].Kill()
			}
		case err := <-errc:
			if err != io.EOF {
				// A encode or decode has failed; bail.
				log.Println(err)
			}
			// Shut down any running processes.
			for _, p := range proc {
				p.Kill()
			}
			return
		}
	}
}

// process represents a running process.
type process struct {
	id   string
	out  chan<- *Message
	done chan struct{} // closed when wait completes
	run  *exec.Cmd
}

// startProcess builds and runs the given program, sending its output
// and end event as Messages on the provided channel.
func startProcess(id, body string, out chan<- *Message) *process {
	p := &process{
		id:   id,
		out:  out,
		done: make(chan struct{}),
	}
	if err := p.start(body); err != nil {
		p.end(err)
		return nil
	}
	go p.wait()
	return p
}

// Kill stops the process if it is running and waits for it to exit.
func (p *process) Kill() {
	if p == nil {
		return
	}
	p.run.Process.Kill()
	<-p.done // block until process exits
}

// start builds and starts the given program, sending its output to p.out,
// and stores the running *exec.Cmd in the run field.
func (p *process) start(body string) error {
	// We "go build" and then exec the binary so that the
	// resultant *exec.Cmd is a handle to the user's program
	// (rather than the go tool process).
	// This makes Kill work.

	bin := filepath.Join(tmpdir, "compile"+strconv.Itoa(<-uniq))
	src := bin + ".go"
	if runtime.GOOS == "windows" {
		bin += ".exe"
	}

	// write body to x.go
	defer os.Remove(src)
	err := ioutil.WriteFile(src, []byte(body), 0666)
	if err != nil {
		return err
	}

	// build x.go, creating x
	defer os.Remove(bin)
	dir, file := filepath.Split(src)
	cmd := p.cmd(dir, "go", "build", "-o", bin, file)
	cmd.Stdout = cmd.Stderr // send compiler output to stderr
	if err := cmd.Run(); err != nil {
		return err
	}

	// run x
	cmd = p.cmd("", bin)
	if err := cmd.Start(); err != nil {
		return err
	}
	p.run = cmd
	return nil
}

// wait waits for the running process to complete
// and sends its error state to the client.
func (p *process) wait() {
	p.end(p.run.Wait())
	close(p.done) // unblock waiting Kill calls
}

// end sends an "end" message to the client, containing the process id and the
// given error value.
func (p *process) end(err error) {
	m := &Message{Id: p.id, Kind: "end"}
	if err != nil {
		m.Body = err.Error()
	}
	p.out <- m
}

// cmd builds an *exec.Cmd that writes its standard output and error to the
// process' output channel.
func (p *process) cmd(dir string, args ...string) *exec.Cmd {
	cmd := exec.Command(args[0], args[1:]...)
	cmd.Dir = dir
	cmd.Stdout = &messageWriter{p.id, "stdout", p.out}
	cmd.Stderr = &messageWriter{p.id, "stderr", p.out}
	return cmd
}

// messageWriter is an io.Writer that converts all writes to Message sends on
// the out channel with the specified id and kind.
type messageWriter struct {
	id, kind string
	out      chan<- *Message
}

func (w *messageWriter) Write(b []byte) (n int, err error) {
	w.out <- &Message{Id: w.id, Kind: w.kind, Body: string(b)}
	return len(b), nil
}

// limiter returns a channel that wraps dest. Messages sent to the channel are
// sent to dest. After msgLimit Messages have been passed on, a "kill" Message
// is sent to the kill channel, and only "end" messages are passed.
func limiter(kill chan<- *Message, dest chan<- *Message) chan<- *Message {
	ch := make(chan *Message)
	go func() {
		n := 0
		for m := range ch {
			switch {
			case n < msgLimit || m.Kind == "end":
				dest <- m
				if m.Kind == "end" {
					return
				}
			case n == msgLimit:
				// process produced too much output. Kill it.
				kill <- &Message{Id: m.Id, Kind: "kill"}
			}
			n++
		}
	}()
	return ch
}

var tmpdir string

func init() {
	// find real path to temporary directory
	var err error
	tmpdir, err = filepath.EvalSymlinks(os.TempDir())
	if err != nil {
		log.Fatal(err)
	}
}

var uniq = make(chan int) // a source of numbers for naming temporary files

func init() {
	go func() {
		for i := 0; ; i++ {
			uniq <- i
		}
	}()
}
<commit_msg>go.talks/pkg/socket: prevent use on AppEngine<commit_after>// Copyright 2012 The Go Authors.  All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// +build !appengine

// Package socket implements an WebSocket-based playground backend.
// Clients connect to a websocket handler and send run/kill commands, and
// the server sends the output and exit status of the running processes.
// Multiple clients running multiple processes may be served concurrently.
// The wire format is JSON and is described by the Message type.
//
// This will not run on App Engine as WebSockets are not supported there.
package socket

import (
	"encoding/json"
	"io"
	"io/ioutil"
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"strconv"

	"code.google.com/p/go.net/websocket"
)

// Handler implements a WebSocket handler for a client connection.
var Handler = websocket.Handler(socketHandler)

const msgLimit = 1000 // max number of messages to send per session

// Message is the wire format for the websocket connection to the browser.
// It is used for both sending output messages and receiving commands, as
// distinguished by the Kind field.
type Message struct {
	Id   string // client-provided unique id for the process
	Kind string // in: "run", "kill" out: "stdout", "stderr", "end"
	Body string
}

// socketHandler handles the websocket connection for a given present session.
// It handles transcoding Messages to and from JSON format, and starting
// and killing processes.
func socketHandler(c *websocket.Conn) {
	in, out := make(chan *Message), make(chan *Message)
	errc := make(chan error, 1)

	// Decode messages from client and send to the in channel.
	go func() {
		dec := json.NewDecoder(c)
		for {
			var m Message
			if err := dec.Decode(&m); err != nil {
				errc <- err
				return
			}
			in <- &m
		}
	}()

	// Receive messages from the out channel and encode to the client.
	go func() {
		enc := json.NewEncoder(c)
		for m := range out {
			if err := enc.Encode(m); err != nil {
				errc <- err
				return
			}
		}
	}()

	// Start and kill processes and handle errors.
	proc := make(map[string]*process)
	for {
		select {
		case m := <-in:
			switch m.Kind {
			case "run":
				proc[m.Id].Kill()
				lOut := limiter(in, out)
				proc[m.Id] = startProcess(m.Id, m.Body, lOut)
			case "kill":
				proc[m.Id].Kill()
			}
		case err := <-errc:
			if err != io.EOF {
				// A encode or decode has failed; bail.
				log.Println(err)
			}
			// Shut down any running processes.
			for _, p := range proc {
				p.Kill()
			}
			return
		}
	}
}

// process represents a running process.
type process struct {
	id   string
	out  chan<- *Message
	done chan struct{} // closed when wait completes
	run  *exec.Cmd
}

// startProcess builds and runs the given program, sending its output
// and end event as Messages on the provided channel.
func startProcess(id, body string, out chan<- *Message) *process {
	p := &process{
		id:   id,
		out:  out,
		done: make(chan struct{}),
	}
	if err := p.start(body); err != nil {
		p.end(err)
		return nil
	}
	go p.wait()
	return p
}

// Kill stops the process if it is running and waits for it to exit.
func (p *process) Kill() {
	if p == nil {
		return
	}
	p.run.Process.Kill()
	<-p.done // block until process exits
}

// start builds and starts the given program, sending its output to p.out,
// and stores the running *exec.Cmd in the run field.
func (p *process) start(body string) error {
	// We "go build" and then exec the binary so that the
	// resultant *exec.Cmd is a handle to the user's program
	// (rather than the go tool process).
	// This makes Kill work.

	bin := filepath.Join(tmpdir, "compile"+strconv.Itoa(<-uniq))
	src := bin + ".go"
	if runtime.GOOS == "windows" {
		bin += ".exe"
	}

	// write body to x.go
	defer os.Remove(src)
	err := ioutil.WriteFile(src, []byte(body), 0666)
	if err != nil {
		return err
	}

	// build x.go, creating x
	defer os.Remove(bin)
	dir, file := filepath.Split(src)
	cmd := p.cmd(dir, "go", "build", "-o", bin, file)
	cmd.Stdout = cmd.Stderr // send compiler output to stderr
	if err := cmd.Run(); err != nil {
		return err
	}

	// run x
	cmd = p.cmd("", bin)
	if err := cmd.Start(); err != nil {
		return err
	}
	p.run = cmd
	return nil
}

// wait waits for the running process to complete
// and sends its error state to the client.
func (p *process) wait() {
	p.end(p.run.Wait())
	close(p.done) // unblock waiting Kill calls
}

// end sends an "end" message to the client, containing the process id and the
// given error value.
func (p *process) end(err error) {
	m := &Message{Id: p.id, Kind: "end"}
	if err != nil {
		m.Body = err.Error()
	}
	p.out <- m
}

// cmd builds an *exec.Cmd that writes its standard output and error to the
// process' output channel.
func (p *process) cmd(dir string, args ...string) *exec.Cmd {
	cmd := exec.Command(args[0], args[1:]...)
	cmd.Dir = dir
	cmd.Stdout = &messageWriter{p.id, "stdout", p.out}
	cmd.Stderr = &messageWriter{p.id, "stderr", p.out}
	return cmd
}

// messageWriter is an io.Writer that converts all writes to Message sends on
// the out channel with the specified id and kind.
type messageWriter struct {
	id, kind string
	out      chan<- *Message
}

func (w *messageWriter) Write(b []byte) (n int, err error) {
	w.out <- &Message{Id: w.id, Kind: w.kind, Body: string(b)}
	return len(b), nil
}

// limiter returns a channel that wraps dest. Messages sent to the channel are
// sent to dest. After msgLimit Messages have been passed on, a "kill" Message
// is sent to the kill channel, and only "end" messages are passed.
func limiter(kill chan<- *Message, dest chan<- *Message) chan<- *Message {
	ch := make(chan *Message)
	go func() {
		n := 0
		for m := range ch {
			switch {
			case n < msgLimit || m.Kind == "end":
				dest <- m
				if m.Kind == "end" {
					return
				}
			case n == msgLimit:
				// process produced too much output. Kill it.
				kill <- &Message{Id: m.Id, Kind: "kill"}
			}
			n++
		}
	}()
	return ch
}

var tmpdir string

func init() {
	// find real path to temporary directory
	var err error
	tmpdir, err = filepath.EvalSymlinks(os.TempDir())
	if err != nil {
		log.Fatal(err)
	}
}

var uniq = make(chan int) // a source of numbers for naming temporary files

func init() {
	go func() {
		for i := 0; ; i++ {
			uniq <- i
		}
	}()
}
<|endoftext|>


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

// File I/O for logs.

// Author: Bram Gruneir (bram@cockroachlabs.com)

package log

import (
	"flag"
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"os/user"
	"path/filepath"
	"regexp"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/pkg/errors"
)

// MaxSize is the maximum size of a log file in bytes.
var MaxSize uint64 = 1024 * 1024 * 10

// If non-empty, overrides the choice of directory in which to write logs. See
// createLogDirs for the full list of possible destinations. Note that the
// default is to log to stderr independent of this setting. See --logtostderr.
var logDir = os.TempDir()
var logDirSet bool

// DirSet returns true of the log directory has been changed from its default.
func DirSet() bool {
	return logDirSet
}

type stringValue struct {
	val *string
	set *bool
}

var _ flag.Value = &stringValue{}

func newStringValue(val *string, set *bool) *stringValue {
	return &stringValue{val: val, set: set}
}

func (s *stringValue) Set(val string) error {
	*s.val = val
	*s.set = true
	return nil
}

func (s *stringValue) Type() string {
	return "string"
}

func (s *stringValue) String() string {
	if s.val == nil {
		return ""
	}
	return *s.val
}

// logFileRE matches log files to avoid exposing non-log files accidentally
// and it splits the details of the filename into groups for easy parsing.
// The log file format is {process}.{host}.{username}.log.{severity}.{timestamp}
// cockroach.Brams-MacBook-Pro.bram.log.WARNING.2015-06-09T16_10_48-04_00.30209
// All underscore in process, host and username are escaped to double
// underscores and all periods are escaped to an underscore.
// For compatibility with Windows filenames, all colons from the timestamp
// (RFC3339) are converted to underscores.
var logFileRE = regexp.MustCompile(`([^\.]+)\.([^\.]+)\.([^\.]+)\.log\.(ERROR|WARNING|INFO)\.([^\.]+)\.(\d+)`)

var (
	pid      = os.Getpid()
	program  = filepath.Base(os.Args[0])
	host     = "unknownhost"
	userName = "unknownuser"
)

func init() {
	h, err := os.Hostname()
	if err == nil {
		host = shortHostname(h)
	}

	current, err := user.Current()
	if err == nil {
		userName = current.Username
	}

	// Sanitize userName since it may contain filepath separators on Windows.
	userName = strings.Replace(userName, `\`, "_", -1)
}

// shortHostname returns its argument, truncating at the first period.
// For instance, given "www.google.com" it returns "www".
func shortHostname(hostname string) string {
	if i := strings.Index(hostname, "."); i >= 0 {
		return hostname[:i]
	}
	return hostname
}

// removePeriods removes all extraneous periods. This is required to ensure that
// the only periods in the filename are the ones added by logName so it can
// be easily parsed.
func removePeriods(s string) string {
	return strings.Replace(s, ".", "", -1)
}

// logName returns a new log file name containing the severity, with start time
// t, and the name for the symlink for the severity.
func logName(severity Severity, t time.Time) (name, link string) {
	// Replace the ':'s in the time format with '_'s to allow for log files in
	// Windows.
	tFormatted := strings.Replace(t.Format(time.RFC3339), ":", "_", -1)

	name = fmt.Sprintf("%s.%s.%s.log.%s.%s.%d",
		removePeriods(program),
		removePeriods(host),
		removePeriods(userName),
		severity.Name(),
		tFormatted,
		pid)
	return name, removePeriods(program) + "." + severity.Name()
}

var errMalformedName = errors.New("malformed log filename")
var errMalformedSev = errors.New("malformed severity")

func parseLogFilename(filename string) (FileDetails, error) {
	matches := logFileRE.FindStringSubmatch(filename)
	if matches == nil || len(matches) != 7 {
		return FileDetails{}, errMalformedName
	}

	sev, sevFound := SeverityByName(matches[4])
	if !sevFound {
		return FileDetails{}, errMalformedSev
	}

	// Replace the '_'s with ':'s to restore the correct time format.
	fixTime := strings.Replace(matches[5], "_", ":", -1)
	time, err := time.Parse(time.RFC3339, fixTime)
	if err != nil {
		return FileDetails{}, err
	}

	pid, err := strconv.ParseInt(matches[6], 10, 0)
	if err != nil {
		return FileDetails{}, err
	}

	return FileDetails{
		Program:  matches[1],
		Host:     matches[2],
		UserName: matches[3],
		Severity: sev,
		Time:     time.UnixNano(),
		PID:      pid,
	}, nil
}

var errDirectoryNotSet = errors.New("log: log directory not set")

// create creates a new log file and returns the file and its filename, which
// contains severity ("INFO", "FATAL", etc.) and t. If the file is created
// successfully, create also attempts to update the symlink for that tag, ignoring
// errors.
func create(severity Severity, t time.Time) (f *os.File, filename string, err error) {
	if len(logDir) == 0 {
		return nil, "", errDirectoryNotSet
	}
	name, link := logName(severity, t)
	fname := filepath.Join(logDir, name)

	// Open the file os.O_APPEND|os.O_CREATE rather than use os.Create.
	// Append is almost always more efficient than O_RDRW on most modern file systems.
	f, err = os.OpenFile(fname, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0664)
	if err == nil {
		symlink := filepath.Join(logDir, link)
		_ = os.Remove(symlink) // ignore err
		err = os.Symlink(fname, symlink)
	}
	return f, fname, errors.Wrapf(err, "log: cannot create log")
}

var errNotAFile = errors.New("not a regular file")

// getFileDetails verifies that the file specified by filename is a
// regular file and filename matches the expected filename pattern.
// Returns the log file details success; otherwise error.
func getFileDetails(info os.FileInfo) (FileDetails, error) {
	if info.Mode()&os.ModeType != 0 {
		return FileDetails{}, errNotAFile
	}

	details, err := parseLogFilename(info.Name())
	if err != nil {
		return FileDetails{}, err
	}

	return details, nil
}

func verifyFile(filename string) error {
	info, err := os.Stat(filename)
	if err != nil {
		return err
	}
	_, err = getFileDetails(info)
	return err
}

// ListLogFiles returns a slice of FileInfo structs for each log file
// on the local node, in any of the configured log directories.
func ListLogFiles() ([]FileInfo, error) {
	var results []FileInfo
	if logDir == "" {
		return nil, nil
	}
	infos, err := ioutil.ReadDir(logDir)
	if err != nil {
		return results, err
	}
	for _, info := range infos {
		details, err := getFileDetails(info)
		if err == nil {
			results = append(results, FileInfo{
				Name:         info.Name(),
				SizeBytes:    info.Size(),
				ModTimeNanos: info.ModTime().UnixNano(),
				Details:      details,
			})
		}
	}
	return results, nil
}

// GetLogReader returns a reader for the specified filename. In
// restricted mode, the filename must be the base name of a file in
// this process's log directory (this is safe for cases when the
// filename comes from external sources, such as the admin UI via
// HTTP). In unrestricted mode any path is allowed, with the added
// feature that relative paths will be searched in both the current
// directory and this process's log directory.
func GetLogReader(filename string, restricted bool) (io.ReadCloser, error) {
	// Verify there are no path separators in a restricted-mode pathname.
	if restricted && filepath.Base(filename) != filename {
		return nil, fmt.Errorf("pathnames must be basenames only: %s", filename)
	}
	if !filepath.IsAbs(filename) {
		filename = filepath.Join(logDir, filename)
	}
	if !restricted {
		var err error
		filename, err = filepath.EvalSymlinks(filename)
		if err != nil {
			return nil, err
		}
	}
	if err := verifyFile(filename); err != nil {
		return nil, err
	}
	return os.Open(filename)
}

// sortableFileInfoSlice is required so we can sort FileInfos.
type sortableFileInfoSlice []FileInfo

func (a sortableFileInfoSlice) Len() int      { return len(a) }
func (a sortableFileInfoSlice) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a sortableFileInfoSlice) Less(i, j int) bool {
	return a[i].Details.Time < a[j].Details.Time
}

// selectFiles selects all log files that have an timestamp before the endTime and
// the correct severity. It then sorts them in decreasing order, with the most
// recent as the first one.
func selectFiles(logFiles []FileInfo, severity Severity, endTimestamp int64) []FileInfo {
	files := sortableFileInfoSlice{}
	for _, logFile := range logFiles {
		if logFile.Details.Severity == severity && logFile.Details.Time <= endTimestamp {
			files = append(files, logFile)
		}
	}

	// Sort the files in reverse order so we will fetch the newest first.
	sort.Sort(sort.Reverse(files))
	return files
}

// FetchEntriesFromFiles fetches all available log entries on disk that match
// the log 'severity' (or worse) and are between the 'startTimestamp' and
// 'endTimestamp'. It will stop reading new files if the number of entries
// exceeds 'maxEntries'. Log entries are further filtered by the regexp
// 'pattern' if provided. The logs entries are returned in reverse chronological
// order.
func FetchEntriesFromFiles(
	severity Severity, startTimestamp, endTimestamp int64, maxEntries int, pattern *regexp.Regexp,
) ([]Entry, error) {
	logFiles, err := ListLogFiles()
	if err != nil {
		return nil, err
	}

	selectedFiles := selectFiles(logFiles, severity, endTimestamp)

	entries := []Entry{}
	for _, file := range selectedFiles {
		newEntries, entryBeforeStart, err := readAllEntriesFromFile(
			file,
			startTimestamp,
			endTimestamp,
			maxEntries-len(entries),
			pattern)
		if err != nil {
			return nil, err
		}
		entries = append(entries, newEntries...)
		if len(entries) >= maxEntries {
			break
		}
		if entryBeforeStart {
			// Stop processing files that won't have any timestamps after
			// startTime.
			break
		}
	}
	return entries, nil
}

// readAllEntriesFromFile reads in all log entries from a given file that are
// between the 'startTimestamp' and 'endTimestamp' and match the 'pattern' if it
// exists. It returns the entries in the reverse chronological order. It also
// returns a flag that denotes if any timestamp occurred before the
// 'startTimestamp' to inform the caller that no more log files need to be
// processed. If the number of entries returned exceeds 'maxEntries' then
// processing of new entries is stopped immediately.
func readAllEntriesFromFile(
	file FileInfo, startTimestamp, endTimestamp int64, maxEntries int, pattern *regexp.Regexp,
) ([]Entry, bool, error) {
	reader, err := GetLogReader(file.Name, true /* restricted */)
	if reader == nil || err != nil {
		return nil, false, err
	}
	defer reader.Close()
	entries := []Entry{}
	decoder := NewEntryDecoder(reader)
	entryBeforeStart := false
	for {
		entry := Entry{}
		if err := decoder.Decode(&entry); err != nil {
			if err == io.EOF {
				break
			}
			return nil, false, err
		}
		var match bool
		if pattern == nil {
			match = true
		} else {
			match = pattern.MatchString(entry.Message) ||
				pattern.MatchString(entry.File)
		}
		if match && entry.Time >= startTimestamp && entry.Time <= endTimestamp {
			entries = append([]Entry{entry}, entries...)
			if len(entries) >= maxEntries {
				break
			}
		}
		if entry.Time < startTimestamp {
			entryBeforeStart = true
		}

	}
	return entries, entryBeforeStart, nil
}
<commit_msg>log: use unique temp dir by default<commit_after>// Copyright 2013 Google Inc. All Rights Reserved.
//
// Go support for leveled logs, analogous to https://code.google.com/p/google-clog/
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// File I/O for logs.

// Author: Bram Gruneir (bram@cockroachlabs.com)

package log

import (
	"flag"
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"os/user"
	"path/filepath"
	"regexp"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/pkg/errors"
)

// MaxSize is the maximum size of a log file in bytes.
var MaxSize uint64 = 1024 * 1024 * 10

// If non-empty, overrides the choice of directory in which to write logs. See
// createLogDirs for the full list of possible destinations. Note that the
// default is to log to stderr independent of this setting. See --logtostderr.
var logDir = func() string {
	name, err := ioutil.TempDir("", "cockroach")
	if err != nil {
		panic(err)
	}
	return name
}()
var logDirSet bool

// DirSet returns true of the log directory has been changed from its default.
func DirSet() bool {
	return logDirSet
}

type stringValue struct {
	val *string
	set *bool
}

var _ flag.Value = &stringValue{}

func newStringValue(val *string, set *bool) *stringValue {
	return &stringValue{val: val, set: set}
}

func (s *stringValue) Set(val string) error {
	*s.val = val
	*s.set = true
	return nil
}

func (s *stringValue) Type() string {
	return "string"
}

func (s *stringValue) String() string {
	if s.val == nil {
		return ""
	}
	return *s.val
}

// logFileRE matches log files to avoid exposing non-log files accidentally
// and it splits the details of the filename into groups for easy parsing.
// The log file format is {process}.{host}.{username}.log.{severity}.{timestamp}
// cockroach.Brams-MacBook-Pro.bram.log.WARNING.2015-06-09T16_10_48-04_00.30209
// All underscore in process, host and username are escaped to double
// underscores and all periods are escaped to an underscore.
// For compatibility with Windows filenames, all colons from the timestamp
// (RFC3339) are converted to underscores.
var logFileRE = regexp.MustCompile(`([^\.]+)\.([^\.]+)\.([^\.]+)\.log\.(ERROR|WARNING|INFO)\.([^\.]+)\.(\d+)`)

var (
	pid      = os.Getpid()
	program  = filepath.Base(os.Args[0])
	host     = "unknownhost"
	userName = "unknownuser"
)

func init() {
	h, err := os.Hostname()
	if err == nil {
		host = shortHostname(h)
	}

	current, err := user.Current()
	if err == nil {
		userName = current.Username
	}

	// Sanitize userName since it may contain filepath separators on Windows.
	userName = strings.Replace(userName, `\`, "_", -1)
}

// shortHostname returns its argument, truncating at the first period.
// For instance, given "www.google.com" it returns "www".
func shortHostname(hostname string) string {
	if i := strings.Index(hostname, "."); i >= 0 {
		return hostname[:i]
	}
	return hostname
}

// removePeriods removes all extraneous periods. This is required to ensure that
// the only periods in the filename are the ones added by logName so it can
// be easily parsed.
func removePeriods(s string) string {
	return strings.Replace(s, ".", "", -1)
}

// logName returns a new log file name containing the severity, with start time
// t, and the name for the symlink for the severity.
func logName(severity Severity, t time.Time) (name, link string) {
	// Replace the ':'s in the time format with '_'s to allow for log files in
	// Windows.
	tFormatted := strings.Replace(t.Format(time.RFC3339), ":", "_", -1)

	name = fmt.Sprintf("%s.%s.%s.log.%s.%s.%d",
		removePeriods(program),
		removePeriods(host),
		removePeriods(userName),
		severity.Name(),
		tFormatted,
		pid)
	return name, removePeriods(program) + "." + severity.Name()
}

var errMalformedName = errors.New("malformed log filename")
var errMalformedSev = errors.New("malformed severity")

func parseLogFilename(filename string) (FileDetails, error) {
	matches := logFileRE.FindStringSubmatch(filename)
	if matches == nil || len(matches) != 7 {
		return FileDetails{}, errMalformedName
	}

	sev, sevFound := SeverityByName(matches[4])
	if !sevFound {
		return FileDetails{}, errMalformedSev
	}

	// Replace the '_'s with ':'s to restore the correct time format.
	fixTime := strings.Replace(matches[5], "_", ":", -1)
	time, err := time.Parse(time.RFC3339, fixTime)
	if err != nil {
		return FileDetails{}, err
	}

	pid, err := strconv.ParseInt(matches[6], 10, 0)
	if err != nil {
		return FileDetails{}, err
	}

	return FileDetails{
		Program:  matches[1],
		Host:     matches[2],
		UserName: matches[3],
		Severity: sev,
		Time:     time.UnixNano(),
		PID:      pid,
	}, nil
}

var errDirectoryNotSet = errors.New("log: log directory not set")

// create creates a new log file and returns the file and its filename, which
// contains severity ("INFO", "FATAL", etc.) and t. If the file is created
// successfully, create also attempts to update the symlink for that tag, ignoring
// errors.
func create(severity Severity, t time.Time) (f *os.File, filename string, err error) {
	if len(logDir) == 0 {
		return nil, "", errDirectoryNotSet
	}
	name, link := logName(severity, t)
	fname := filepath.Join(logDir, name)

	// Open the file os.O_APPEND|os.O_CREATE rather than use os.Create.
	// Append is almost always more efficient than O_RDRW on most modern file systems.
	f, err = os.OpenFile(fname, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0664)
	if err == nil {
		symlink := filepath.Join(logDir, link)
		_ = os.Remove(symlink) // ignore err
		err = os.Symlink(fname, symlink)
	}
	return f, fname, errors.Wrapf(err, "log: cannot create log")
}

var errNotAFile = errors.New("not a regular file")

// getFileDetails verifies that the file specified by filename is a
// regular file and filename matches the expected filename pattern.
// Returns the log file details success; otherwise error.
func getFileDetails(info os.FileInfo) (FileDetails, error) {
	if info.Mode()&os.ModeType != 0 {
		return FileDetails{}, errNotAFile
	}

	details, err := parseLogFilename(info.Name())
	if err != nil {
		return FileDetails{}, err
	}

	return details, nil
}

func verifyFile(filename string) error {
	info, err := os.Stat(filename)
	if err != nil {
		return err
	}
	_, err = getFileDetails(info)
	return err
}

// ListLogFiles returns a slice of FileInfo structs for each log file
// on the local node, in any of the configured log directories.
func ListLogFiles() ([]FileInfo, error) {
	var results []FileInfo
	if logDir == "" {
		return nil, nil
	}
	infos, err := ioutil.ReadDir(logDir)
	if err != nil {
		return results, err
	}
	for _, info := range infos {
		details, err := getFileDetails(info)
		if err == nil {
			results = append(results, FileInfo{
				Name:         info.Name(),
				SizeBytes:    info.Size(),
				ModTimeNanos: info.ModTime().UnixNano(),
				Details:      details,
			})
		}
	}
	return results, nil
}

// GetLogReader returns a reader for the specified filename. In
// restricted mode, the filename must be the base name of a file in
// this process's log directory (this is safe for cases when the
// filename comes from external sources, such as the admin UI via
// HTTP). In unrestricted mode any path is allowed, with the added
// feature that relative paths will be searched in both the current
// directory and this process's log directory.
func GetLogReader(filename string, restricted bool) (io.ReadCloser, error) {
	// Verify there are no path separators in a restricted-mode pathname.
	if restricted && filepath.Base(filename) != filename {
		return nil, fmt.Errorf("pathnames must be basenames only: %s", filename)
	}
	if !filepath.IsAbs(filename) {
		filename = filepath.Join(logDir, filename)
	}
	if !restricted {
		var err error
		filename, err = filepath.EvalSymlinks(filename)
		if err != nil {
			return nil, err
		}
	}
	if err := verifyFile(filename); err != nil {
		return nil, err
	}
	return os.Open(filename)
}

// sortableFileInfoSlice is required so we can sort FileInfos.
type sortableFileInfoSlice []FileInfo

func (a sortableFileInfoSlice) Len() int      { return len(a) }
func (a sortableFileInfoSlice) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a sortableFileInfoSlice) Less(i, j int) bool {
	return a[i].Details.Time < a[j].Details.Time
}

// selectFiles selects all log files that have an timestamp before the endTime and
// the correct severity. It then sorts them in decreasing order, with the most
// recent as the first one.
func selectFiles(logFiles []FileInfo, severity Severity, endTimestamp int64) []FileInfo {
	files := sortableFileInfoSlice{}
	for _, logFile := range logFiles {
		if logFile.Details.Severity == severity && logFile.Details.Time <= endTimestamp {
			files = append(files, logFile)
		}
	}

	// Sort the files in reverse order so we will fetch the newest first.
	sort.Sort(sort.Reverse(files))
	return files
}

// FetchEntriesFromFiles fetches all available log entries on disk that match
// the log 'severity' (or worse) and are between the 'startTimestamp' and
// 'endTimestamp'. It will stop reading new files if the number of entries
// exceeds 'maxEntries'. Log entries are further filtered by the regexp
// 'pattern' if provided. The logs entries are returned in reverse chronological
// order.
func FetchEntriesFromFiles(
	severity Severity, startTimestamp, endTimestamp int64, maxEntries int, pattern *regexp.Regexp,
) ([]Entry, error) {
	logFiles, err := ListLogFiles()
	if err != nil {
		return nil, err
	}

	selectedFiles := selectFiles(logFiles, severity, endTimestamp)

	entries := []Entry{}
	for _, file := range selectedFiles {
		newEntries, entryBeforeStart, err := readAllEntriesFromFile(
			file,
			startTimestamp,
			endTimestamp,
			maxEntries-len(entries),
			pattern)
		if err != nil {
			return nil, err
		}
		entries = append(entries, newEntries...)
		if len(entries) >= maxEntries {
			break
		}
		if entryBeforeStart {
			// Stop processing files that won't have any timestamps after
			// startTime.
			break
		}
	}
	return entries, nil
}

// readAllEntriesFromFile reads in all log entries from a given file that are
// between the 'startTimestamp' and 'endTimestamp' and match the 'pattern' if it
// exists. It returns the entries in the reverse chronological order. It also
// returns a flag that denotes if any timestamp occurred before the
// 'startTimestamp' to inform the caller that no more log files need to be
// processed. If the number of entries returned exceeds 'maxEntries' then
// processing of new entries is stopped immediately.
func readAllEntriesFromFile(
	file FileInfo, startTimestamp, endTimestamp int64, maxEntries int, pattern *regexp.Regexp,
) ([]Entry, bool, error) {
	reader, err := GetLogReader(file.Name, true /* restricted */)
	if reader == nil || err != nil {
		return nil, false, err
	}
	defer reader.Close()
	entries := []Entry{}
	decoder := NewEntryDecoder(reader)
	entryBeforeStart := false
	for {
		entry := Entry{}
		if err := decoder.Decode(&entry); err != nil {
			if err == io.EOF {
				break
			}
			return nil, false, err
		}
		var match bool
		if pattern == nil {
			match = true
		} else {
			match = pattern.MatchString(entry.Message) ||
				pattern.MatchString(entry.File)
		}
		if match && entry.Time >= startTimestamp && entry.Time <= endTimestamp {
			entries = append([]Entry{entry}, entries...)
			if len(entries) >= maxEntries {
				break
			}
		}
		if entry.Time < startTimestamp {
			entryBeforeStart = true
		}

	}
	return entries, entryBeforeStart, nil
}
<|endoftext|>


<commit_before>package main

import (
	"bufio"
	"fmt"
	"github.com/ginuerzh/poker"
	"github.com/gorilla/websocket"
	"log"
	"math/rand"
	"net"
	"net/url"
	"os"
	"strconv"
	"strings"
	"time"
)

const (
	readWait = 10 * time.Second
	version  = "1.0"
)

func randName() string {
	rand.Seed(time.Now().Unix())
	var b []byte
	for i := 0; i < 5; i++ {
		b = append(b, byte(rand.Intn(26)+97))
	}
	b[0] -= 32
	return string(b)
}

func main() {
	c, err := net.Dial("tcp", "localhost:8000")
	if err != nil {
		log.Fatal(err)
	}
	u, err := url.Parse("http://localhost:8000/")
	if err != nil {
		log.Fatal(err)
	}

	ws, _, err := websocket.NewClient(c, u, nil, 1024, 1024)
	if err != nil {
		log.Fatal(err)
	}
	conn := poker.NewConn(ws, 10)

	ver := &poker.Version{
		Ver: version,
	}

	if err := conn.WriteJSON(ver); err != nil {
		log.Fatal(err)
	}

	if err := conn.ReadJSON(ver); err != nil {
		log.Fatal(err)
	}

	auth := poker.Auth{Mechanism: "plain", Text: randName()}
	if err := conn.WriteJSON(auth); err != nil {
		log.Fatal(err)
	}

	resp := &poker.AuthResp{}
	if err := conn.ReadJSON(resp); err != nil {
		log.Fatal(err)
	}

	o := poker.NewOccupant(resp.Id, conn)
	o.Name = resp.Name
	o.Chips = resp.Chips

	fmt.Printf("%s(%s) %d\n", o.Id, o.Name, o.Chips)

	go handleMessage(o)

	cmdLoop(o)

}

func handleMessage(o *poker.Occupant) {
	for {
		message, _ := o.GetMessage(0)
		if message == nil {
			break
		}

		switch message.Type {
		case poker.MsgPresence:
			handlePresence(o, message)
		}
	}
}

func handlePresence(o *poker.Occupant, message *poker.Message) {
	switch message.Action {
	case poker.ActState:
		o.Room = message.Room
		fmt.Printf("Enter room, %d Occupants\n", o.Room.N)
	case poker.ActJoin:
		occupant := message.Occupant
		o.Room.Occupants[occupant.Pos-1] = occupant
		o.Room.N++
		fmt.Printf("%s(%s) Join.\n", occupant.Id, occupant.Name)
	case poker.ActLeave:
		occupant := message.Occupant
		o.Room.Occupants[occupant.Pos-1] = nil
		o.Room.N--
		if occupant.Id == o.Id {
			o.Room = nil
			o.Pos = 0
			fmt.Println("You are kicked.")
		} else {
			fmt.Printf("%s(%s) Leave.\n", occupant.Id, occupant.Name)
		}
	case poker.ActButton:
		pos, _ := strconv.Atoi(message.Class)

		o.Room.Button = pos
		o.Room.Bet = 0
		o.Room.Cards = nil
		o.Room.Pot = make([]int, 1)
		o.Room.Each(0, func(o *poker.Occupant) bool {
			if o != nil {
				o.Bet = 0
				o.Action = ""
				o.Cards = nil
				o.Hand = 0
			}
			return true
		})

		dealer := o.Room.Occupants[pos-1]
		fmt.Printf("Button: %s(%s).\n", dealer.Id, dealer.Name)
	case poker.ActPreflop:
		fmt.Println("Preflop:", message.Class)
		cards := strings.Split(message.Class, ",")
		o.Cards = append(o.Cards, poker.ParseCard(cards[0]))
		o.Cards = append(o.Cards, poker.ParseCard(cards[1]))
	case poker.ActFlop:
		o.Room.Each(0, func(o *poker.Occupant) bool {
			if o != nil {
				o.Bet = 0
				o.Action = ""
			}
			return true
		})
		fmt.Println("Flop:", message.Class)
		cards := strings.Split(message.Class, ",")
		o.Room.Cards = append(o.Room.Cards, poker.ParseCard(cards[0]))
		o.Room.Cards = append(o.Room.Cards, poker.ParseCard(cards[1]))
		o.Room.Cards = append(o.Room.Cards, poker.ParseCard(cards[2]))
		o.Hand, _ = strconv.Atoi(cards[3])
	case poker.ActTurn:
		o.Room.Each(0, func(o *poker.Occupant) bool {
			if o != nil {
				o.Bet = 0
				o.Action = ""
			}
			return true
		})
		fmt.Println("Turn:", message.Class)
		cards := strings.Split(message.Class, ",")
		o.Room.Cards = append(o.Room.Cards, poker.ParseCard(cards[0]))
		o.Hand, _ = strconv.Atoi(cards[1])
	case poker.ActRiver:
		o.Room.Each(0, func(o *poker.Occupant) bool {
			if o != nil {
				o.Bet = 0
				o.Action = ""
			}
			return true
		})
		fmt.Println("River:", message.Class)
		cards := strings.Split(message.Class, ",")
		o.Room.Cards = append(o.Room.Cards, poker.ParseCard(cards[0]))
		o.Hand, _ = strconv.Atoi(cards[1])
	case poker.ActShowdown:
		fmt.Println("pot:", o.Room.Pot)
	case poker.ActAction:
		a := strings.Split(message.Class, ",")
		pos, _ := strconv.Atoi(a[0])
		o.Room.Bet, _ = strconv.Atoi(a[1])
		if o.Room.Occupants[pos-1].Id == o.Id {
			log.Printf("Your bet turn (%d/%d/%d):\n",
				o.Room.Occupants[pos-1].Bet, o.Room.Bet, o.Room.Occupants[pos-1].Chips)
		}
	case poker.ActPot:
		pots := strings.Split(message.Class, ",")
		o.Room.Pot = nil
		for i, _ := range pots {
			pot, _ := strconv.Atoi(pots[i])
			o.Room.Pot = append(o.Room.Pot, pot)
		}
	case poker.ActBet:
		occupant := o.Room.Occupant(message.From)
		occupant.Room = o.Room
		bets := strings.Split(message.Class, ",")
		occupant.Action = bets[0]
		occupant.Bet, _ = strconv.Atoi(bets[1])
		occupant.Chips, _ = strconv.Atoi(bets[2])

		if occupant.Id == o.Id {
			fmt.Printf("You %s: %d\n", occupant.Action, occupant.Bet)
		} else {
			fmt.Printf("%s(%s) %s: %d\n", occupant.Id, occupant.Name, occupant.Action, occupant.Bet)
		}
	}
}

func cmdLoop(o *poker.Occupant) {
	reader := bufio.NewReader(os.Stdin)

	for {
		fmt.Print("poker> ")
		cmd, _ := reader.ReadString('\n')
		cmd = strings.ToLower(strings.Trim(cmd, " \n"))

		if len(cmd) == 0 {
			continue
		}
		switch cmd[0] {
		case 'j':
			if o.Room == nil {
				o.SendMessage(&poker.Message{
					Type:   poker.MsgPresence,
					Action: poker.ActJoin,
					To:     "0",
				})
			}

		case 'l':
			if o.Room != nil {
				o.SendMessage(&poker.Message{
					Type:   poker.MsgPresence,
					Action: poker.ActLeave,
					To:     "0",
				})
			}
			o.Pos = 0
			o.Room = nil
		case 'c':
			if o.Room != nil {
				cards := []poker.Card{}
				cards = append(cards, o.Cards...)

				cards = append(cards, o.Room.Cards...)
				fmt.Println(cards)
			}
		case 'q':
			return
		default:
			bet, _ := strconv.ParseInt(cmd, 10, 32)
			o.SendMessage(&poker.Message{
				Type:   poker.MsgPresence,
				Action: poker.ActBet,
				Class:  strconv.FormatInt(bet, 10),
			})
		}
	}
}
<commit_msg>update client example<commit_after>package main

import (
	"bufio"
	"fmt"
	"github.com/ginuerzh/poker/server"
	"github.com/gorilla/websocket"
	"log"
	"math/rand"
	"net"
	"net/url"
	"os"
	"strconv"
	"strings"
	"time"
)

const (
	readWait = 10 * time.Second
	version  = "1.0"
)

func randName() string {
	rand.Seed(time.Now().Unix())
	var b []byte
	for i := 0; i < 5; i++ {
		b = append(b, byte(rand.Intn(26)+97))
	}
	b[0] -= 32
	return string(b)
}

func main() {
	c, err := net.Dial("tcp", "localhost:8989")
	if err != nil {
		log.Fatal(err)
	}
	u, err := url.Parse("http://localhost:8989/ws")
	if err != nil {
		log.Fatal(err)
	}

	ws, _, err := websocket.NewClient(c, u, nil, 1024, 1024)
	if err != nil {
		log.Fatal(err)
	}
	conn := poker.NewConn(ws, 10)

	ver := &poker.Version{
		Ver: version,
	}

	if err := conn.WriteJSON(ver); err != nil {
		log.Fatal(err)
	}

	if err := conn.ReadJSON(ver); err != nil {
		log.Fatal(err)
	}

	auth := poker.Auth{Mechanism: "plain", Text: randName()}
	if err := conn.WriteJSON(auth); err != nil {
		log.Fatal(err)
	}

	resp := &poker.AuthResp{}
	if err := conn.ReadJSON(resp); err != nil {
		log.Fatal(err)
	}

	o := poker.NewOccupant(resp.Id, conn)
	o.Name = resp.Name
	o.Chips = resp.Chips

	fmt.Printf("%s(%s) %d\n", o.Id, o.Name, o.Chips)

	go handleMessage(o)

	cmdLoop(o)

}

func handleMessage(o *poker.Occupant) {
	for {
		message, _ := o.GetMessage(0)
		if message == nil {
			break
		}

		switch message.Type {
		case poker.MsgPresence:
			handlePresence(o, message)
		}
	}
}

func handlePresence(o *poker.Occupant, message *poker.Message) {
	switch message.Action {
	case poker.ActState:
		o.Room = message.Room
		fmt.Printf("Enter room, %d Occupants\n", o.Room.N)
	case poker.ActJoin:
		occupant := message.Occupant
		o.Room.Occupants[occupant.Pos-1] = occupant
		o.Room.N++
		fmt.Printf("%s(%s) Join.\n", occupant.Id, occupant.Name)
	case poker.ActLeave:
		occupant := message.Occupant
		o.Room.Occupants[occupant.Pos-1] = nil
		o.Room.N--
		if occupant.Id == o.Id {
			o.Room = nil
			o.Pos = 0
			fmt.Println("You are kicked.")
		} else {
			fmt.Printf("%s(%s) Leave.\n", occupant.Id, occupant.Name)
		}
	case poker.ActButton:
		pos, _ := strconv.Atoi(message.Class)

		o.Room.Button = pos
		o.Room.Bet = 0
		o.Room.Cards = nil
		o.Room.Pot = make([]int, 1)
		o.Room.Each(0, func(o *poker.Occupant) bool {
			if o != nil {
				o.Bet = 0
				o.Action = ""
				o.Cards = nil
				o.Hand = 0
			}
			return true
		})

		dealer := o.Room.Occupants[pos-1]
		fmt.Printf("Button: %s(%s).\n", dealer.Id, dealer.Name)
	case poker.ActPreflop:
		fmt.Println("Preflop:", message.Class)
		cards := strings.Split(message.Class, ",")
		o.Cards = append(o.Cards, poker.ParseCard(cards[0]))
		o.Cards = append(o.Cards, poker.ParseCard(cards[1]))
	case poker.ActFlop:
		o.Room.Each(0, func(o *poker.Occupant) bool {
			if o != nil {
				o.Bet = 0
				o.Action = ""
			}
			return true
		})
		fmt.Println("Flop:", message.Class)
		cards := strings.Split(message.Class, ",")
		o.Room.Cards = append(o.Room.Cards, poker.ParseCard(cards[0]))
		o.Room.Cards = append(o.Room.Cards, poker.ParseCard(cards[1]))
		o.Room.Cards = append(o.Room.Cards, poker.ParseCard(cards[2]))
		o.Hand, _ = strconv.Atoi(cards[3])
	case poker.ActTurn:
		o.Room.Each(0, func(o *poker.Occupant) bool {
			if o != nil {
				o.Bet = 0
				o.Action = ""
			}
			return true
		})
		fmt.Println("Turn:", message.Class)
		cards := strings.Split(message.Class, ",")
		o.Room.Cards = append(o.Room.Cards, poker.ParseCard(cards[0]))
		o.Hand, _ = strconv.Atoi(cards[1])
	case poker.ActRiver:
		o.Room.Each(0, func(o *poker.Occupant) bool {
			if o != nil {
				o.Bet = 0
				o.Action = ""
			}
			return true
		})
		fmt.Println("River:", message.Class)
		cards := strings.Split(message.Class, ",")
		o.Room.Cards = append(o.Room.Cards, poker.ParseCard(cards[0]))
		o.Hand, _ = strconv.Atoi(cards[1])
	case poker.ActShowdown:
		fmt.Println("pot:", o.Room.Pot)
	case poker.ActAction:
		a := strings.Split(message.Class, ",")
		pos, _ := strconv.Atoi(a[0])
		o.Room.Bet, _ = strconv.Atoi(a[1])
		if o.Room.Occupants[pos-1].Id == o.Id {
			log.Printf("Your bet turn (%d/%d/%d):\n",
				o.Room.Occupants[pos-1].Bet, o.Room.Bet, o.Room.Occupants[pos-1].Chips)
		}
	case poker.ActPot:
		pots := strings.Split(message.Class, ",")
		o.Room.Pot = nil
		for i, _ := range pots {
			pot, _ := strconv.Atoi(pots[i])
			o.Room.Pot = append(o.Room.Pot, pot)
		}
	case poker.ActBet:
		occupant := o.Room.Occupant(message.From)
		occupant.Room = o.Room
		bets := strings.Split(message.Class, ",")
		occupant.Action = bets[0]
		occupant.Bet, _ = strconv.Atoi(bets[1])
		occupant.Chips, _ = strconv.Atoi(bets[2])

		if occupant.Id == o.Id {
			fmt.Printf("You %s: %d\n", occupant.Action, occupant.Bet)
		} else {
			fmt.Printf("%s(%s) %s: %d\n", occupant.Id, occupant.Name, occupant.Action, occupant.Bet)
		}
	}
}

func cmdLoop(o *poker.Occupant) {
	reader := bufio.NewReader(os.Stdin)

	for {
		fmt.Print("poker> ")
		cmd, _ := reader.ReadString('\n')
		cmd = strings.ToLower(strings.Trim(cmd, " \n"))

		if len(cmd) == 0 {
			continue
		}
		switch cmd[0] {
		case 'j':
			if o.Room == nil {
				o.SendMessage(&poker.Message{
					Type:   poker.MsgPresence,
					Action: poker.ActJoin,
					To:     "0",
				})
			}

		case 'l':
			if o.Room != nil {
				o.SendMessage(&poker.Message{
					Type:   poker.MsgPresence,
					Action: poker.ActLeave,
					To:     "0",
				})
			}
			o.Pos = 0
			o.Room = nil
		case 'c':
			if o.Room != nil {
				cards := []poker.Card{}
				cards = append(cards, o.Cards...)

				cards = append(cards, o.Room.Cards...)
				fmt.Println(cards)
			}
		case 'q':
			return
		default:
			bet, _ := strconv.ParseInt(cmd, 10, 32)
			o.SendMessage(&poker.Message{
				Type:   poker.MsgPresence,
				Action: poker.ActBet,
				Class:  strconv.FormatInt(bet, 10),
			})
		}
	}
}
<|endoftext|>


<commit_before>package notifier

import (
	"fmt"
	"io/ioutil"
	"net/http"
	"os"
	"strings"
	"sync"
	"testing"
	"time"

	"github.com/oinume/lekcije/server/emailer"
	"github.com/oinume/lekcije/server/fetcher"
	"github.com/oinume/lekcije/server/logger"
	"github.com/oinume/lekcije/server/model"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"go.uber.org/zap/zapcore"
)

var helper = model.NewTestHelper()
var _ = fmt.Print

type mockSenderTransport struct {
	sync.Mutex
	called      int
	requestBody string
}

func (t *mockSenderTransport) RoundTrip(req *http.Request) (*http.Response, error) {
	t.Lock()
	t.called++
	t.Unlock()
	time.Sleep(time.Millisecond * 500)
	resp := &http.Response{
		Header:     make(http.Header),
		Request:    req,
		StatusCode: http.StatusAccepted,
		Status:     "202 Accepted",
	}
	body, err := ioutil.ReadAll(req.Body)
	if err != nil {
		return resp, err
	}
	t.requestBody = string(body)
	defer req.Body.Close()
	resp.Body = ioutil.NopCloser(strings.NewReader(""))
	return resp, nil
}

func TestMain(m *testing.M) {
	db := helper.DB()
	defer db.Close()
	helper.TruncateAllTables(db)
	os.Exit(m.Run())
}

func TestTeachersAndLessons_FilterBy(t *testing.T) {
	user := helper.CreateRandomUser()
	timeSpans := []*model.NotificationTimeSpan{
		{UserID: user.ID, Number: 1, FromTime: "15:30:00", ToTime: "16:30:00"},
		{UserID: user.ID, Number: 2, FromTime: "20:00:00", ToTime: "22:00:00"},
	}
	teacher := helper.CreateRandomTeacher()
	// TODO: table driven test
	lessons := []*model.Lesson{
		{TeacherID: teacher.ID, Datetime: time.Date(2018, 1, 1, 15, 0, 0, 0, time.UTC)}, // excluded
		{TeacherID: teacher.ID, Datetime: time.Date(2018, 1, 1, 16, 0, 0, 0, time.UTC)}, // included
		{TeacherID: teacher.ID, Datetime: time.Date(2018, 1, 1, 17, 0, 0, 0, time.UTC)}, // excluded
		{TeacherID: teacher.ID, Datetime: time.Date(2018, 1, 1, 21, 0, 0, 0, time.UTC)}, // included
		{TeacherID: teacher.ID, Datetime: time.Date(2018, 1, 1, 23, 0, 0, 0, time.UTC)}, // excluded
	}
	tal := NewTeachersAndLessons(10)
	tal.data[teacher.ID] = &model.TeacherLessons{Teacher: teacher, Lessons: lessons}

	filtered := tal.FilterBy(model.NotificationTimeSpanList(timeSpans))
	if got, want := filtered.CountLessons(), 2; got != want {
		t.Fatalf("unexpected filtered lessons count: got=%v, want=%v", got, want)
	}

	wantTimes := []struct {
		hour, minute int
	}{
		{16, 0},
		{21, 0},
	}
	tl := filtered.data[teacher.ID]
	for i, wantTime := range wantTimes {
		if got, want := tl.Lessons[i].Datetime.Hour(), wantTime.hour; got != want {
			t.Errorf("unexpected hour: got=%v, want=%v", got, want)
		}
		if got, want := tl.Lessons[i].Datetime.Minute(), wantTime.minute; got != want {
			t.Errorf("unexpected minute: got=%v, want=%v", got, want)
		}
	}
}

func TestTeachersAndLessons_FilterByEmpty(t *testing.T) {
	//user := helper.CreateRandomUser()
	timeSpans := make([]*model.NotificationTimeSpan, 0)
	teacher := helper.CreateRandomTeacher()
	// TODO: table driven test
	lessons := []*model.Lesson{
		{TeacherID: teacher.ID, Datetime: time.Date(2018, 1, 1, 15, 0, 0, 0, time.UTC)},
		{TeacherID: teacher.ID, Datetime: time.Date(2018, 1, 1, 16, 0, 0, 0, time.UTC)},
	}
	tal := NewTeachersAndLessons(10)
	tal.data[teacher.ID] = &model.TeacherLessons{Teacher: teacher, Lessons: lessons}

	filtered := tal.FilterBy(model.NotificationTimeSpanList(timeSpans))
	if got, want := filtered.CountLessons(), len(lessons); got != want {
		t.Fatalf("unexpected filtered lessons count: got=%v, want=%v", got, want)
	}

	wantTimes := []struct {
		hour, minute int
	}{
		{15, 0},
		{16, 0},
	}
	tl := filtered.data[teacher.ID]
	for i, wantTime := range wantTimes {
		if got, want := tl.Lessons[i].Datetime.Hour(), wantTime.hour; got != want {
			t.Errorf("unexpected hour: got=%v, want=%v", got, want)
		}
		if got, want := tl.Lessons[i].Datetime.Minute(), wantTime.minute; got != want {
			t.Errorf("unexpected minute: got=%v, want=%v", got, want)
		}
	}
}

func TestNotifier_SendNotification(t *testing.T) {
	db := helper.DB()
	logger.InitializeAppLogger(os.Stdout, zapcore.DebugLevel)

	fetcherMockTransport, err := fetcher.NewMockTransport("../fetcher/testdata/3986.html")
	if err != nil {
		t.Fatalf("fetcher.NewMockTransport failed: err=%v", err)
	}
	fetcherHTTPClient := &http.Client{
		Transport: fetcherMockTransport,
	}

	t.Run("10_users", func(t *testing.T) {
		var users []*model.User
		const numOfUsers = 10
		for i := 0; i < numOfUsers; i++ {
			name := fmt.Sprintf("oinume+%02d", i)
			user := helper.CreateUser(name, name+"@gmail.com")
			teacher := helper.CreateRandomTeacher()
			helper.CreateFollowingTeacher(user.ID, teacher)
			users = append(users, user)
		}

		fetcher := fetcher.NewLessonFetcher(fetcherHTTPClient, 1, false, helper.LoadMCountries(), nil)
		senderTransport := &mockSenderTransport{}
		senderHTTPClient := &http.Client{
			Transport: senderTransport,
		}
		sender := emailer.NewSendGridSender(senderHTTPClient)
		n := NewNotifier(db, fetcher, true, sender)

		for _, user := range users {
			if err := n.SendNotification(user); err != nil {
				t.Fatalf("SendNotification failed: err=%v", err)
			}
		}
		n.Close() // Wait all async requests are done

		//if got, want := senderTransport.called, numOfUsers; got <= want {
		//	t.Errorf("unexpected senderTransport.called: got=%v, want=%v", got, want)
		//}
	})

	t.Run("narrow_down_with_notification_time_span", func(t *testing.T) {
		user := helper.CreateRandomUser()
		teacher := helper.CreateRandomTeacher()
		helper.CreateFollowingTeacher(user.ID, teacher)

		notificationTimeSpanService := model.NewNotificationTimeSpanService(helper.DB())
		timeSpans := []*model.NotificationTimeSpan{
			{UserID: user.ID, Number: 1, FromTime: "02:00:00", ToTime: "03:00:00"},
			{UserID: user.ID, Number: 2, FromTime: "06:00:00", ToTime: "07:00:00"},
		}
		if err := notificationTimeSpanService.UpdateAll(user.ID, timeSpans); err != nil {
			t.Fatalf("UpdateAll failed: err=%v", err)
		}

		fetcher := fetcher.NewLessonFetcher(fetcherHTTPClient, 1, false, helper.LoadMCountries(), nil)
		senderTransport := &mockSenderTransport{}
		senderHTTPClient := &http.Client{
			Transport: senderTransport,
		}
		sender := emailer.NewSendGridSender(senderHTTPClient)
		n := NewNotifier(db, fetcher, true, sender)
		if err := n.SendNotification(user); err != nil {
			t.Fatalf("SendNotification failed: err=%v", err)
		}

		n.Close() // Wait all async requests are done before reading request body
		content := senderTransport.requestBody
		// TODO: table drive test
		if !strings.Contains(content, "02:30") {
			t.Errorf("content must contain 02:30 due to notification time span")
		}
		if !strings.Contains(content, "06:00") {
			t.Errorf("content must contain 06:00 due to notification time span")
		}
		if strings.Contains(content, "05:00") {
			t.Errorf("content must not contain 23:30 due to notification time span")
		}
		//fmt.Printf("content = %v\n", content)
	})
}

func TestNotifier_Close(t *testing.T) {
	a := assert.New(t)
	r := require.New(t)
	db := helper.DB()
	logger.InitializeAppLogger(os.Stdout, zapcore.DebugLevel)

	fetcherMockTransport, err := fetcher.NewMockTransport("../fetcher/testdata/3986.html")
	r.NoError(err, "fetcher.NewMockTransport failed")
	fetcherHTTPClient := &http.Client{
		Transport: fetcherMockTransport,
	}
	fetcher := fetcher.NewLessonFetcher(fetcherHTTPClient, 1, false, helper.LoadMCountries(), nil)

	senderTransport := &mockSenderTransport{}
	senderHTTPClient := &http.Client{
		Transport: senderTransport,
	}
	sender := emailer.NewSendGridSender(senderHTTPClient)

	user := helper.CreateRandomUser()
	teacher := helper.CreateTeacher(3982, "Hena")
	helper.CreateFollowingTeacher(user.ID, teacher)

	n := NewNotifier(db, fetcher, false, sender)
	err = n.SendNotification(user)
	r.NoError(err, "SendNotification failed")
	n.Close()

	teacherService := model.NewTeacherService(db)
	updatedTeacher, err := teacherService.FindByPK(teacher.ID)
	r.NoError(err)
	a.NotEqual(teacher.CountryID, updatedTeacher.CountryID)
	a.NotEqual(teacher.FavoriteCount, updatedTeacher.FavoriteCount)
	a.NotEqual(teacher.Rating, updatedTeacher.Rating)
	a.NotEqual(teacher.ReviewCount, updatedTeacher.ReviewCount)
}
<commit_msg>Try to fix data race<commit_after>package notifier

import (
	"fmt"
	"io/ioutil"
	"net/http"
	"os"
	"strings"
	"sync"
	"testing"
	"time"

	"github.com/oinume/lekcije/server/emailer"
	"github.com/oinume/lekcije/server/fetcher"
	"github.com/oinume/lekcije/server/logger"
	"github.com/oinume/lekcije/server/model"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"go.uber.org/zap/zapcore"
)

var helper = model.NewTestHelper()
var _ = fmt.Print

type mockSenderTransport struct {
	sync.Mutex
	called      int
	requestBody string
}

func (t *mockSenderTransport) RoundTrip(req *http.Request) (*http.Response, error) {
	t.Lock()
	t.called++
	defer t.Unlock()
	time.Sleep(time.Millisecond * 500)
	resp := &http.Response{
		Header:     make(http.Header),
		Request:    req,
		StatusCode: http.StatusAccepted,
		Status:     "202 Accepted",
	}
	body, err := ioutil.ReadAll(req.Body)
	if err != nil {
		return resp, err
	}
	t.requestBody = string(body)
	defer req.Body.Close()
	resp.Body = ioutil.NopCloser(strings.NewReader(""))
	return resp, nil
}

func TestMain(m *testing.M) {
	db := helper.DB()
	defer db.Close()
	helper.TruncateAllTables(db)
	os.Exit(m.Run())
}

func TestTeachersAndLessons_FilterBy(t *testing.T) {
	user := helper.CreateRandomUser()
	timeSpans := []*model.NotificationTimeSpan{
		{UserID: user.ID, Number: 1, FromTime: "15:30:00", ToTime: "16:30:00"},
		{UserID: user.ID, Number: 2, FromTime: "20:00:00", ToTime: "22:00:00"},
	}
	teacher := helper.CreateRandomTeacher()
	// TODO: table driven test
	lessons := []*model.Lesson{
		{TeacherID: teacher.ID, Datetime: time.Date(2018, 1, 1, 15, 0, 0, 0, time.UTC)}, // excluded
		{TeacherID: teacher.ID, Datetime: time.Date(2018, 1, 1, 16, 0, 0, 0, time.UTC)}, // included
		{TeacherID: teacher.ID, Datetime: time.Date(2018, 1, 1, 17, 0, 0, 0, time.UTC)}, // excluded
		{TeacherID: teacher.ID, Datetime: time.Date(2018, 1, 1, 21, 0, 0, 0, time.UTC)}, // included
		{TeacherID: teacher.ID, Datetime: time.Date(2018, 1, 1, 23, 0, 0, 0, time.UTC)}, // excluded
	}
	tal := NewTeachersAndLessons(10)
	tal.data[teacher.ID] = &model.TeacherLessons{Teacher: teacher, Lessons: lessons}

	filtered := tal.FilterBy(model.NotificationTimeSpanList(timeSpans))
	if got, want := filtered.CountLessons(), 2; got != want {
		t.Fatalf("unexpected filtered lessons count: got=%v, want=%v", got, want)
	}

	wantTimes := []struct {
		hour, minute int
	}{
		{16, 0},
		{21, 0},
	}
	tl := filtered.data[teacher.ID]
	for i, wantTime := range wantTimes {
		if got, want := tl.Lessons[i].Datetime.Hour(), wantTime.hour; got != want {
			t.Errorf("unexpected hour: got=%v, want=%v", got, want)
		}
		if got, want := tl.Lessons[i].Datetime.Minute(), wantTime.minute; got != want {
			t.Errorf("unexpected minute: got=%v, want=%v", got, want)
		}
	}
}

func TestTeachersAndLessons_FilterByEmpty(t *testing.T) {
	//user := helper.CreateRandomUser()
	timeSpans := make([]*model.NotificationTimeSpan, 0)
	teacher := helper.CreateRandomTeacher()
	// TODO: table driven test
	lessons := []*model.Lesson{
		{TeacherID: teacher.ID, Datetime: time.Date(2018, 1, 1, 15, 0, 0, 0, time.UTC)},
		{TeacherID: teacher.ID, Datetime: time.Date(2018, 1, 1, 16, 0, 0, 0, time.UTC)},
	}
	tal := NewTeachersAndLessons(10)
	tal.data[teacher.ID] = &model.TeacherLessons{Teacher: teacher, Lessons: lessons}

	filtered := tal.FilterBy(model.NotificationTimeSpanList(timeSpans))
	if got, want := filtered.CountLessons(), len(lessons); got != want {
		t.Fatalf("unexpected filtered lessons count: got=%v, want=%v", got, want)
	}

	wantTimes := []struct {
		hour, minute int
	}{
		{15, 0},
		{16, 0},
	}
	tl := filtered.data[teacher.ID]
	for i, wantTime := range wantTimes {
		if got, want := tl.Lessons[i].Datetime.Hour(), wantTime.hour; got != want {
			t.Errorf("unexpected hour: got=%v, want=%v", got, want)
		}
		if got, want := tl.Lessons[i].Datetime.Minute(), wantTime.minute; got != want {
			t.Errorf("unexpected minute: got=%v, want=%v", got, want)
		}
	}
}

func TestNotifier_SendNotification(t *testing.T) {
	db := helper.DB()
	logger.InitializeAppLogger(os.Stdout, zapcore.DebugLevel)

	fetcherMockTransport, err := fetcher.NewMockTransport("../fetcher/testdata/3986.html")
	if err != nil {
		t.Fatalf("fetcher.NewMockTransport failed: err=%v", err)
	}
	fetcherHTTPClient := &http.Client{
		Transport: fetcherMockTransport,
	}

	t.Run("10_users", func(t *testing.T) {
		var users []*model.User
		const numOfUsers = 10
		for i := 0; i < numOfUsers; i++ {
			name := fmt.Sprintf("oinume+%02d", i)
			user := helper.CreateUser(name, name+"@gmail.com")
			teacher := helper.CreateRandomTeacher()
			helper.CreateFollowingTeacher(user.ID, teacher)
			users = append(users, user)
		}

		fetcher := fetcher.NewLessonFetcher(fetcherHTTPClient, 1, false, helper.LoadMCountries(), nil)
		senderTransport := &mockSenderTransport{}
		senderHTTPClient := &http.Client{
			Transport: senderTransport,
		}
		sender := emailer.NewSendGridSender(senderHTTPClient)
		n := NewNotifier(db, fetcher, true, sender)

		for _, user := range users {
			if err := n.SendNotification(user); err != nil {
				t.Fatalf("SendNotification failed: err=%v", err)
			}
		}
		n.Close() // Wait all async requests are done

		//if got, want := senderTransport.called, numOfUsers; got <= want {
		//	t.Errorf("unexpected senderTransport.called: got=%v, want=%v", got, want)
		//}
	})

	t.Run("narrow_down_with_notification_time_span", func(t *testing.T) {
		user := helper.CreateRandomUser()
		teacher := helper.CreateRandomTeacher()
		helper.CreateFollowingTeacher(user.ID, teacher)

		notificationTimeSpanService := model.NewNotificationTimeSpanService(helper.DB())
		timeSpans := []*model.NotificationTimeSpan{
			{UserID: user.ID, Number: 1, FromTime: "02:00:00", ToTime: "03:00:00"},
			{UserID: user.ID, Number: 2, FromTime: "06:00:00", ToTime: "07:00:00"},
		}
		if err := notificationTimeSpanService.UpdateAll(user.ID, timeSpans); err != nil {
			t.Fatalf("UpdateAll failed: err=%v", err)
		}

		fetcher := fetcher.NewLessonFetcher(fetcherHTTPClient, 1, false, helper.LoadMCountries(), nil)
		senderTransport := &mockSenderTransport{}
		senderHTTPClient := &http.Client{
			Transport: senderTransport,
		}
		sender := emailer.NewSendGridSender(senderHTTPClient)
		n := NewNotifier(db, fetcher, true, sender)
		if err := n.SendNotification(user); err != nil {
			t.Fatalf("SendNotification failed: err=%v", err)
		}

		n.Close() // Wait all async requests are done before reading request body
		content := senderTransport.requestBody
		// TODO: table drive test
		if !strings.Contains(content, "02:30") {
			t.Errorf("content must contain 02:30 due to notification time span")
		}
		if !strings.Contains(content, "06:00") {
			t.Errorf("content must contain 06:00 due to notification time span")
		}
		if strings.Contains(content, "05:00") {
			t.Errorf("content must not contain 23:30 due to notification time span")
		}
		//fmt.Printf("content = %v\n", content)
	})
}

func TestNotifier_Close(t *testing.T) {
	a := assert.New(t)
	r := require.New(t)
	db := helper.DB()
	logger.InitializeAppLogger(os.Stdout, zapcore.DebugLevel)

	fetcherMockTransport, err := fetcher.NewMockTransport("../fetcher/testdata/3986.html")
	r.NoError(err, "fetcher.NewMockTransport failed")
	fetcherHTTPClient := &http.Client{
		Transport: fetcherMockTransport,
	}
	fetcher := fetcher.NewLessonFetcher(fetcherHTTPClient, 1, false, helper.LoadMCountries(), nil)

	senderTransport := &mockSenderTransport{}
	senderHTTPClient := &http.Client{
		Transport: senderTransport,
	}
	sender := emailer.NewSendGridSender(senderHTTPClient)

	user := helper.CreateRandomUser()
	teacher := helper.CreateTeacher(3982, "Hena")
	helper.CreateFollowingTeacher(user.ID, teacher)

	n := NewNotifier(db, fetcher, false, sender)
	err = n.SendNotification(user)
	r.NoError(err, "SendNotification failed")
	n.Close()

	teacherService := model.NewTeacherService(db)
	updatedTeacher, err := teacherService.FindByPK(teacher.ID)
	r.NoError(err)
	a.NotEqual(teacher.CountryID, updatedTeacher.CountryID)
	a.NotEqual(teacher.FavoriteCount, updatedTeacher.FavoriteCount)
	a.NotEqual(teacher.Rating, updatedTeacher.Rating)
	a.NotEqual(teacher.ReviewCount, updatedTeacher.ReviewCount)
}
<|endoftext|>


<commit_before>package s3backend

import (
	"errors"
	"fmt"
	"io"
	"path"

	"code.uber.internal/infra/kraken/core"
	"code.uber.internal/infra/kraken/lib/backend/backenderrors"
	"code.uber.internal/infra/kraken/lib/backend/namepath"
	"code.uber.internal/infra/kraken/utils/log"
	"code.uber.internal/infra/kraken/utils/rwutil"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/awserr"
	"github.com/aws/aws-sdk-go/aws/credentials"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/s3"
	"github.com/aws/aws-sdk-go/service/s3/s3manager"
)

// Client implements a backend.Client for S3.
type Client struct {
	config Config
	pather namepath.Pather
	s3     S3
}

// Option allows setting optional Client parameters.
type Option func(*Client)

// WithS3 configures a Client with a custom S3 implementation.
func WithS3(s3 S3) Option {
	return func(c *Client) { c.s3 = s3 }
}

// NewClient creates a new Client.
func NewClient(config Config, userAuth UserAuthConfig, opts ...Option) (*Client, error) {
	config.applyDefaults()
	if config.Username == "" {
		return nil, errors.New("invalid config: username required")
	}
	if config.Region == "" {
		return nil, errors.New("invalid config: region required")
	}
	if config.Bucket == "" {
		return nil, errors.New("invalid config: bucket required")
	}
	if !path.IsAbs(config.RootDirectory) {
		return nil, errors.New("invalid config: root_directory must be absolute path")
	}

	pather, err := namepath.New(config.RootDirectory, config.NamePath)
	if err != nil {
		return nil, fmt.Errorf("namepath: %s", err)
	}

	auth, ok := userAuth[config.Username]
	if !ok {
		return nil, errors.New("auth not configured for username")
	}
	creds := credentials.NewStaticCredentials(
		auth.S3.AccessKeyID, auth.S3.AccessSecretKey, auth.S3.SessionToken)

	api := s3.New(session.New(), aws.NewConfig().WithRegion(config.Region).WithCredentials(creds))

	downloader := s3manager.NewDownloaderWithClient(api, func(d *s3manager.Downloader) {
		d.PartSize = config.DownloadPartSize
		d.Concurrency = config.DownloadConcurrency
	})

	uploader := s3manager.NewUploaderWithClient(api, func(u *s3manager.Uploader) {
		u.PartSize = config.UploadPartSize
		u.Concurrency = config.UploadConcurrency
	})

	client := &Client{config, pather, join{api, downloader, uploader}}
	for _, opt := range opts {
		opt(client)
	}
	return client, nil
}

// Stat returns blob info for name.
func (c *Client) Stat(name string) (*core.BlobInfo, error) {
	path, err := c.pather.BlobPath(name)
	if err != nil {
		return nil, fmt.Errorf("blob path: %s", err)
	}
	output, err := c.s3.HeadObject(&s3.HeadObjectInput{
		Bucket: aws.String(c.config.Bucket),
		Key:    aws.String(path),
	})
	if err != nil {
		if isNotFound(err) {
			return nil, backenderrors.ErrBlobNotFound
		}
		return nil, err
	}
	var size int64
	if output.ContentLength != nil {
		size = *output.ContentLength
	}
	return core.NewBlobInfo(size), nil
}

// Download downloads the content from a configured bucket and writes the
// data to dst.
func (c *Client) Download(name string, dst io.Writer) error {
	path, err := c.pather.BlobPath(name)
	if err != nil {
		return fmt.Errorf("blob path: %s", err)
	}

	// The S3 download API uses io.WriterAt to perform concurrent chunked download.
	// We attempt to upcast dst to io.WriterAt for this purpose, else we download into
	// in-memory buffer and drain it into dst after the download is finished.
	writerAt, ok := dst.(io.WriterAt)
	if !ok {
		writerAt = rwutil.NewCappedBuffer(int(c.config.BufferGuard))
	}

	input := &s3.GetObjectInput{
		Bucket: aws.String(c.config.Bucket),
		Key:    aws.String(path),
	}
	if _, err := c.s3.Download(writerAt, input); err != nil {
		if isNotFound(err) {
			return backenderrors.ErrBlobNotFound
		}
		return err
	}

	if capBuf, ok := writerAt.(*rwutil.CappedBuffer); ok {
		if err = capBuf.DrainInto(dst); err != nil {
			return err
		}
	}

	return nil
}

// Upload uploads src to a configured bucket.
func (c *Client) Upload(name string, src io.Reader) error {
	path, err := c.pather.BlobPath(name)
	if err != nil {
		return fmt.Errorf("blob path: %s", err)
	}
	input := &s3manager.UploadInput{
		Bucket: aws.String(c.config.Bucket),
		Key:    aws.String(path),
		Body:   src,
	}
	_, err = c.s3.Upload(input, func(u *s3manager.Uploader) {
		u.LeavePartsOnError = false // Delete the parts if the upload fails.
	})
	return err
}

func isNotFound(err error) bool {
	awsErr, ok := err.(awserr.Error)
	return ok && awsErr.Code() == s3.ErrCodeNoSuchKey || awsErr.Code() == "NotFound"
}

// List lists names with start with prefix.
func (c *Client) List(prefix string) ([]string, error) {
	// For whatever reason, the S3 list API does not accept an absolute path
	// for prefix. Thus, the root is stripped from the input and added manually
	// to each output key.
	var names []string
	err := c.s3.ListObjectsPages(&s3.ListObjectsInput{
		Bucket:  aws.String(c.config.Bucket),
		MaxKeys: aws.Int64(int64(c.config.ListMaxKeys)),
		Prefix:  aws.String(path.Join(c.pather.BasePath(), prefix)[1:]),
	}, func(page *s3.ListObjectsOutput, last bool) bool {
		for _, object := range page.Contents {
			if object.Key == nil {
				log.With(
					"prefix", prefix,
					"object", object).Error("List encountered nil S3 object key")
				continue
			}
			name, err := c.pather.NameFromBlobPath(path.Join("/", *object.Key))
			if err != nil {
				log.With("key", *object.Key).Errorf("Error converting blob path into name: %s", err)
				continue
			}
			names = append(names, name)
		}
		return true
	})
	if err != nil {
		return nil, err
	}
	return names, nil
}
<commit_msg>Fix bug detecting s3 not found error<commit_after>package s3backend

import (
	"errors"
	"fmt"
	"io"
	"path"

	"code.uber.internal/infra/kraken/core"
	"code.uber.internal/infra/kraken/lib/backend/backenderrors"
	"code.uber.internal/infra/kraken/lib/backend/namepath"
	"code.uber.internal/infra/kraken/utils/log"
	"code.uber.internal/infra/kraken/utils/rwutil"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/awserr"
	"github.com/aws/aws-sdk-go/aws/credentials"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/s3"
	"github.com/aws/aws-sdk-go/service/s3/s3manager"
)

// Client implements a backend.Client for S3.
type Client struct {
	config Config
	pather namepath.Pather
	s3     S3
}

// Option allows setting optional Client parameters.
type Option func(*Client)

// WithS3 configures a Client with a custom S3 implementation.
func WithS3(s3 S3) Option {
	return func(c *Client) { c.s3 = s3 }
}

// NewClient creates a new Client.
func NewClient(config Config, userAuth UserAuthConfig, opts ...Option) (*Client, error) {
	config.applyDefaults()
	if config.Username == "" {
		return nil, errors.New("invalid config: username required")
	}
	if config.Region == "" {
		return nil, errors.New("invalid config: region required")
	}
	if config.Bucket == "" {
		return nil, errors.New("invalid config: bucket required")
	}
	if !path.IsAbs(config.RootDirectory) {
		return nil, errors.New("invalid config: root_directory must be absolute path")
	}

	pather, err := namepath.New(config.RootDirectory, config.NamePath)
	if err != nil {
		return nil, fmt.Errorf("namepath: %s", err)
	}

	auth, ok := userAuth[config.Username]
	if !ok {
		return nil, errors.New("auth not configured for username")
	}
	creds := credentials.NewStaticCredentials(
		auth.S3.AccessKeyID, auth.S3.AccessSecretKey, auth.S3.SessionToken)

	api := s3.New(session.New(), aws.NewConfig().WithRegion(config.Region).WithCredentials(creds))

	downloader := s3manager.NewDownloaderWithClient(api, func(d *s3manager.Downloader) {
		d.PartSize = config.DownloadPartSize
		d.Concurrency = config.DownloadConcurrency
	})

	uploader := s3manager.NewUploaderWithClient(api, func(u *s3manager.Uploader) {
		u.PartSize = config.UploadPartSize
		u.Concurrency = config.UploadConcurrency
	})

	client := &Client{config, pather, join{api, downloader, uploader}}
	for _, opt := range opts {
		opt(client)
	}
	return client, nil
}

// Stat returns blob info for name.
func (c *Client) Stat(name string) (*core.BlobInfo, error) {
	path, err := c.pather.BlobPath(name)
	if err != nil {
		return nil, fmt.Errorf("blob path: %s", err)
	}
	output, err := c.s3.HeadObject(&s3.HeadObjectInput{
		Bucket: aws.String(c.config.Bucket),
		Key:    aws.String(path),
	})
	if err != nil {
		if isNotFound(err) {
			return nil, backenderrors.ErrBlobNotFound
		}
		return nil, err
	}
	var size int64
	if output.ContentLength != nil {
		size = *output.ContentLength
	}
	return core.NewBlobInfo(size), nil
}

// Download downloads the content from a configured bucket and writes the
// data to dst.
func (c *Client) Download(name string, dst io.Writer) error {
	path, err := c.pather.BlobPath(name)
	if err != nil {
		return fmt.Errorf("blob path: %s", err)
	}

	// The S3 download API uses io.WriterAt to perform concurrent chunked download.
	// We attempt to upcast dst to io.WriterAt for this purpose, else we download into
	// in-memory buffer and drain it into dst after the download is finished.
	writerAt, ok := dst.(io.WriterAt)
	if !ok {
		writerAt = rwutil.NewCappedBuffer(int(c.config.BufferGuard))
	}

	input := &s3.GetObjectInput{
		Bucket: aws.String(c.config.Bucket),
		Key:    aws.String(path),
	}
	if _, err := c.s3.Download(writerAt, input); err != nil {
		if isNotFound(err) {
			return backenderrors.ErrBlobNotFound
		}
		return err
	}

	if capBuf, ok := writerAt.(*rwutil.CappedBuffer); ok {
		if err = capBuf.DrainInto(dst); err != nil {
			return err
		}
	}

	return nil
}

// Upload uploads src to a configured bucket.
func (c *Client) Upload(name string, src io.Reader) error {
	path, err := c.pather.BlobPath(name)
	if err != nil {
		return fmt.Errorf("blob path: %s", err)
	}
	input := &s3manager.UploadInput{
		Bucket: aws.String(c.config.Bucket),
		Key:    aws.String(path),
		Body:   src,
	}
	_, err = c.s3.Upload(input, func(u *s3manager.Uploader) {
		u.LeavePartsOnError = false // Delete the parts if the upload fails.
	})
	return err
}

func isNotFound(err error) bool {
	awsErr, ok := err.(awserr.Error)
	return ok && (awsErr.Code() == s3.ErrCodeNoSuchKey || awsErr.Code() == "NotFound")
}

// List lists names with start with prefix.
func (c *Client) List(prefix string) ([]string, error) {
	// For whatever reason, the S3 list API does not accept an absolute path
	// for prefix. Thus, the root is stripped from the input and added manually
	// to each output key.
	var names []string
	err := c.s3.ListObjectsPages(&s3.ListObjectsInput{
		Bucket:  aws.String(c.config.Bucket),
		MaxKeys: aws.Int64(int64(c.config.ListMaxKeys)),
		Prefix:  aws.String(path.Join(c.pather.BasePath(), prefix)[1:]),
	}, func(page *s3.ListObjectsOutput, last bool) bool {
		for _, object := range page.Contents {
			if object.Key == nil {
				log.With(
					"prefix", prefix,
					"object", object).Error("List encountered nil S3 object key")
				continue
			}
			name, err := c.pather.NameFromBlobPath(path.Join("/", *object.Key))
			if err != nil {
				log.With("key", *object.Key).Errorf("Error converting blob path into name: %s", err)
				continue
			}
			names = append(names, name)
		}
		return true
	})
	if err != nil {
		return nil, err
	}
	return names, nil
}
<|endoftext|>


<commit_before>package main

//go:generate go get -u github.com/jteeuwen/go-bindata/...
//go:generate go-bindata -pkg $GOPACKAGE -o assets.go -prefix assets/ assets/

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"mime"
	"net"
	"net/http"
	"path/filepath"

	"github.com/zserge/webview"
)

func startServer() string {
	ln, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		log.Fatal(err)
	}
	go func() {
		defer ln.Close()
		http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
			path := r.URL.Path
			if len(path) > 0 && path[0] == '/' {
				path = path[1:]
			}
			if path == "" {
				path = "index.html"
			}
			if bs, err := Asset(path); err != nil {
				w.WriteHeader(http.StatusNotFound)
			} else {
				w.Header().Add("Content-Type", mime.TypeByExtension(filepath.Ext(path)))
				io.Copy(w, bytes.NewBuffer(bs))
			}
		})
		log.Fatal(http.Serve(ln, nil))
	}()
	return "http://" + ln.Addr().String()
}

type Task struct {
	Name string `json:"name"`
	Done bool   `json:"done"`
}

var Tasks = []Task{}

func render(w webview.WebView, tasks []Task) {
	b, err := json.Marshal(tasks)
	if err == nil {
		w.Eval(fmt.Sprintf("rpc.render(%s)", string(b)))
	}
}

func handleRPC(w webview.WebView, data string) {
	cmd := struct {
		Name string `json:"cmd"`
	}{}
	if err := json.Unmarshal([]byte(data), &cmd); err != nil {
		log.Println(err)
		return
	}
	switch cmd.Name {
	case "init":
		render(w, Tasks)
	case "log":
		logInfo := struct {
			Text string `json:"text"`
		}{}
		if err := json.Unmarshal([]byte(data), &logInfo); err != nil {
			log.Println(err)
		} else {
			log.Println(logInfo.Text)
		}
	case "addTask":
		task := Task{}
		if err := json.Unmarshal([]byte(data), &task); err != nil {
			log.Println(err)
		} else if len(task.Name) > 0 {
			Tasks = append(Tasks, task)
			render(w, Tasks)
		}
	case "markTask":
		taskInfo := struct {
			Index int  `json:"index"`
			Done  bool `json:"done"`
		}{}
		if err := json.Unmarshal([]byte(data), &taskInfo); err != nil {
			log.Println(err)
		} else if taskInfo.Index >= 0 && taskInfo.Index < len(Tasks) {
			Tasks[taskInfo.Index].Done = taskInfo.Done
			render(w, Tasks)
		}
	case "clearDoneTasks":
		newTasks := []Task{}
		for _, task := range Tasks {
			if !task.Done {
				newTasks = append(newTasks, task)
			}
		}
		Tasks = newTasks
		render(w, Tasks)
	}
}

func main() {
	url := startServer()
	w := webview.New(webview.Settings{
		Width:  320,
		Height: 480,
		Title:  "Todo App",
		URL:    url,
		ExternalInvokeCallback: handleRPC,
	})
	defer w.Exit()
	w.Run()
}
<commit_msg>examples: add comments for exported types to make lint happy<commit_after>package main

//go:generate go get -u github.com/jteeuwen/go-bindata/...
//go:generate go-bindata -pkg $GOPACKAGE -o assets.go -prefix assets/ assets/

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"mime"
	"net"
	"net/http"
	"path/filepath"

	"github.com/zserge/webview"
)

func startServer() string {
	ln, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		log.Fatal(err)
	}
	go func() {
		defer ln.Close()
		http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
			path := r.URL.Path
			if len(path) > 0 && path[0] == '/' {
				path = path[1:]
			}
			if path == "" {
				path = "index.html"
			}
			if bs, err := Asset(path); err != nil {
				w.WriteHeader(http.StatusNotFound)
			} else {
				w.Header().Add("Content-Type", mime.TypeByExtension(filepath.Ext(path)))
				io.Copy(w, bytes.NewBuffer(bs))
			}
		})
		log.Fatal(http.Serve(ln, nil))
	}()
	return "http://" + ln.Addr().String()
}

// Task is a data model type, it contains information about task name and status (done/not done).
type Task struct {
	Name string `json:"name"`
	Done bool   `json:"done"`
}

// Tasks is a global data model, to keep things simple.
var Tasks = []Task{}

func render(w webview.WebView, tasks []Task) {
	b, err := json.Marshal(tasks)
	if err == nil {
		w.Eval(fmt.Sprintf("rpc.render(%s)", string(b)))
	}
}

func handleRPC(w webview.WebView, data string) {
	cmd := struct {
		Name string `json:"cmd"`
	}{}
	if err := json.Unmarshal([]byte(data), &cmd); err != nil {
		log.Println(err)
		return
	}
	switch cmd.Name {
	case "init":
		render(w, Tasks)
	case "log":
		logInfo := struct {
			Text string `json:"text"`
		}{}
		if err := json.Unmarshal([]byte(data), &logInfo); err != nil {
			log.Println(err)
		} else {
			log.Println(logInfo.Text)
		}
	case "addTask":
		task := Task{}
		if err := json.Unmarshal([]byte(data), &task); err != nil {
			log.Println(err)
		} else if len(task.Name) > 0 {
			Tasks = append(Tasks, task)
			render(w, Tasks)
		}
	case "markTask":
		taskInfo := struct {
			Index int  `json:"index"`
			Done  bool `json:"done"`
		}{}
		if err := json.Unmarshal([]byte(data), &taskInfo); err != nil {
			log.Println(err)
		} else if taskInfo.Index >= 0 && taskInfo.Index < len(Tasks) {
			Tasks[taskInfo.Index].Done = taskInfo.Done
			render(w, Tasks)
		}
	case "clearDoneTasks":
		newTasks := []Task{}
		for _, task := range Tasks {
			if !task.Done {
				newTasks = append(newTasks, task)
			}
		}
		Tasks = newTasks
		render(w, Tasks)
	}
}

func main() {
	url := startServer()
	w := webview.New(webview.Settings{
		Width:  320,
		Height: 480,
		Title:  "Todo App",
		URL:    url,
		ExternalInvokeCallback: handleRPC,
	})
	defer w.Exit()
	w.Run()
}
<|endoftext|>


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

package thumbnail

import (
	"fmt"
	"github.com/andreaskoch/allmark2/common/logger"
	"github.com/andreaskoch/allmark2/common/route"
	"github.com/andreaskoch/allmark2/common/util/fsutil"
	"github.com/andreaskoch/allmark2/dataaccess"
	"github.com/andreaskoch/allmark2/services/imageconversion"
	"io"
	"path/filepath"
)

var (
	SizeSmall = ThumbDimension{
		MaxWidth:  200,
		MaxHeight: 300,
	}

	SizeMedium = ThumbDimension{
		MaxWidth:  400,
		MaxHeight: 600,
	}

	SizeLarge = ThumbDimension{
		MaxWidth:  800,
		MaxHeight: 1200,
	}
)

func NewConversionService(logger logger.Logger, repository dataaccess.Repository, thumbnailIndex *Index) *ConversionService {

	// create a new conversion service
	conversionService := &ConversionService{
		logger:     logger,
		repository: repository,

		index:           thumbnailIndex,
		thumbnailFolder: thumbnailIndex.GetThumbnailFolder(),
	}

	// start the conversion
	conversionService.startConversion()

	return conversionService
}

type ConversionService struct {
	logger     logger.Logger
	repository dataaccess.Repository

	index           *Index
	thumbnailFolder string
}

// Start the conversion process.
func (conversion *ConversionService) startConversion() {

	// distinctive update
	conversion.repository.OnUpdate(func(route route.Route) {
		item := conversion.repository.Item(route)
		conversion.createThumbnailsForItem(item)
	})

	// full run
	go conversion.fullConversion()
}

// Process all items in the repository.
func (conversion *ConversionService) fullConversion() {
	for _, item := range conversion.repository.Items() {

		conversion.createThumbnailsForItem(item)

	}
}

// Create thumbnail for all image files found in the supplied item.
func (conversion *ConversionService) createThumbnailsForItem(item *dataaccess.Item) {

	if item == nil {
		return
	}

	for _, file := range item.Files() {

		// create the thumbnails
		conversion.createThumbnailsForFile(file)

	}

}

// Create thumbnail for all image files found in the supplied item.
func (conversion *ConversionService) createThumbnailsForFile(file *dataaccess.File) {

	conversion.createThumbnail(file, SizeSmall)
	conversion.createThumbnail(file, SizeMedium)
	conversion.createThumbnail(file, SizeLarge)

}

// Creates a thumbnail for the supplied file with the specified dimensions.
func (conversion *ConversionService) createThumbnail(file *dataaccess.File, dimensions ThumbDimension) {

	// get the mime type
	mimeType, err := file.MimeType()
	if err != nil {
		conversion.logger.Warn("Unable to detect mime type for file. Error: %s", err.Error())
		return
	}

	// check the mime type
	if !imageconversion.MimeTypeIsSupported(mimeType) {
		conversion.logger.Debug("The mime-type %q is currently not supported.", mimeType)
		return
	}

	// determine the file name
	fileExtension := imageconversion.GetFileExtensionFromMimeType(mimeType)
	filename := fmt.Sprintf("%s-%v-%v.%s", file.Id(), dimensions.MaxWidth, dimensions.MaxHeight, fileExtension)

	// assemble the full file route
	fullFileRoute, err := route.Combine(file.Parent(), file.Route())
	if err != nil {
		conversion.logger.Warn("Unable to combine routes %q and %q.", file.Parent(), file.Route())
		return
	}

	thumb := newThumb(fullFileRoute, conversion.thumbnailFolder, filename, dimensions)

	// check the index
	if conversion.isInIndex(thumb) {
		conversion.logger.Debug("Thumb %q already available in the index", thumb.String())
		return
	}

	// determine the file path
	filePath := filepath.Join(conversion.thumbnailFolder, filename)

	// open the target file
	target, fileError := fsutil.OpenFile(filePath)
	if fileError != nil {
		conversion.logger.Warn("Unable to detect mime type for file. Error: %s", fileError.Error())
		return
	}

	defer target.Close()

	// convert the image
	conversionError := file.Data(func(content io.ReadSeeker) error {
		return imageconversion.Resize(content, mimeType, dimensions.MaxWidth, dimensions.MaxHeight, target)
	})

	// handle errors
	if conversionError != nil {
		conversion.logger.Warn("Unable to create thumbnail for file %q. Error: %s", file, conversionError.Error())
		return
	}

	// add to index
	conversion.addToIndex(thumb)
	conversion.logger.Debug("Adding Thumb %q to index", thumb.String())
}

func (conversion *ConversionService) isInIndex(thumb Thumb) bool {

	// check if there are thumb for the route
	thumbs, entryExists := conversion.index.GetThumbs(thumb.Route)
	if !entryExists {
		return false
	}

	// check if there is a thumb with that dimensions
	if _, thumbExists := thumbs[thumb.Dimensions.String()]; thumbExists {
		// check if the file exists
		thumbnailFilePath := conversion.index.GetThumbnailFilepath(thumb)
		return fsutil.FileExists(thumbnailFilePath)

	}

	return false
}

func (conversion *ConversionService) addToIndex(thumb Thumb) {
	thumbs, entryExists := conversion.index.GetThumbs(thumb.Route)
	if !entryExists {
		thumbs = make(Thumbs)
	}

	thumbs[thumb.Dimensions.String()] = thumb
	conversion.index.SetThumbs(thumb.Route, thumbs)
}
<commit_msg>Changed the thumbnail dimensions according to http://www.smashingmagazine.com/2014/05/14/responsive-images-done-right-guide-picture-srcset/<commit_after>// Copyright 2013 Andreas Koch. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package thumbnail

import (
	"fmt"
	"github.com/andreaskoch/allmark2/common/logger"
	"github.com/andreaskoch/allmark2/common/route"
	"github.com/andreaskoch/allmark2/common/util/fsutil"
	"github.com/andreaskoch/allmark2/dataaccess"
	"github.com/andreaskoch/allmark2/services/imageconversion"
	"io"
	"path/filepath"
)

var (
	SizeSmall = ThumbDimension{
		MaxWidth:  320,
		MaxHeight: 240,
	}

	SizeMedium = ThumbDimension{
		MaxWidth:  640,
		MaxHeight: 480,
	}

	SizeLarge = ThumbDimension{
		MaxWidth:  1024,
		MaxHeight: 768,
	}
)

func NewConversionService(logger logger.Logger, repository dataaccess.Repository, thumbnailIndex *Index) *ConversionService {

	panic(thumbnailIndex.GetThumbnailFolder())

	// create a new conversion service
	conversionService := &ConversionService{
		logger:     logger,
		repository: repository,

		index:           thumbnailIndex,
		thumbnailFolder: thumbnailIndex.GetThumbnailFolder(),
	}

	// start the conversion
	conversionService.startConversion()

	return conversionService
}

type ConversionService struct {
	logger     logger.Logger
	repository dataaccess.Repository

	index           *Index
	thumbnailFolder string
}

// Start the conversion process.
func (conversion *ConversionService) startConversion() {

	// distinctive update
	conversion.repository.OnUpdate(func(route route.Route) {
		item := conversion.repository.Item(route)
		conversion.createThumbnailsForItem(item)
	})

	// full run
	go conversion.fullConversion()
}

// Process all items in the repository.
func (conversion *ConversionService) fullConversion() {
	for _, item := range conversion.repository.Items() {

		conversion.createThumbnailsForItem(item)

	}
}

// Create thumbnail for all image files found in the supplied item.
func (conversion *ConversionService) createThumbnailsForItem(item *dataaccess.Item) {

	if item == nil {
		return
	}

	for _, file := range item.Files() {

		// create the thumbnails
		conversion.createThumbnailsForFile(file)

	}

}

// Create thumbnail for all image files found in the supplied item.
func (conversion *ConversionService) createThumbnailsForFile(file *dataaccess.File) {

	conversion.createThumbnail(file, SizeSmall)
	conversion.createThumbnail(file, SizeMedium)
	conversion.createThumbnail(file, SizeLarge)

}

// Creates a thumbnail for the supplied file with the specified dimensions.
func (conversion *ConversionService) createThumbnail(file *dataaccess.File, dimensions ThumbDimension) {

	// get the mime type
	mimeType, err := file.MimeType()
	if err != nil {
		conversion.logger.Warn("Unable to detect mime type for file. Error: %s", err.Error())
		return
	}

	// check the mime type
	if !imageconversion.MimeTypeIsSupported(mimeType) {
		conversion.logger.Debug("The mime-type %q is currently not supported.", mimeType)
		return
	}

	// determine the file name
	fileExtension := imageconversion.GetFileExtensionFromMimeType(mimeType)
	filename := fmt.Sprintf("%s-%v-%v.%s", file.Id(), dimensions.MaxWidth, dimensions.MaxHeight, fileExtension)

	// assemble the full file route
	fullFileRoute, err := route.Combine(file.Parent(), file.Route())
	if err != nil {
		conversion.logger.Warn("Unable to combine routes %q and %q.", file.Parent(), file.Route())
		return
	}

	thumb := newThumb(fullFileRoute, conversion.thumbnailFolder, filename, dimensions)

	// check the index
	if conversion.isInIndex(thumb) {
		conversion.logger.Debug("Thumb %q already available in the index", thumb.String())
		return
	}

	// determine the file path
	filePath := filepath.Join(conversion.thumbnailFolder, filename)

	// open the target file
	target, fileError := fsutil.OpenFile(filePath)
	if fileError != nil {
		conversion.logger.Warn("Unable to detect mime type for file. Error: %s", fileError.Error())
		return
	}

	defer target.Close()

	// convert the image
	conversionError := file.Data(func(content io.ReadSeeker) error {
		return imageconversion.Resize(content, mimeType, dimensions.MaxWidth, dimensions.MaxHeight, target)
	})

	// handle errors
	if conversionError != nil {
		conversion.logger.Warn("Unable to create thumbnail for file %q. Error: %s", file, conversionError.Error())
		return
	}

	// add to index
	conversion.addToIndex(thumb)
	conversion.logger.Debug("Adding Thumb %q to index", thumb.String())
}

func (conversion *ConversionService) isInIndex(thumb Thumb) bool {

	// check if there are thumb for the route
	thumbs, entryExists := conversion.index.GetThumbs(thumb.Route)
	if !entryExists {
		return false
	}

	// check if there is a thumb with that dimensions
	if _, thumbExists := thumbs[thumb.Dimensions.String()]; thumbExists {
		// check if the file exists
		thumbnailFilePath := conversion.index.GetThumbnailFilepath(thumb)
		return fsutil.FileExists(thumbnailFilePath)

	}

	return false
}

func (conversion *ConversionService) addToIndex(thumb Thumb) {
	thumbs, entryExists := conversion.index.GetThumbs(thumb.Route)
	if !entryExists {
		thumbs = make(Thumbs)
	}

	thumbs[thumb.Dimensions.String()] = thumb
	conversion.index.SetThumbs(thumb.Route, thumbs)
}
<|endoftext|>