<commit_before>package docker

import (
	"bufio"
	"context"
	"encoding/json"
	"github.com/ViBiOh/dashboard/auth"
	"github.com/docker/docker/api/types"
	"github.com/docker/docker/api/types/filters"
	"github.com/gorilla/websocket"
	"log"
	"net/http"
	"regexp"
)

const ignoredByteLogSize = 8
const tailSize = `100`
const start = `start`
const stop = `stop`

var eventsDemand = regexp.MustCompile(`^events`)
var logsDemand = regexp.MustCompile(`^logs (.+)`)
var statsDemand = regexp.MustCompile(`^stats (.+) (.+)`)
var busWebsocketRequest = regexp.MustCompile(`bus`)
var logWebsocketRequest = regexp.MustCompile(`containers/([^/]+)/logs`)
var statsWebsocketRequest = regexp.MustCompile(`containers/([^/]+)/stats`)
var eventsWebsocketRequest = regexp.MustCompile(`events`)

var upgrader = websocket.Upgrader{
	ReadBufferSize:  1024,
	WriteBufferSize: 1024,
	CheckOrigin: func(r *http.Request) bool {
		return hostCheck.MatchString(r.Host)
	},
}

func readUntilClose(user *auth.User, ws *websocket.Conn, name string) bool {
	messageType, _, err := ws.ReadMessage()

	if messageType == websocket.CloseMessage {
		return true
	}

	if err != nil {
		if websocket.IsUnexpectedCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway, websocket.CloseNoStatusReceived, websocket.CloseAbnormalClosure) {
			log.Printf(`[%s] Error while reading from %s socket: %v`, user.Username, name, err)
		}

		return true
	}

	return false
}

func readContent(user *auth.User, ws *websocket.Conn, name string, done chan<- int, content chan<- []byte) {
	for {
		messageType, message, err := ws.ReadMessage()

		if messageType == websocket.CloseMessage {
			close(done)
			return
		}

		if err != nil {
			if websocket.IsUnexpectedCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway, websocket.CloseNoStatusReceived, websocket.CloseAbnormalClosure) {
				log.Printf(`[%s] Error while reading from %s socket: %v`, user.Username, name, err)
			}

			close(done)
			return
		}

		content <- message
	}
}

func upgradeAndAuth(w http.ResponseWriter, r *http.Request) (*websocket.Conn, *auth.User, error) {
	ws, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		defer ws.Close()
		return nil, nil, err
	}

	_, basicAuth, err := ws.ReadMessage()
	if err != nil {
		defer ws.Close()
		return nil, nil, err
	}

	user, err := auth.IsAuthenticatedByAuth(string(basicAuth))
	if err != nil {
		ws.WriteMessage(websocket.TextMessage, []byte(err.Error()))

		defer ws.Close()
		return nil, nil, err
	}

	return ws, user, nil
}

func logsContainerWebsocketHandler(w http.ResponseWriter, r *http.Request, containerID []byte) {
	ws, user, err := upgradeAndAuth(w, r)
	if err != nil {
		log.Print(err)
		return
	}
	defer ws.Close()

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	logs, err := docker.ContainerLogs(ctx, string(containerID), types.ContainerLogsOptions{ShowStdout: true, ShowStderr: true, Follow: true, Tail: tailSize})
	if err != nil {
		log.Print(err)
		return
	}
	defer logs.Close()

	go func() {
		defer cancel()

		scanner := bufio.NewScanner(logs)
		for scanner.Scan() {
			select {
			case <-ctx.Done():
				return

			default:
				logLine := scanner.Bytes()
				if len(logLine) > ignoredByteLogSize {
					if err = ws.WriteMessage(websocket.TextMessage, logLine[ignoredByteLogSize:]); err != nil {
						log.Printf(`[%s] Error while writing to logs socket: %v`, user.Username, err)
						return
					}
				}
			}
		}
	}()

	for {
		select {
		case <-ctx.Done():
			break

		default:
			if readUntilClose(user, ws, `logs`) {
				return
			}
		}
	}
}

func eventsWebsocketHandler(w http.ResponseWriter, r *http.Request) {
	ws, user, err := upgradeAndAuth(w, r)
	if err != nil {
		return
	}
	defer ws.Close()

	filtersArgs := filters.NewArgs()
	if labelFilters(&filtersArgs, user, nil) != nil {
		log.Printf(`[%s] Error while defining label filters: %v`, user.Username, err)
		return
	}
	if eventFilters(&filtersArgs) != nil {
		log.Printf(`[%s] Error while defining event filters: %v`, user.Username, err)
		return
	}

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	messages, errors := docker.Events(ctx, types.EventsOptions{Filters: filtersArgs})

	go func() {
		defer cancel()

		for {
			select {
			case <-ctx.Done():
				return

			case message := <-messages:
				messageJSON, err := json.Marshal(message)
				if err != nil {
					log.Printf(`[%s] Error while marshalling event: %v`, user.Username, err)
					return
				}

				if err = ws.WriteMessage(websocket.TextMessage, messageJSON); err != nil {
					log.Printf(`[%s] Error while writing to events socket: %v`, user.Username, err)
					return
				}

			case err := <-errors:
				log.Printf(`[%s] Error while reading events: %v`, user.Username, err)
				return
			}
		}
	}()

	for {
		select {
		case <-ctx.Done():
			break

		default:
			if readUntilClose(user, ws, `events`) {
				return
			}
		}
	}
}

func statsWebsocketHandler(w http.ResponseWriter, r *http.Request, containerID []byte) {
	ws, user, err := upgradeAndAuth(w, r)
	if err != nil {
		log.Print(err)
		return
	}
	defer ws.Close()

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	stats, err := docker.ContainerStats(ctx, string(containerID), true)
	if err != nil {
		log.Print(err)
		return
	}
	defer stats.Body.Close()

	go func() {
		defer cancel()

		scanner := bufio.NewScanner(stats.Body)
		for scanner.Scan() {
			select {
			case <-ctx.Done():
				log.Printf(`[%s] Stats context is over for writing`, user.Username)
				return

			default:
				if err = ws.WriteMessage(websocket.TextMessage, scanner.Bytes()); err != nil {
					log.Printf(`[%s] Error while writing to stats socket: %v`, user.Username, err)
					return
				}
			}
		}
	}()

	for {
		select {
		case <-ctx.Done():
			log.Printf(`[%s] Stats context is over for reading`, user.Username)
			return

		default:
			if readUntilClose(user, ws, `stats`) {
				return
			}
		}
	}
}

func streamStats(ctx context.Context, user *auth.User, containerID string, output chan<- []byte) {
	stats, err := docker.ContainerStats(ctx, containerID, true)
	if err != nil {
		log.Printf(`[%s] Error while streaming stats: %v`, user.Username, err)
		return
	}
	defer stats.Body.Close()

	scanner := bufio.NewScanner(stats.Body)
	for scanner.Scan() {
		output <- scanner.Bytes()
	}
}

func busWebsocketHandler(w http.ResponseWriter, r *http.Request) {
	ws, user, err := upgradeAndAuth(w, r)
	if err != nil {
		log.Print(err)
		return
	}
	defer ws.Close()

	done := make(chan int)

	output := make(chan []byte)
	defer close(output)

	input := make(chan []byte)
	defer close(input)

	go readContent(user, ws, `streaming`, done, input)
	log.Printf(`[%s] Streaming started`, user.Username)

	var statsCancelFunc context.CancelFunc

	for {
		select {
		case <-done:
			log.Printf(`[%s] Streaming ended`, user.Username)
			return

		case inputBytes := <-input:
			if eventsDemand.Match(inputBytes) {
				log.Printf(`[%s] Streaming events`, user.Username)
			} else if logsDemand.Match(inputBytes) {
				log.Printf(`[%s] Streaming logs for %s`, user.Username, logsDemand.FindSubmatch(inputBytes)[1])
			} else if statsDemand.Match(inputBytes) {
				containerID := statsDemand.FindSubmatch(inputBytes)[1]
				action := string(statsDemand.FindSubmatch(inputBytes)[2])

				if action == stop && statsCancelFunc != nil {
					log.Printf(`[%s] Stopping stats stream`, user.Username)
					statsCancelFunc()
				} else if action == start {
					log.Printf(`[%s] Starting stats stream for %s`, user.Username, containerID)

					if statsCancelFunc != nil {
						log.Printf(`[%s] Cancelling previous stats stream`, user.Username)
						statsCancelFunc()
					}
					statsContext, statsCancelFunc := context.WithCancel(context.Background())
					defer statsCancelFunc()

					streamStats(statsContext, user, string(containerID), output)
				}
			}

		case outputBytes := <-output:
			if err = ws.WriteMessage(websocket.TextMessage, outputBytes); err != nil {
				log.Printf(`[%s] Error while writing to streaming: %v`, user.Username, err)
				close(done)
			}
		}
	}
}

// WebsocketHandler for Docker Websocket request. Should be use with net/http
type WebsocketHandler struct {
}

func (handler WebsocketHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	urlPath := []byte(r.URL.Path)

	if logWebsocketRequest.Match(urlPath) {
		logsContainerWebsocketHandler(w, r, logWebsocketRequest.FindSubmatch(urlPath)[1])
	} else if eventsWebsocketRequest.Match((urlPath)) {
		eventsWebsocketHandler(w, r)
	} else if statsWebsocketRequest.Match(urlPath) {
		statsWebsocketHandler(w, r, statsWebsocketRequest.FindSubmatch(urlPath)[1])
	} else if busWebsocketRequest.Match(urlPath) {
		busWebsocketHandler(w, r)
	}
}
<commit_msg>Update websocket.go<commit_after>package docker

import (
	"bufio"
	"context"
	"encoding/json"
	"github.com/ViBiOh/dashboard/auth"
	"github.com/docker/docker/api/types"
	"github.com/docker/docker/api/types/filters"
	"github.com/gorilla/websocket"
	"log"
	"net/http"
	"regexp"
)

const ignoredByteLogSize = 8
const tailSize = `100`
const start = `start`
const stop = `stop`

var eventsDemand = regexp.MustCompile(`^events`)
var logsDemand = regexp.MustCompile(`^logs (.+)`)
var statsDemand = regexp.MustCompile(`^stats (.+) (.+)`)
var busWebsocketRequest = regexp.MustCompile(`bus`)
var logWebsocketRequest = regexp.MustCompile(`containers/([^/]+)/logs`)
var statsWebsocketRequest = regexp.MustCompile(`containers/([^/]+)/stats`)
var eventsWebsocketRequest = regexp.MustCompile(`events`)

var upgrader = websocket.Upgrader{
	ReadBufferSize:  1024,
	WriteBufferSize: 1024,
	CheckOrigin: func(r *http.Request) bool {
		return hostCheck.MatchString(r.Host)
	},
}

func readUntilClose(user *auth.User, ws *websocket.Conn, name string) bool {
	messageType, _, err := ws.ReadMessage()

	if messageType == websocket.CloseMessage {
		return true
	}

	if err != nil {
		if websocket.IsUnexpectedCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway, websocket.CloseNoStatusReceived, websocket.CloseAbnormalClosure) {
			log.Printf(`[%s] Error while reading from %s socket: %v`, user.Username, name, err)
		}

		return true
	}

	return false
}

func readContent(user *auth.User, ws *websocket.Conn, name string, done chan<- int, content chan<- []byte) {
	for {
		messageType, message, err := ws.ReadMessage()

		if messageType == websocket.CloseMessage {
			close(done)
			return
		}

		if err != nil {
			if websocket.IsUnexpectedCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway, websocket.CloseNoStatusReceived, websocket.CloseAbnormalClosure) {
				log.Printf(`[%s] Error while reading from %s socket: %v`, user.Username, name, err)
			}

			close(done)
			return
		}

		content <- message
	}
}

func upgradeAndAuth(w http.ResponseWriter, r *http.Request) (*websocket.Conn, *auth.User, error) {
	ws, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		defer ws.Close()
		return nil, nil, err
	}

	_, basicAuth, err := ws.ReadMessage()
	if err != nil {
		defer ws.Close()
		return nil, nil, err
	}

	user, err := auth.IsAuthenticatedByAuth(string(basicAuth))
	if err != nil {
		ws.WriteMessage(websocket.TextMessage, []byte(err.Error()))

		defer ws.Close()
		return nil, nil, err
	}

	return ws, user, nil
}

func logsContainerWebsocketHandler(w http.ResponseWriter, r *http.Request, containerID []byte) {
	ws, user, err := upgradeAndAuth(w, r)
	if err != nil {
		log.Print(err)
		return
	}
	defer ws.Close()

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	logs, err := docker.ContainerLogs(ctx, string(containerID), types.ContainerLogsOptions{ShowStdout: true, ShowStderr: true, Follow: true, Tail: tailSize})
	if err != nil {
		log.Print(err)
		return
	}
	defer logs.Close()

	go func() {
		defer cancel()

		scanner := bufio.NewScanner(logs)
		for scanner.Scan() {
			select {
			case <-ctx.Done():
				return

			default:
				logLine := scanner.Bytes()
				if len(logLine) > ignoredByteLogSize {
					if err = ws.WriteMessage(websocket.TextMessage, logLine[ignoredByteLogSize:]); err != nil {
						log.Printf(`[%s] Error while writing to logs socket: %v`, user.Username, err)
						return
					}
				}
			}
		}
	}()

	for {
		select {
		case <-ctx.Done():
			break

		default:
			if readUntilClose(user, ws, `logs`) {
				return
			}
		}
	}
}

func eventsWebsocketHandler(w http.ResponseWriter, r *http.Request) {
	ws, user, err := upgradeAndAuth(w, r)
	if err != nil {
		return
	}
	defer ws.Close()

	filtersArgs := filters.NewArgs()
	if labelFilters(&filtersArgs, user, nil) != nil {
		log.Printf(`[%s] Error while defining label filters: %v`, user.Username, err)
		return
	}
	if eventFilters(&filtersArgs) != nil {
		log.Printf(`[%s] Error while defining event filters: %v`, user.Username, err)
		return
	}

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	messages, errors := docker.Events(ctx, types.EventsOptions{Filters: filtersArgs})

	go func() {
		defer cancel()

		for {
			select {
			case <-ctx.Done():
				return

			case message := <-messages:
				messageJSON, err := json.Marshal(message)
				if err != nil {
					log.Printf(`[%s] Error while marshalling event: %v`, user.Username, err)
					return
				}

				if err = ws.WriteMessage(websocket.TextMessage, messageJSON); err != nil {
					log.Printf(`[%s] Error while writing to events socket: %v`, user.Username, err)
					return
				}

			case err := <-errors:
				log.Printf(`[%s] Error while reading events: %v`, user.Username, err)
				return
			}
		}
	}()

	for {
		select {
		case <-ctx.Done():
			break

		default:
			if readUntilClose(user, ws, `events`) {
				return
			}
		}
	}
}

func statsWebsocketHandler(w http.ResponseWriter, r *http.Request, containerID []byte) {
	ws, user, err := upgradeAndAuth(w, r)
	if err != nil {
		log.Print(err)
		return
	}
	defer ws.Close()

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	stats, err := docker.ContainerStats(ctx, string(containerID), true)
	if err != nil {
		log.Print(err)
		return
	}
	defer stats.Body.Close()

	go func() {
		defer cancel()

		scanner := bufio.NewScanner(stats.Body)
		for scanner.Scan() {
			select {
			case <-ctx.Done():
				log.Printf(`[%s] Stats context is over for writing`, user.Username)
				return

			default:
				if err = ws.WriteMessage(websocket.TextMessage, scanner.Bytes()); err != nil {
					log.Printf(`[%s] Error while writing to stats socket: %v`, user.Username, err)
					return
				}
			}
		}
	}()

	for {
		select {
		case <-ctx.Done():
			log.Printf(`[%s] Stats context is over for reading`, user.Username)
			return

		default:
			if readUntilClose(user, ws, `stats`) {
				return
			}
		}
	}
}

func streamStats(ctx context.Context, user *auth.User, containerID string, output chan<- []byte) {
	stats, err := docker.ContainerStats(ctx, containerID, true)
	if err != nil {
		log.Printf(`[%s] Error while streaming stats: %v`, user.Username, err)
		return
	}
	defer stats.Body.Close()

	scanner := bufio.NewScanner(stats.Body)
	for scanner.Scan() {
		output <- scanner.Bytes()
	}
}

func busWebsocketHandler(w http.ResponseWriter, r *http.Request) {
	ws, user, err := upgradeAndAuth(w, r)
	if err != nil {
		log.Print(err)
		return
	}
	defer ws.Close()

	done := make(chan int)

	output := make(chan []byte)
	defer close(output)

	input := make(chan []byte)
	defer close(input)

	go readContent(user, ws, `streaming`, done, input)
	log.Printf(`[%s] Streaming started`, user.Username)

	var statsCancelFunc context.CancelFunc

	for {
		select {
		case <-done:
			log.Printf(`[%s] Streaming ended`, user.Username)
			return

		case inputBytes := <-input:
			if eventsDemand.Match(inputBytes) {
				log.Printf(`[%s] Streaming events`, user.Username)
			} else if logsDemand.Match(inputBytes) {
				log.Printf(`[%s] Streaming logs for %s`, user.Username, logsDemand.FindSubmatch(inputBytes)[1])
			} else if statsDemand.Match(inputBytes) {
				containerID := statsDemand.FindSubmatch(inputBytes)[1]
				action := string(statsDemand.FindSubmatch(inputBytes)[2])

				if action == stop && statsCancelFunc != nil {
					log.Printf(`[%s] Stopping stats stream`, user.Username)
					statsCancelFunc()
				} else if action == start {
					log.Printf(`[%s] Starting stats stream for %s`, user.Username, containerID)

					if statsCancelFunc != nil {
						log.Printf(`[%s] Cancelling previous stats stream`, user.Username)
						statsCancelFunc()
					}
					statsContext, statsCancelFunc := context.WithCancel(context.Background())
					defer statsCancelFunc()

					streamStats(statsContext, user, string(containerID), output)
				}
			}

		case outputBytes := <-output:
			if err = ws.WriteMessage(websocket.TextMessage, outputBytes); err != nil {
				log.Printf(`[%s] Error while writing to streaming: %v`, user.Username, err)
				close(done)
			}
		}
	}
}

// WebsocketHandler for Docker Websocket request. Should be use with net/http
type WebsocketHandler struct {
}

func (handler WebsocketHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	urlPath := []byte(r.URL.Path)

	if logWebsocketRequest.Match(urlPath) {
		logsContainerWebsocketHandler(w, r, logWebsocketRequest.FindSubmatch(urlPath)[1])
	} else if eventsWebsocketRequest.Match((urlPath)) {
		eventsWebsocketHandler(w, r)
	} else if statsWebsocketRequest.Match(urlPath) {
		statsWebsocketHandler(w, r, statsWebsocketRequest.FindSubmatch(urlPath)[1])
	} else if busWebsocketRequest.Match(urlPath) {
		busWebsocketHandler(w, r)
	}
}
<|endoftext|>


<commit_before>// Copyright 2017 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 parser

import (
	"bytes"
	"strings"

	"github.com/issue9/version"

	"github.com/caixw/apidoc/locale"
	"github.com/caixw/apidoc/openapi"
)

// @api 的格式如下：
//
// @api GET /users/{id}/logs 获取用户信息
// @group g1
// @tags t1,t2
// @version 1.0
// @deprecated desc
// @query page int default desc
// @query size int default desc
// @query state array.string [normal,lock] 状态码
// @param id int desc
// @param id int desc
// @header name desc
// @header name desc
//
// @request application/json object
// @param count int optional desc
// @param list array must desc
// @param list.id int optional desc
// @param list.name int must desc
// @param list.groups array.string optional desc markdown enum:
//  * xx: xxxxx
//  * xx: xxxxx
// @example
// {
//  count: 5,
//  list: [
//    {id:1, name: 'name1', 'groups': [1,2]},
//    {id:2, name: 'name2', 'groups': [1,2]}
//  ]
// }
//
// @request application/yaml object
//
// @response 200 application/json array
// @apiheader string xxx
// @param id int desc
// @param name string desc
// @param group object desc
// @param group.id int desc
//
// @response 404 application/json object
// @apiheader string xxx
// @param code int desc
// @param message string desc
// @param detail array.object desc
// @param detail.id string desc
// @param detail.message string desc

type api struct {
	method      string
	path        string
	summary     string
	description string
	group       string
	tags        []string
	version     string
	deprecated  bool
	params      []*openapi.Parameter // 包含 query 和 param

	request   *openapi.RequestBody
	responses map[string]*openapi.Response
}

var seqaratorDot = []byte{'.'}

func (p *parser) parseAPI(l *lexer) error {
	obj := &api{}
	for tag, eof := l.tag(); !eof; tag, eof = l.tag() {
		switch string(bytes.ToLower(tag.name)) {
		case "@api":
			if obj.method != "" || obj.path != "" || obj.summary != "" {
				return tag.syntaxError(locale.ErrDuplicateTag, "@api")
			}
			data := split(tag.data, 3)
			if len(data) != 3 {
				return tag.syntaxError(locale.ErrTagArgNotEnough, "@api")
			}

			obj.method = strings.ToUpper(string(data[0])) // TODO 验证请求方法
			obj.path = string(data[1])
			obj.summary = string(data[2])

			if err := obj.parseAPI(l); err != nil {
				return err
			}
		case "@apirequest":
			if err := obj.parseRequest(l, tag); err != nil {
				return err
			}
		case "@apiresponse":
			if err := obj.parseResponse(l, tag); err != nil {
				return err
			}
		default:
			return tag.syntaxError(locale.ErrInvalidTag, string(tag.name))
		}
	}
	return nil
}

func (obj *api) parseRequest(l *lexer, tag *tag) error {
	data := split(tag.data, 2)
	if len(data) != 2 {
		return tag.syntaxError(locale.ErrInvalidFormat, "@apiRequest")
	}

	if obj.request == nil {
		obj.request = &openapi.RequestBody{
			Content: make(map[string]*openapi.MediaType, 3),
		}
	}

	mimetype := string(data[0])
	obj.request.Content[mimetype] = &openapi.MediaType{}
	schema := obj.request.Content[mimetype].Schema

	if serr := buildSchema(schema, nil, data[1], nil, nil); serr != nil {
		serr.File = tag.file
		serr.Line = tag.ln
		return serr
	}

	for tag, eof := l.tag(); !eof; tag, eof = l.tag() {
		switch string(bytes.ToLower(tag.name)) {
		case "@apiexample":
			obj.request.Content[mimetype].Example = openapi.ExampleValue(string(tag.data))
		case "@apiparam":
			data := split(tag.data, 4)
			if len(data) != 4 {
				return tag.syntaxError(locale.ErrInvalidFormat, "@apiParam")
			}

			if serr := buildSchema(schema, data[0], data[1], data[2], data[3]); serr != nil {
				serr.File = tag.file
				serr.Line = tag.ln
				return serr
			}
		default:
			l.backup(tag)
			return nil
		}
	}

	return nil
}

func (obj *api) parseResponse(l *lexer, tag *tag) error {
	data := split(tag.data, 3)
	if len(data) != 3 {
		return tag.syntaxError(locale.ErrInvalidFormat, "@apiResponse")
	}
	status := string(data[0])
	mimetype := string(data[1])

	if obj.responses == nil {
		obj.responses = make(map[string]*openapi.Response, 10)
	}

	obj.responses[status] = &openapi.Response{
		Content: make(map[string]*openapi.MediaType, 2),
	}
	schema := obj.responses[status].Content[mimetype].Schema

	if serr := buildSchema(schema, nil, data[1], nil, nil); serr != nil {
		serr.File = tag.file
		serr.Line = tag.ln
		return serr
	}

	for tag, eof := l.tag(); !eof; tag, eof = l.tag() {
		switch string(bytes.ToLower(tag.name)) {
		case "@apiexample":
			obj.responses[status].Content[mimetype].Example = openapi.ExampleValue(string(tag.data))

		case "@apiheader":
			data := split(tag.data, 2)
			if len(data) != 2 {
				return tag.syntaxError(locale.ErrInvalidFormat, "@apiHeader")
			}

			if obj.responses[status].Headers == nil {
				obj.responses[status].Headers = make(map[string]*openapi.Header, 2)
			}

			obj.responses[status].Headers[string(data[0])] = &openapi.Header{
				Description: openapi.Description(data[1]),
			}
		case "@apiparam":
			data := split(tag.data, 4)
			if len(data) != 4 {
				return tag.syntaxError(locale.ErrInvalidFormat, "@apiParam")
			}

			if serr := buildSchema(schema, data[0], data[1], data[2], data[3]); serr != nil {
				serr.File = tag.file
				serr.Line = tag.ln
				return serr
			}
		default:
			l.backup(tag)
			return nil
		}
	}

	return nil
}

func (obj *api) parseAPI(l *lexer) error {
	for tag, eof := l.tag(); !eof; tag, eof = l.tag() {
		switch string(bytes.ToLower(tag.name)) {
		case "@apigroup":
			if obj.group != "" {
				return tag.syntaxError(locale.ErrDuplicateTag, "@apiGroup")
			}
			obj.group = string(tag.data)
		case "@apitags":
			if len(obj.tags) > 0 {
				return tag.syntaxError(locale.ErrDuplicateTag, "@apiTags")
			}

			data := tag.data
			start := 0
			for {
				index := bytes.IndexByte(tag.data, ',')

				if index <= 0 {
					obj.tags = append(obj.tags, string(data[start:]))
					break
				}

				obj.tags = append(obj.tags, string(data[start:index]))
				data = tag.data[index+1:]
			}
		case "@apiversion":
			if obj.version != "" {
				return tag.syntaxError(locale.ErrDuplicateTag, "@apiVersion")
			}
			obj.version = string(tag.data)

			if !version.SemVerValid(obj.version) {
				return tag.syntaxError(locale.ErrInvalidFormat, "@apiVersion")
			}
		case "@apideprecated":
			// TODO 输出警告信息
			obj.deprecated = true
		case "@apiquery":
			if obj.params == nil {
				obj.params = make([]*openapi.Parameter, 0, 10)
			}
			// TODO 复杂类型的检测 @apiquery state string.array.enum default desc
			params := split(tag.data, 4)
			if len(params) != 4 {
				return tag.syntaxError(locale.ErrTagArgNotEnough, "@apiQuery")
			}

			obj.params = append(obj.params, &openapi.Parameter{
				Name:            string(params[0]),
				IN:              openapi.ParameterINQuery,
				Description:     openapi.Description(params[3]),
				Required:        false,
				AllowEmptyValue: true,
				Schema: &openapi.Schema{
					Type:    string(params[1]), // TODO 检测类型是否符合 openapi 要求
					Default: string(params[2]),
				},
			})
		case "@apiparam":
			if obj.params == nil {
				obj.params = make([]*openapi.Parameter, 0, 10)
			}

			params := split(tag.data, 4)
			if len(params) != 4 {
				return tag.syntaxError(locale.ErrTagArgNotEnough, "@apiParam")
			}

			obj.params = append(obj.params, &openapi.Parameter{
				Name:        string(params[0]),
				IN:          openapi.ParameterINPath,
				Description: openapi.Description(params[3]),
				Required:    true,
				Schema: &openapi.Schema{
					Type:    string(params[1]), // TODO 检测类型是否符合 openapi 要求
					Default: string(params[2]),
				},
			})
		case "@apiheader":
			if obj.params == nil {
				obj.params = make([]*openapi.Parameter, 0, 10)
			}

			params := split(tag.data, 4)
			if len(params) != 4 {
				return tag.syntaxError(locale.ErrTagArgNotEnough, "@apiHeader")
			}

			obj.params = append(obj.params, &openapi.Parameter{
				Name:            string(params[0]),
				IN:              openapi.ParameterINHeader,
				Description:     openapi.Description(params[3]),
				Required:        false,
				AllowEmptyValue: true,
			})
		default:
			l.backup(tag)
			return nil
		}
	}
	return nil
}
<commit_msg>完成 parser 转换<commit_after>// Copyright 2017 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 parser

import (
	"bytes"
	"net/http"
	"strings"

	"github.com/caixw/apidoc/locale"
	"github.com/caixw/apidoc/openapi"
)

// @api 的格式如下：
//
// @api GET /users/{id}/logs 获取用户信息
// @group g1
// @tags t1,t2
// @deprecated desc
// @query page int default desc
// @query size int default desc
// @query state array.string [normal,lock] 状态码
// @param id int desc
// @param id int desc
// @header name desc
// @header name desc
//
// @request application/json object
// @param count int optional desc
// @param list array must desc
// @param list.id int optional desc
// @param list.name int must desc
// @param list.groups array.string optional desc markdown enum:
//  * xx: xxxxx
//  * xx: xxxxx
// @example
// {
//  count: 5,
//  list: [
//    {id:1, name: 'name1', 'groups': [1,2]},
//    {id:2, name: 'name2', 'groups': [1,2]}
//  ]
// }
//
// @request application/yaml object
//
// @response 200 application/json array.object
// @apiheader string xxx
// @param id int desc
// @param name string desc
// @param group object desc
// @param group.id int desc
//
// @response 404 application/json object
// @apiheader string xxx
// @param code int desc
// @param message string desc
// @param detail array.object desc
// @param detail.id string desc
// @param detail.message string desc

type api struct {
	method      string
	path        string
	summary     string
	description string
	group       string
	tags        []string
	deprecated  bool
	params      []*openapi.Parameter // 包含 query 和 param

	request   *openapi.RequestBody
	responses map[string]*openapi.Response
}

var seqaratorDot = []byte{'.'}

func (p *parser) parseAPI(l *lexer) error {
	obj := &api{}
	for tag, eof := l.tag(); !eof; tag, eof = l.tag() {
		switch string(bytes.ToLower(tag.name)) {
		case "@api":
			if obj.method != "" || obj.path != "" || obj.summary != "" {
				return tag.syntaxError(locale.ErrDuplicateTag, "@api")
			}
			data := split(tag.data, 3)
			if len(data) != 3 {
				return tag.syntaxError(locale.ErrTagArgNotEnough, "@api")
			}

			obj.method = strings.ToUpper(string(data[0])) // TODO 验证请求方法
			obj.path = string(data[1])
			obj.summary = string(data[2])

			if err := obj.parseAPI(l); err != nil {
				return err
			}
		case "@apirequest":
			if err := obj.parseRequest(l, tag); err != nil {
				return err
			}
		case "@apiresponse":
			if err := obj.parseResponse(l, tag); err != nil {
				return err
			}
		default:
			return tag.syntaxError(locale.ErrInvalidTag, string(tag.name))
		}
	}

	return p.fromAPI(obj, l.data.File, l.data.Line)
}

func (p *parser) fromAPI(api *api, file string, line int) error {
	doc := p.getDoc(api.group)
	doc.locker.Lock()
	defer doc.locker.Unlock()

	path := doc.OpenAPI.Paths[api.path]
	if path == nil {
		path = &openapi.PathItem{}
		doc.OpenAPI.Paths[api.path] = path
	}

	op := &openapi.Operation{}
	switch strings.ToUpper(api.method) {
	case http.MethodGet:
		if path.Get != nil {
			return &syntaxError{File: file, Line: line, MessageKey: locale.ErrMethodExists}
		}
		path.Get = op
	case http.MethodPost:
		if path.Post != nil {
			return &syntaxError{File: file, Line: line, MessageKey: locale.ErrMethodExists}
		}
		path.Post = op
	case http.MethodPut:
		if path.Put != nil {
			return &syntaxError{File: file, Line: line, MessageKey: locale.ErrMethodExists}
		}
		path.Put = op
	case http.MethodPatch:
		if path.Patch != nil {
			return &syntaxError{File: file, Line: line, MessageKey: locale.ErrMethodExists}
		}
		path.Patch = op
	case http.MethodDelete:
		if path.Delete != nil {
			return &syntaxError{File: file, Line: line, MessageKey: locale.ErrMethodExists}
		}
		path.Delete = op
	case http.MethodOptions:
		if path.Options != nil {
			return &syntaxError{File: file, Line: line, MessageKey: locale.ErrMethodExists}
		}
		path.Options = op
	case http.MethodTrace:
		if path.Trace != nil {
			return &syntaxError{File: file, Line: line, MessageKey: locale.ErrMethodExists}
		}
		path.Trace = op
	default:
		return &syntaxError{File: file, Line: line, MessageKey: locale.ErrInvalidMethod}
	}

	op.Tags = api.tags
	op.Summary = api.summary
	op.Responses = api.responses
	op.RequestBody = api.request
	op.Parameters = api.params
	op.Deprecated = api.deprecated
	op.Description = openapi.Description(api.description)

	return nil
}

func (obj *api) parseRequest(l *lexer, tag *tag) error {
	data := split(tag.data, 2)
	if len(data) != 2 {
		return tag.syntaxError(locale.ErrInvalidFormat, "@apiRequest")
	}

	if obj.request == nil {
		obj.request = &openapi.RequestBody{
			Content: make(map[string]*openapi.MediaType, 3),
		}
	}

	mimetype := string(data[0])
	obj.request.Content[mimetype] = &openapi.MediaType{}
	schema := obj.request.Content[mimetype].Schema

	if serr := buildSchema(schema, nil, data[1], nil, nil); serr != nil {
		serr.File = tag.file
		serr.Line = tag.ln
		return serr
	}

	for tag, eof := l.tag(); !eof; tag, eof = l.tag() {
		switch string(bytes.ToLower(tag.name)) {
		case "@apiexample":
			obj.request.Content[mimetype].Example = openapi.ExampleValue(string(tag.data))
		case "@apiparam":
			data := split(tag.data, 4)
			if len(data) != 4 {
				return tag.syntaxError(locale.ErrInvalidFormat, "@apiParam")
			}

			if serr := buildSchema(schema, data[0], data[1], data[2], data[3]); serr != nil {
				serr.File = tag.file
				serr.Line = tag.ln
				return serr
			}
		default:
			l.backup(tag)
			return nil
		}
	}

	return nil
}

func (obj *api) parseResponse(l *lexer, tag *tag) error {
	data := split(tag.data, 3)
	if len(data) != 3 {
		return tag.syntaxError(locale.ErrInvalidFormat, "@apiResponse")
	}
	status := string(data[0])
	mimetype := string(data[1])

	if obj.responses == nil {
		obj.responses = make(map[string]*openapi.Response, 10)
	}

	obj.responses[status] = &openapi.Response{
		Content: make(map[string]*openapi.MediaType, 2),
	}
	schema := obj.responses[status].Content[mimetype].Schema

	if serr := buildSchema(schema, nil, data[1], nil, nil); serr != nil {
		serr.File = tag.file
		serr.Line = tag.ln
		return serr
	}

	for tag, eof := l.tag(); !eof; tag, eof = l.tag() {
		switch string(bytes.ToLower(tag.name)) {
		case "@apiexample":
			obj.responses[status].Content[mimetype].Example = openapi.ExampleValue(string(tag.data))

		case "@apiheader":
			data := split(tag.data, 2)
			if len(data) != 2 {
				return tag.syntaxError(locale.ErrInvalidFormat, "@apiHeader")
			}

			if obj.responses[status].Headers == nil {
				obj.responses[status].Headers = make(map[string]*openapi.Header, 2)
			}

			obj.responses[status].Headers[string(data[0])] = &openapi.Header{
				Description: openapi.Description(data[1]),
			}
		case "@apiparam":
			data := split(tag.data, 4)
			if len(data) != 4 {
				return tag.syntaxError(locale.ErrInvalidFormat, "@apiParam")
			}

			if serr := buildSchema(schema, data[0], data[1], data[2], data[3]); serr != nil {
				serr.File = tag.file
				serr.Line = tag.ln
				return serr
			}
		default:
			l.backup(tag)
			return nil
		}
	}

	return nil
}

func (obj *api) parseAPI(l *lexer) error {
	for tag, eof := l.tag(); !eof; tag, eof = l.tag() {
		switch string(bytes.ToLower(tag.name)) {
		case "@apigroup":
			if obj.group != "" {
				return tag.syntaxError(locale.ErrDuplicateTag, "@apiGroup")
			}
			obj.group = string(tag.data)
		case "@apitags":
			if len(obj.tags) > 0 {
				return tag.syntaxError(locale.ErrDuplicateTag, "@apiTags")
			}

			data := tag.data
			start := 0
			for {
				index := bytes.IndexByte(tag.data, ',')

				if index <= 0 {
					obj.tags = append(obj.tags, string(data[start:]))
					break
				}

				obj.tags = append(obj.tags, string(data[start:index]))
				data = tag.data[index+1:]
			}
		case "@apideprecated":
			// TODO 输出警告信息
			obj.deprecated = true
		case "@apiquery":
			if obj.params == nil {
				obj.params = make([]*openapi.Parameter, 0, 10)
			}

			params := split(tag.data, 4)
			if len(params) != 4 {
				return tag.syntaxError(locale.ErrTagArgNotEnough, "@apiQuery")
			}

			obj.params = append(obj.params, &openapi.Parameter{
				Name:            string(params[0]),
				IN:              openapi.ParameterINQuery,
				Description:     openapi.Description(params[3]),
				Required:        false,
				AllowEmptyValue: true,
				Schema: &openapi.Schema{
					Type:    string(params[1]), // TODO 检测类型是否符合 openapi 要求
					Default: string(params[2]),
				},
			})
		case "@apiparam":
			if obj.params == nil {
				obj.params = make([]*openapi.Parameter, 0, 10)
			}

			params := split(tag.data, 4)
			if len(params) != 4 {
				return tag.syntaxError(locale.ErrTagArgNotEnough, "@apiParam")
			}

			obj.params = append(obj.params, &openapi.Parameter{
				Name:        string(params[0]),
				IN:          openapi.ParameterINPath,
				Description: openapi.Description(params[3]),
				Required:    true,
				Schema: &openapi.Schema{
					Type:    string(params[1]), // TODO 检测类型是否符合 openapi 要求
					Default: string(params[2]),
				},
			})
		case "@apiheader":
			if obj.params == nil {
				obj.params = make([]*openapi.Parameter, 0, 10)
			}

			params := split(tag.data, 4)
			if len(params) != 4 {
				return tag.syntaxError(locale.ErrTagArgNotEnough, "@apiHeader")
			}

			obj.params = append(obj.params, &openapi.Parameter{
				Name:            string(params[0]),
				IN:              openapi.ParameterINHeader,
				Description:     openapi.Description(params[3]),
				Required:        false,
				AllowEmptyValue: true,
			})
		default:
			l.backup(tag)
			return nil
		}
	}
	return nil
}
<|endoftext|>


<commit_before>package jsonrpc

import (
	"encoding/json"
	"io"
	"sync"
)

// A Connection is a JSON-RPC channel over an existing ReadWriter, such as a socket.
type Connection struct {
	dialect Dialect
	enc     *json.Encoder
	dec     *json.Decoder

	outbox chan map[string]interface{} // outgoing requests and notifications

	lock        sync.Mutex
	nextID      int                   // id to use for the next request
	resultChans map[int]chan Response // request id -> result channel
	stop        chan struct{}         // will be closed on error
	err         error                 // holds any error encountered on read or write
}

// A Dialect is a specific wire encoding of requests and notifications.
type Dialect struct {
	// The RequestMap maps parameters sent a request function (ID, Method,
	// Params) to a map that is JSON marshalled over the wire.
	RequestMap func(int, string, []interface{}) map[string]interface{}
	// The NotificationMap maps parameters sent a request function (Method,
	// Params) to a map that is JSON marshalled over the wire.
	NotificationMap func(string, []interface{}) map[string]interface{}
}

// A Response is a JSON-RPC 2.0 response as received by the server in response
// to a request.
type Response struct {
	ID      int            // Sequence number autogenerated by the request function
	Result  interface{}    // Result of a successfull request; may be nil.
	Error   *ResponseError // Error of an unsuccessfull (but successfully sent) request; may be nil.
	JSONRPC string         // JSON-RPC version string as returned by the server (most likely "2.0" or blank).
}

// A ResponseError is an error object returned by the JSON-RPC server in
// response to unsuccessfull or erroneous requests.
type ResponseError struct {
	Code    int
	Message string
	Data    interface{}
}

// NewConnection creates a new JSON-RPC channel over the specified ReadWriter,
// using a specific Dialect to encode the transmitted data.
func NewConnection(rw io.ReadWriter, dialect Dialect) *Connection {
	enc := json.NewEncoder(rw)
	dec := json.NewDecoder(rw)
	conn := &Connection{
		dialect:     dialect,
		enc:         enc,
		dec:         dec,
		outbox:      make(chan map[string]interface{}),
		stop:        make(chan struct{}),
		resultChans: make(map[int]chan Response),
	}
	go conn.writer()
	go conn.reader()
	return conn
}

/*
A Request function performs a request on the JSON-RPC server when called and
returns a channel where the response may be read.  A nil channel and an error
is returned if the request could not be sent due to communication problems.

The response object that is sent on the channel may contain an error from the
server. The result channel is closed in the case of a read error after sending
the request but before recieving the response.

The meaning of the arguments to the request function is defined by the Dialect,
but in the standard dialect these are simply sent verbatim as the method
arguments.
*/
type Request func(...interface{}) (<-chan Response, error)

// Request returns a new request function for use over the channel.
func (c *Connection) Request(method string) Request {
	return func(params ...interface{}) (<-chan Response, error) {
		res := make(chan Response, 1)

		c.lock.Lock()
		id := c.nextID
		c.resultChans[id] = res
		c.nextID++
		c.lock.Unlock()

		select {
		case <-c.stop:
			// The stop channel has been closed due to an error.
			c.lock.Lock()
			defer c.lock.Unlock()
			return nil, c.err
		case c.outbox <- c.dialect.RequestMap(id, method, params):
			return res, nil
		}
	}
}

// A Notification function performs a notification on the JSON-RPC server when
// called. An error is returned if the request could not be sent due to
// communication problems. The meaning of the arguments to the request function
// is defined by the Dialect, but in the standard dialect these are simply sent
// verbatim as the method arguments.
type Notification func(...interface{}) error

// Notification returns a new notification function for use over the channel.
func (c *Connection) Notification(method string) Notification {
	return func(params ...interface{}) error {
		select {
		case <-c.stop:
			// The stop channel has been closed due to an error.
			c.lock.Lock()
			defer c.lock.Unlock()
			return c.err
		default:
			c.outbox <- c.dialect.NotificationMap(method, params)
			return nil
		}
	}
}

func (c *Connection) writer() {
	for {
		select {
		case <-c.stop:
			// stop channel has been closed due to an error, time to bail
			return
		case wc := <-c.outbox:
			err := c.enc.Encode(wc)
			if err != nil {
				c.lock.Lock()
				defer c.lock.Unlock()
				c.err = err
				close(c.stop)
				return
			}
		}
	}
}

func (c *Connection) reader() {
	defer func() {

		// If we have any outstanding requests pending when the reader exits
		// due to an error, close those channels to signal that there will
		// never be a response and to avoid a deadlock.

		for k, v := range c.resultChans {
			println("warning: shutting down, cancelling lost response for id", k)
			close(v)
		}
	}()

	for {
		var res Response
		err := c.dec.Decode(&res)
		if err != nil {
			c.lock.Lock()
			defer c.lock.Unlock()
			c.err = err
			close(c.stop)
			return
		}

		c.lock.Lock()
		ch, ok := c.resultChans[res.ID]
		delete(c.resultChans, res.ID)
		c.lock.Unlock()

		if !ok {
			println("warning: result for unsent id", res.ID, "(reader out of sync with reality)")
			continue
		}

		ch <- res
	}
}
<commit_msg>Like I said, deprecated<commit_after>package jsonrpc // import "github.com/calmh/deprecated_jsonrpc"

import (
	"encoding/json"
	"io"
	"sync"
)

// A Connection is a JSON-RPC channel over an existing ReadWriter, such as a socket.
type Connection struct {
	dialect Dialect
	enc     *json.Encoder
	dec     *json.Decoder

	outbox chan map[string]interface{} // outgoing requests and notifications

	lock        sync.Mutex
	nextID      int                   // id to use for the next request
	resultChans map[int]chan Response // request id -> result channel
	stop        chan struct{}         // will be closed on error
	err         error                 // holds any error encountered on read or write
}

// A Dialect is a specific wire encoding of requests and notifications.
type Dialect struct {
	// The RequestMap maps parameters sent a request function (ID, Method,
	// Params) to a map that is JSON marshalled over the wire.
	RequestMap func(int, string, []interface{}) map[string]interface{}
	// The NotificationMap maps parameters sent a request function (Method,
	// Params) to a map that is JSON marshalled over the wire.
	NotificationMap func(string, []interface{}) map[string]interface{}
}

// A Response is a JSON-RPC 2.0 response as received by the server in response
// to a request.
type Response struct {
	ID      int            // Sequence number autogenerated by the request function
	Result  interface{}    // Result of a successfull request; may be nil.
	Error   *ResponseError // Error of an unsuccessfull (but successfully sent) request; may be nil.
	JSONRPC string         // JSON-RPC version string as returned by the server (most likely "2.0" or blank).
}

// A ResponseError is an error object returned by the JSON-RPC server in
// response to unsuccessfull or erroneous requests.
type ResponseError struct {
	Code    int
	Message string
	Data    interface{}
}

// NewConnection creates a new JSON-RPC channel over the specified ReadWriter,
// using a specific Dialect to encode the transmitted data.
func NewConnection(rw io.ReadWriter, dialect Dialect) *Connection {
	enc := json.NewEncoder(rw)
	dec := json.NewDecoder(rw)
	conn := &Connection{
		dialect:     dialect,
		enc:         enc,
		dec:         dec,
		outbox:      make(chan map[string]interface{}),
		stop:        make(chan struct{}),
		resultChans: make(map[int]chan Response),
	}
	go conn.writer()
	go conn.reader()
	return conn
}

/*
A Request function performs a request on the JSON-RPC server when called and
returns a channel where the response may be read.  A nil channel and an error
is returned if the request could not be sent due to communication problems.

The response object that is sent on the channel may contain an error from the
server. The result channel is closed in the case of a read error after sending
the request but before recieving the response.

The meaning of the arguments to the request function is defined by the Dialect,
but in the standard dialect these are simply sent verbatim as the method
arguments.
*/
type Request func(...interface{}) (<-chan Response, error)

// Request returns a new request function for use over the channel.
func (c *Connection) Request(method string) Request {
	return func(params ...interface{}) (<-chan Response, error) {
		res := make(chan Response, 1)

		c.lock.Lock()
		id := c.nextID
		c.resultChans[id] = res
		c.nextID++
		c.lock.Unlock()

		select {
		case <-c.stop:
			// The stop channel has been closed due to an error.
			c.lock.Lock()
			defer c.lock.Unlock()
			return nil, c.err
		case c.outbox <- c.dialect.RequestMap(id, method, params):
			return res, nil
		}
	}
}

// A Notification function performs a notification on the JSON-RPC server when
// called. An error is returned if the request could not be sent due to
// communication problems. The meaning of the arguments to the request function
// is defined by the Dialect, but in the standard dialect these are simply sent
// verbatim as the method arguments.
type Notification func(...interface{}) error

// Notification returns a new notification function for use over the channel.
func (c *Connection) Notification(method string) Notification {
	return func(params ...interface{}) error {
		select {
		case <-c.stop:
			// The stop channel has been closed due to an error.
			c.lock.Lock()
			defer c.lock.Unlock()
			return c.err
		default:
			c.outbox <- c.dialect.NotificationMap(method, params)
			return nil
		}
	}
}

func (c *Connection) writer() {
	for {
		select {
		case <-c.stop:
			// stop channel has been closed due to an error, time to bail
			return
		case wc := <-c.outbox:
			err := c.enc.Encode(wc)
			if err != nil {
				c.lock.Lock()
				defer c.lock.Unlock()
				c.err = err
				close(c.stop)
				return
			}
		}
	}
}

func (c *Connection) reader() {
	defer func() {

		// If we have any outstanding requests pending when the reader exits
		// due to an error, close those channels to signal that there will
		// never be a response and to avoid a deadlock.

		for k, v := range c.resultChans {
			println("warning: shutting down, cancelling lost response for id", k)
			close(v)
		}
	}()

	for {
		var res Response
		err := c.dec.Decode(&res)
		if err != nil {
			c.lock.Lock()
			defer c.lock.Unlock()
			c.err = err
			close(c.stop)
			return
		}

		c.lock.Lock()
		ch, ok := c.resultChans[res.ID]
		delete(c.resultChans, res.ID)
		c.lock.Unlock()

		if !ok {
			println("warning: result for unsent id", res.ID, "(reader out of sync with reality)")
			continue
		}

		ch <- res
	}
}
<|endoftext|>


<commit_before>package main

import (
	"bytes"
	"flag"
	"html/template"
	"io/ioutil"
	"log"
	"net/http"
	"os/exec"
	"strconv"
	"strings"
)

type Album struct {
	Title, Artist, Folder string
}

var albums []Album

var html *template.Template

func handler(w http.ResponseWriter, r *http.Request) {
	if r.URL.Path == "/favicon.ico" {
		http.NotFound(w, r)
		return
	}

	if r.FormValue("u") != "" {
		ioutil.WriteFile("/tmp/mplayer", []byte("loadlist "+r.FormValue("u")+"\n"), 0644)
	} else if r.FormValue("f") != "" {
		ioutil.WriteFile("/tmp/mplayer", []byte("loadfile '"+r.FormValue("f")+"'\n"), 0644)
	} else if r.FormValue("d") != "" {
		folder := r.FormValue("d")
		cmd := exec.Command("find", folder, "-type", "f")
		var out bytes.Buffer
		cmd.Stdout = &out
		cmd.Run()
		playlist, _ := ioutil.TempFile("", "jukebox")
		ioutil.WriteFile(playlist.Name(), []byte(out.String()), 0644)
		ioutil.WriteFile("/tmp/mplayer", []byte("loadlist '"+playlist.Name()+"'\n"), 0644)
	} else if r.FormValue("c") != "" {
		ioutil.WriteFile("/tmp/mplayer", []byte(r.FormValue("c")+"\n"), 0644)
	}

	html.Execute(w, albums)
}

func buildTemplates() {
	const _html = `<!DOCTYPE html>
<title>jukebox</title><meta name="viewport" content="width=device-width, initial-scale=1.0">

Stop

Radio 4

Radio 5 live

Groove Salad
  {{range .}}
{{.Artist}} - {{.Title}}{{end}}

`
	html = template.Must(template.New("html").Parse(_html))
}

func findAlbums(root string) {
	cmd := exec.Command("find", root, "-mindepth", "2", "-maxdepth", "2", "-type", "d")
	output, err := cmd.Output()
	if err != nil {
		log.Fatal("findAlbums: ", err)
	}
	lines := strings.Split(string(output), "\n")

	albums = make([]Album, len(lines)-1)
	for i, line := range lines {
		if len(line) > 0 {
			parts := strings.Split(line, "/")
			albums[i] = Album{
				parts[len(parts)-1],
				parts[len(parts)-2],
				line,
			}
		}
	}
}

func main() {
	port := flag.Int("port", 80, "port")
	root := flag.String("root", "", "root")
	flag.Parse()
	if *root == "" {
		panic("root required")
	}

	buildTemplates()
	findAlbums(*root)

	http.HandleFunc("/", handler)
	err := http.ListenAndServe(":"+strconv.Itoa(*port), nil)
	if err != nil {
		log.Fatal("ListenAndServe: ", err)
	}
}
<commit_msg>Spawn Mplayer<commit_after>package main

import (
	"bytes"
	"flag"
	"html/template"
	"io"
	"io/ioutil"
	"log"
	"net/http"
	"os/exec"
	"strconv"
	"strings"
)

type Album struct {
	Title, Artist, Folder string
}

var albums []Album

var html *template.Template

var mplayer io.Writer

func handler(w http.ResponseWriter, r *http.Request) {
	if r.URL.Path == "/favicon.ico" {
		http.NotFound(w, r)
		return
	}

	if r.FormValue("u") != "" {
		mplayer.Write([]byte("loadlist " + r.FormValue("u") + "\n"))
	} else if r.FormValue("f") != "" {
		mplayer.Write([]byte("loadfile '" + r.FormValue("f") + "'\n"))
	} else if r.FormValue("d") != "" {
		folder := r.FormValue("d")
		cmd := exec.Command("find", folder, "-type", "f")
		var out bytes.Buffer
		cmd.Stdout = &out
		cmd.Run()
		playlist, _ := ioutil.TempFile("", "jukebox")
		ioutil.WriteFile(playlist.Name(), []byte(out.String()), 0644)
		mplayer.Write([]byte("loadlist '" + playlist.Name() + "'\n"))
	} else if r.FormValue("c") != "" {
		mplayer.Write([]byte(r.FormValue("c") + "\n"))
	}

	html.Execute(w, albums)
}

func buildTemplates() {
	const _html = `<!DOCTYPE html>
<title>jukebox</title><meta name="viewport" content="width=device-width, initial-scale=1.0">

Stop

Radio 4

Radio 5 live

Groove Salad
  {{range .}}
{{.Artist}} - {{.Title}}{{end}}

`
	html = template.Must(template.New("html").Parse(_html))
}

func findAlbums(root string) {
	cmd := exec.Command("find", root, "-mindepth", "2", "-maxdepth", "2", "-type", "d")
	output, err := cmd.Output()
	if err != nil {
		log.Fatal("findAlbums: ", err)
	}
	lines := strings.Split(string(output), "\n")

	albums = make([]Album, len(lines)-1)
	for i, line := range lines {
		if len(line) > 0 {
			parts := strings.Split(line, "/")
			albums[i] = Album{
				parts[len(parts)-1],
				parts[len(parts)-2],
				line,
			}
		}
	}
}

func startMplayer() {
	cmd := exec.Command("mplayer", "-slave", "-really-quiet", "-cache", "64", "-idle")
	mplayer, _ = cmd.StdinPipe()
	err := cmd.Start()
	if err != nil {
		log.Fatal("mplayer: ", err)
	}
}

func main() {
	port := flag.Int("port", 80, "port")
	root := flag.String("root", "", "root")
	flag.Parse()
	if *root == "" {
		panic("root required")
	}

	buildTemplates()
	findAlbums(*root)
	startMplayer()

	http.HandleFunc("/", handler)
	err := http.ListenAndServe(":"+strconv.Itoa(*port), nil)
	if err != nil {
		log.Fatal("http: ", err)
	}
}
<|endoftext|>


<commit_before>package main

import (
	"flag"
	"fmt"
	"github.com/howeyc/fsnotify"
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"time"
	"syscall"
)

var (
	help    = flag.Bool("help", false, "help")
	command = flag.String("c", "", "command to run when files change in given directories")
	ignore  = flag.String("i", "", "comma separated list of files to ignore")
)

func usage() {
	fmt.Fprintf(os.Stderr, "justrun -c 'SOME BASH COMMAND' [FILEPATH...]\n")
	os.Exit(1)
}

func main() {
	flag.Parse()
	if *help {
		usage()
	}
	ignoreNames := strings.Split(*ignore, ",")
	ignored := make(map[string]bool)
	for _, in := range ignoreNames {
		a, err := filepath.Abs(in)
		if err != nil {
			log.Fatalf("unable to get current working dir")
		}
		ignored[a] = true
	}
	cmdCh := make(chan time.Time)
	w, err := fsnotify.NewWatcher()
	if err != nil {
		log.Fatal(err)
	}
	go func() {
		for {
			select {
			case ev := <-w.Event:
				en, err := filepath.Abs(ev.Name)
				if err != nil {
					log.Fatalf("unable to get current working dir")
				}
				if ignored[en] {
					continue
				}
				//				log.Printf("event %s: file %s", ev, ev.Name)
				cmdCh <- time.Now()
			case err := <-w.Error:
				log.Println("error:", err)
			}
		}
	}()

	if len(flag.Args()) == 0 {
		usage()
	}
	for _, path := range flag.Args() {
		err = w.Watch(path)
		if err != nil {
			log.Fatalf("unable to watch: %s", err)
		}
	}
	lastStartTime := time.Unix(0, 0)
	var proc *os.Process
	done := make(chan error)
	for t := range cmdCh {
		log.Printf("event at %d", t)
		if t.After(lastStartTime.Add(30 * time.Millisecond)) {
			if proc != nil {
				shutdownCommand(proc, done)
			}
			lastStartTime = time.Now()
			proc = runCommand(proc, done)
		}
	}
}

func runCommand(oldProc *os.Process, done chan error) *os.Process {
	log.Println("running")
	cmd := exec.Command("bash", "-c", *command)
	// Necessary so that SIGTERM's will traverse down to the the child
	// processes in the bash command below.
	cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	err := cmd.Start()
	if err != nil {
		log.Printf("error process %d", cmd.Process)
		log.Printf("command failed: %s", err)
		return oldProc
	}
	log.Printf("got process %d", cmd.Process.Pid)

	go func() {
		err := cmd.Wait()
		log.Printf("exiting process %d", cmd.Process.Pid)
		done <- err
	}()
	return cmd.Process
}

func shutdownCommand(proc *os.Process, done chan error) {
	// the negation here means to kill not just the parent pid (which is the
	// bash shell), but also its children. This means long-lived servers can
	// be killed as well quickly exiting processes. e.g. "go build &&
	// ./myserver -http=:6000". fswatch and others won't do this.
	err = syscall.Kill(-proc.Pid, syscall.SIGTERM)
	if err != nil {
		log.Printf("returning after first kill")
		return
	}

	for {
		select {
		case <-done:
			goto done
		case <-time.After(300 * time.Millisecond):
			err := syscall.Kill(-proc.Pid, syscall.SIGTERM)
			log.Printf("kill error?: %#v", err)
			if err != nil {
				log.Printf("returning after kill")
				return
			}
		}
	}
done:
	log.Printf("Shutdown cleanly, i guess? %d", proc.Pid)
}
<commit_msg>term child processes when termed<commit_after>package main

import (
	"flag"
	"fmt"
	"github.com/howeyc/fsnotify"
	"log"
	"os"
	"os/exec"
	"os/signal"
	"path/filepath"
	"strings"
	"sync"
	"syscall"
	"time"
)

var (
	help    = flag.Bool("help", false, "help")
	command = flag.String("c", "", "command to run when files change in given directories")
	ignore  = flag.String("i", "", "comma separated list of files to ignore")

	// for signal handling since we do interesting stuff with setpgid on the
	// child process we create.
	sigLock   = &sync.Mutex{}
	globalCmd *exec.Cmd
)

func usage() {
	fmt.Fprintf(os.Stderr, "justrun -c 'SOME BASH COMMAND' [FILEPATH...]\n")
	os.Exit(1)
}

// TODO make container to clean up locking
func main() {
	flag.Parse()
	if *help {
		usage()
	}
	sigCh := make(chan os.Signal)
	signal.Notify(sigCh, os.Interrupt)
	go waitForInterrupt(sigCh)

	ignoreNames := strings.Split(*ignore, ",")
	ignored := make(map[string]bool)
	for _, in := range ignoreNames {
		a, err := filepath.Abs(in)
		if err != nil {
			log.Fatalf("unable to get current working dir")
		}
		ignored[a] = true
	}
	cmdCh := make(chan time.Time)
	w, err := fsnotify.NewWatcher()
	if err != nil {
		log.Fatal(err)
	}
	go func() {
		for {
			select {
			case ev := <-w.Event:
				en, err := filepath.Abs(ev.Name)
				if err != nil {
					log.Fatalf("unable to get current working dir")
				}
				if ignored[en] {
					continue
				}
				cmdCh <- time.Now()
			case err := <-w.Error:
				log.Println("error:", err)
			}
		}
	}()

	if len(flag.Args()) == 0 {
		usage()
	}
	for _, path := range flag.Args() {
		err = w.Watch(path)
		if err != nil {
			log.Fatalf("unable to watch: %s", err)
		}
	}
	lastStartTime := time.Unix(0, 0)
	var cmd *exec.Cmd
	done := make(chan error)
	for t := range cmdCh {
		if t.After(lastStartTime.Add(30 * time.Millisecond)) {
			if cmd != nil {
				shutdownCommand(cmd, done)
			}
			lastStartTime = time.Now()
			cmd = runCommand(cmd, done)
		}
	}
}

func runCommand(oldCmd *exec.Cmd, done chan error) *exec.Cmd {
	log.Println("running " + *command)
	cmd := exec.Command("bash", "-c", *command)
	// Necessary so that SIGTERM's will traverse down to the the child
	// processes in the bash command below. See shutdownCommand.
	cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	sigLock.Lock()
	err := cmd.Start()
	if err != nil {
		log.Printf("command failed: %s", err)
		return oldCmd
	}
	globalCmd = cmd
	sigLock.Unlock()
	go func() {
		err := cmd.Wait()
		done <- err
	}()
	return cmd
}

func shutdownCommand(cmd *exec.Cmd, done chan error) {
	sigLock.Lock()
	defer sigLock.Unlock()
	// the negation here means to kill not just the parent pid (which is the
	// bash shell), but also its children. This means long-lived servers can
	// be killed as well quickly exiting processes. e.g. "go build &&
	// ./myserver -http=:6000". fswatch and others won't do this.
	err := syscall.Kill(-cmd.Process.Pid, syscall.SIGTERM)
	if err != nil {
		return
	}
	for {
		select {
		case <-done:
			goto done
		case <-time.After(300 * time.Millisecond):
			err := syscall.Kill(-cmd.Process.Pid, syscall.SIGTERM)
			if err != nil {
				goto done
			}
		}
	}
done:
	log.Printf("terminating command %d\n", cmd.Process.Pid)
}

func waitForInterrupt(sigCh chan os.Signal) {
	defer os.Exit(0)
	<-sigCh
	sigLock.Lock()
	defer sigLock.Unlock()
	if globalCmd == nil {
		return
	}
	err := syscall.Kill(-globalCmd.Process.Pid, syscall.SIGTERM)
	if err != nil {
		log.Printf("on interrupt, unable to kill command: %s", err)
	}
}
<|endoftext|>


<commit_before>package lib

import (
	"encoding/hex"
	"fmt"
	"sort"
	"strings"

	git "github.com/libgit2/git2go"
	yaml "gopkg.in/yaml.v2"
)

type BuildCmd struct {
	Cmd  string
	Args []string `yaml:",flow"`
}

type Spec struct {
	Version    string
	Name       string
	Build      map[string]*BuildCmd
	Properties map[string]interface{}
}

type Application struct {
	Name       string
	Path       string
	Build      map[string]*BuildCmd
	Version    string
	Properties map[string]interface{}
}

type Applications []*Application

type Manifest struct {
	Dir          string
	Sha          string
	Applications Applications
}

func ManifestByPr(dir, src, dst string) (*Manifest, error) {
	repo, m, err := openRepo(dir)
	if err != nil {
		return nil, err
	}

	if m != nil {
		return m, nil
	}

	srcC, err := getBranchCommit(repo, src)
	if err != nil {
		return nil, err
	}

	dstC, err := getBranchCommit(repo, dst)
	if err != err {
		return nil, err
	}

	diff, err := getDiffFromMergeBase(repo, srcC, dstC)
	if err != nil {
		return nil, err
	}

	m, err = fromBranch(repo, dir, src)
	if err != nil {
		return nil, err
	}

	return reduceToDiff(m, diff)
}

func ManifestBySha(dir, sha string) (*Manifest, error) {
	repo, m, err := openRepo(dir)
	if err != nil {
		return nil, err
	}

	if m != nil {
		return m, nil
	}

	bytes, err := hex.DecodeString(sha)
	if err != nil {
		return nil, err
	}

	oid := git.NewOidFromBytes(bytes)
	commit, err := repo.LookupCommit(oid)
	if err != nil {
		return nil, err
	}

	return fromCommit(repo, dir, commit)
}

func ManifestByBranch(dir, branch string) (*Manifest, error) {
	repo, m, err := openRepo(dir)
	if err != nil {
		return nil, err
	}

	if m != nil {
		return m, nil
	}

	return fromBranch(repo, dir, branch)
}

func ManifestByDiff(dir, from, to string) (*Manifest, error) {
	repo, m, err := openRepo(dir)
	if err != nil {
		return nil, err
	}

	if m != nil {
		return m, nil
	}

	fromOid, err := git.NewOid(from)
	if err != nil {
		return nil, err
	}

	toOid, err := git.NewOid(to)
	if err != nil {
		return nil, err
	}

	fromC, err := repo.LookupCommit(fromOid)
	if err != nil {
		return nil, err
	}

	toC, err := repo.LookupCommit(toOid)
	if err != nil {
		return nil, err
	}

	diff, err := getDiffFromMergeBase(repo, toC, fromC)
	if err != nil {
		return nil, err
	}

	m, err = fromCommit(repo, dir, fromC)
	if err != nil {
		return nil, err
	}

	return reduceToDiff(m, diff)
}

// Sort interface to sort applications by path
func (a Applications) Len() int {
	return len(a)
}

func (a Applications) Less(i, j int) bool {
	return a[i].Path < a[j].Path
}

func (a Applications) Swap(i, j int) {
	a[i], a[j] = a[j], a[i]
}

func (m *Manifest) indexByName() map[string]*Application {
	q := make(map[string]*Application)
	for _, a := range m.Applications {
		q[a.Name] = a
	}
	return q
}

func (m *Manifest) indexByPath() map[string]*Application {
	q := make(map[string]*Application)
	for _, a := range m.Applications {
		q[fmt.Sprintf("%s/", a.Path)] = a
	}
	return q
}

func fromCommit(repo *git.Repository, dir string, commit *git.Commit) (*Manifest, error) {
	tree, err := commit.Tree()
	if err != nil {
		return nil, err
	}

	vapps := Applications{}

	err = tree.Walk(func(path string, entry *git.TreeEntry) int {
		if entry.Name == ".mbt.yml" && entry.Type == git.ObjectBlob {
			blob, err := repo.LookupBlob(entry.Id)
			if err != nil {
				return 1
			}

			version := ""

			p := strings.TrimRight(path, "/")
			if p != "" {
				// We are not on the root, take the git sha for parent tree object.
				dirEntry, err := tree.EntryByPath(p)
				if err != nil {
					return 1
				}
				version = dirEntry.Id.String()
			} else {
				// We are on the root, take the commit sha.
				version = commit.Id().String()
			}

			a, err := newApplication(p, version, blob.Contents())
			if err != nil {
				// TODO log this or fail
				return 1
			}

			vapps = append(vapps, a)
		}
		return 0
	})

	if err != nil {
		return nil, err
	}

	sort.Sort(vapps)
	return &Manifest{dir, commit.Id().String(), vapps}, nil
}

func newApplication(dir, version string, spec []byte) (*Application, error) {
	a := &Spec{
		Properties: make(map[string]interface{}),
		Build:      make(map[string]*BuildCmd),
	}

	err := yaml.Unmarshal(spec, a)
	if err != nil {
		return nil, err
	}

	return &Application{
		Build:      a.Build,
		Name:       a.Name,
		Properties: a.Properties,
		Version:    version,
		Path:       dir,
	}, nil
}

func newEmptyManifest(dir string) *Manifest {
	return &Manifest{Applications: []*Application{}, Dir: dir, Sha: ""}
}

func fromBranch(repo *git.Repository, dir string, branch string) (*Manifest, error) {
	commit, err := getBranchCommit(repo, branch)
	if err != nil {
		return nil, err
	}

	return fromCommit(repo, dir, commit)
}

func reduceToDiff(manifest *Manifest, diff *git.Diff) (*Manifest, error) {
	q := manifest.indexByPath()
	filtered := make(map[string]*Application)
	err := diff.ForEach(func(delta git.DiffDelta, num float64) (git.DiffForEachHunkCallback, error) {
		for k := range q {
			if _, ok := filtered[k]; ok {
				continue
			}
			if strings.HasPrefix(delta.NewFile.Path, k) {
				filtered[k] = q[k]
			}
		}
		return nil, nil
	}, git.DiffDetailFiles)

	if err != nil {
		return nil, err
	}

	apps := []*Application{}
	for _, v := range filtered {
		apps = append(apps, v)
	}

	return &Manifest{
		Dir:          manifest.Dir,
		Sha:          manifest.Sha,
		Applications: apps,
	}, nil
}

func openRepo(dir string) (*git.Repository, *Manifest, error) {
	repo, err := git.OpenRepository(dir)
	if err != nil {
		return nil, nil, err
	}
	empty, err := repo.IsEmpty()
	if err != nil {
		return nil, nil, err
	}

	if empty {
		return nil, newEmptyManifest(dir), nil
	}

	return repo, nil, nil
}
<commit_msg>Use the correct commit to generate the manifest<commit_after>package lib

import (
	"encoding/hex"
	"fmt"
	"sort"
	"strings"

	git "github.com/libgit2/git2go"
	yaml "gopkg.in/yaml.v2"
)

type BuildCmd struct {
	Cmd  string
	Args []string `yaml:",flow"`
}

type Spec struct {
	Version    string
	Name       string
	Build      map[string]*BuildCmd
	Properties map[string]interface{}
}

type Application struct {
	Name       string
	Path       string
	Build      map[string]*BuildCmd
	Version    string
	Properties map[string]interface{}
}

type Applications []*Application

type Manifest struct {
	Dir          string
	Sha          string
	Applications Applications
}

func ManifestByPr(dir, src, dst string) (*Manifest, error) {
	repo, m, err := openRepo(dir)
	if err != nil {
		return nil, err
	}

	if m != nil {
		return m, nil
	}

	srcC, err := getBranchCommit(repo, src)
	if err != nil {
		return nil, err
	}

	dstC, err := getBranchCommit(repo, dst)
	if err != err {
		return nil, err
	}

	diff, err := getDiffFromMergeBase(repo, srcC, dstC)
	if err != nil {
		return nil, err
	}

	m, err = fromBranch(repo, dir, src)
	if err != nil {
		return nil, err
	}

	return reduceToDiff(m, diff)
}

func ManifestBySha(dir, sha string) (*Manifest, error) {
	repo, m, err := openRepo(dir)
	if err != nil {
		return nil, err
	}

	if m != nil {
		return m, nil
	}

	bytes, err := hex.DecodeString(sha)
	if err != nil {
		return nil, err
	}

	oid := git.NewOidFromBytes(bytes)
	commit, err := repo.LookupCommit(oid)
	if err != nil {
		return nil, err
	}

	return fromCommit(repo, dir, commit)
}

func ManifestByBranch(dir, branch string) (*Manifest, error) {
	repo, m, err := openRepo(dir)
	if err != nil {
		return nil, err
	}

	if m != nil {
		return m, nil
	}

	return fromBranch(repo, dir, branch)
}

func ManifestByDiff(dir, from, to string) (*Manifest, error) {
	repo, m, err := openRepo(dir)
	if err != nil {
		return nil, err
	}

	if m != nil {
		return m, nil
	}

	fromOid, err := git.NewOid(from)
	if err != nil {
		return nil, err
	}

	toOid, err := git.NewOid(to)
	if err != nil {
		return nil, err
	}

	fromC, err := repo.LookupCommit(fromOid)
	if err != nil {
		return nil, err
	}

	toC, err := repo.LookupCommit(toOid)
	if err != nil {
		return nil, err
	}

	diff, err := getDiffFromMergeBase(repo, toC, fromC)
	if err != nil {
		return nil, err
	}

	m, err = fromCommit(repo, dir, toC)
	if err != nil {
		return nil, err
	}

	return reduceToDiff(m, diff)
}

// Sort interface to sort applications by path
func (a Applications) Len() int {
	return len(a)
}

func (a Applications) Less(i, j int) bool {
	return a[i].Path < a[j].Path
}

func (a Applications) Swap(i, j int) {
	a[i], a[j] = a[j], a[i]
}

func (m *Manifest) indexByName() map[string]*Application {
	q := make(map[string]*Application)
	for _, a := range m.Applications {
		q[a.Name] = a
	}
	return q
}

func (m *Manifest) indexByPath() map[string]*Application {
	q := make(map[string]*Application)
	for _, a := range m.Applications {
		q[fmt.Sprintf("%s/", a.Path)] = a
	}
	return q
}

func fromCommit(repo *git.Repository, dir string, commit *git.Commit) (*Manifest, error) {
	tree, err := commit.Tree()
	if err != nil {
		return nil, err
	}

	vapps := Applications{}

	err = tree.Walk(func(path string, entry *git.TreeEntry) int {
		if entry.Name == ".mbt.yml" && entry.Type == git.ObjectBlob {
			blob, err := repo.LookupBlob(entry.Id)
			if err != nil {
				return 1
			}

			version := ""

			p := strings.TrimRight(path, "/")
			if p != "" {
				// We are not on the root, take the git sha for parent tree object.
				dirEntry, err := tree.EntryByPath(p)
				if err != nil {
					return 1
				}
				version = dirEntry.Id.String()
			} else {
				// We are on the root, take the commit sha.
				version = commit.Id().String()
			}

			a, err := newApplication(p, version, blob.Contents())
			if err != nil {
				// TODO log this or fail
				return 1
			}

			vapps = append(vapps, a)
		}
		return 0
	})

	if err != nil {
		return nil, err
	}

	sort.Sort(vapps)
	return &Manifest{dir, commit.Id().String(), vapps}, nil
}

func newApplication(dir, version string, spec []byte) (*Application, error) {
	a := &Spec{
		Properties: make(map[string]interface{}),
		Build:      make(map[string]*BuildCmd),
	}

	err := yaml.Unmarshal(spec, a)
	if err != nil {
		return nil, err
	}

	return &Application{
		Build:      a.Build,
		Name:       a.Name,
		Properties: a.Properties,
		Version:    version,
		Path:       dir,
	}, nil
}

func newEmptyManifest(dir string) *Manifest {
	return &Manifest{Applications: []*Application{}, Dir: dir, Sha: ""}
}

func fromBranch(repo *git.Repository, dir string, branch string) (*Manifest, error) {
	commit, err := getBranchCommit(repo, branch)
	if err != nil {
		return nil, err
	}

	return fromCommit(repo, dir, commit)
}

func reduceToDiff(manifest *Manifest, diff *git.Diff) (*Manifest, error) {
	q := manifest.indexByPath()
	filtered := make(map[string]*Application)
	err := diff.ForEach(func(delta git.DiffDelta, num float64) (git.DiffForEachHunkCallback, error) {
		for k := range q {
			if _, ok := filtered[k]; ok {
				continue
			}
			if strings.HasPrefix(delta.NewFile.Path, k) {
				filtered[k] = q[k]
			}
		}
		return nil, nil
	}, git.DiffDetailFiles)

	if err != nil {
		return nil, err
	}

	apps := []*Application{}
	for _, v := range filtered {
		apps = append(apps, v)
	}

	return &Manifest{
		Dir:          manifest.Dir,
		Sha:          manifest.Sha,
		Applications: apps,
	}, nil
}

func openRepo(dir string) (*git.Repository, *Manifest, error) {
	repo, err := git.OpenRepository(dir)
	if err != nil {
		return nil, nil, err
	}
	empty, err := repo.IsEmpty()
	if err != nil {
		return nil, nil, err
	}

	if empty {
		return nil, newEmptyManifest(dir), nil
	}

	return repo, nil, nil
}
<|endoftext|>


<commit_before>package main

import (
	"bufio"
	"fmt"
	"log"
	"os"
	"regexp"
	"strings"
)

type countCFile struct {
	startRe, endRe *regexp.Regexp
}

func NewCountCFile() *countCFile {
	this := &countCFile{}
	this.startRe = regexp.MustCompile("/\\*")
	this.endRe = regexp.MustCompile("\\*/")
	return this
}

//
// return value:
//  allComment:  true: it's an all comment line
//              false: it's include real code
//  transition: -1: start comment
//               0: no change
//               1: end comment
func (this *countCFile) checkLine(line string) (allComment bool, transition int) {
	allComment = true
	transition = 0
	matched := false

	matched, _ = regexp.MatchString("^\\s*$", line)
	if matched {
		return
	}

	matched, _ = regexp.MatchString("^\\s*//.*$", line)
	if matched {
		return
	}

	matched, _ = regexp.MatchString("^\\s*\\w\\w*", line)
	if matched {
		allComment = false
	}

	commentStarts := this.startRe.FindStringIndex(line)

	commentEnds := this.endRe.FindStringIndex(line)

	if len(commentStarts) > len(commentEnds) {
		transition = -1
	} else if len(commentStarts) < len(commentEnds) {
		transition = 1
	}
	return
}

func (this *countCFile) countFile(fileName *string) (line int) {
	line = 0
	inComment := false

	if *verbose > 3 {
		fmt.Printf("count: %s\n", *fileName)
	}
	if !strings.HasSuffix(*fileName, *extension) {
		return
	}

	file, err := os.Open(*fileName)
	if err != nil {
		log.Fatal(err)
		return 0
	}
	defer file.Close()

	scanner := bufio.NewScanner(file)
	for scanner.Scan() {
		allComment, transition := this.checkLine(scanner.Text())
		if *verbose > 4 {
			fmt.Printf("%v,%v: %s\n", allComment, transition, scanner.Text())
		}
		if inComment {
			if transition == 0 {
				continue
			} else if transition == 1 {
				inComment = false
			}
		} else {
			if transition == -1 {
				inComment = true
			}
		}
		if allComment != true {
			line++
		}
		if err != nil {
			log.Fatal(err)
		}
	}
	err = scanner.Err()
	if err != nil {
		log.Println(*fileName, ": ", err)
	}
	if *verbose > 1 {
		fmt.Printf("%s:%d\n", *fileName, line)
	}
	return line
}
<commit_msg>make line of codes looks larger.<commit_after>package main

import (
	"bufio"
	"fmt"
	"log"
	"os"
	"regexp"
	"strings"
)

type countCFile struct {
	startRe, endRe *regexp.Regexp
}

func NewCountCFile() *countCFile {
	this := &countCFile{}
	this.startRe = regexp.MustCompile("/\\*")
	this.endRe = regexp.MustCompile("\\*/")
	return this
}

//
// return value:
//  allComment:  true: it's an all comment line
//              false: it's include real code
//  transition: -1: start comment
//               0: no change
//               1: end comment
func (this *countCFile) checkLine(line string) (allComment bool, transition int) {
	allComment = true
	transition = 0
	matched := false

	matched, _ = regexp.MatchString("^\\s*$", line)
	if matched {
		return
	}

	matched, _ = regexp.MatchString("^\\s*//.*$", line)
	if matched {
		return
	}

	matched, _ = regexp.MatchString("^\\s*[\\w})\"]\\w*", line)
	if matched {
		allComment = false
	}

	commentStarts := this.startRe.FindStringIndex(line)

	commentEnds := this.endRe.FindStringIndex(line)

	if len(commentStarts) > len(commentEnds) {
		transition = -1
	} else if len(commentStarts) < len(commentEnds) {
		transition = 1
	}
	return
}

func (this *countCFile) countFile(fileName *string) (line int) {
	line = 0
	inComment := false

	if *verbose > 3 {
		fmt.Printf("count: %s\n", *fileName)
	}
	if !strings.HasSuffix(*fileName, *extension) {
		return
	}

	file, err := os.Open(*fileName)
	if err != nil {
		log.Fatal(err)
		return 0
	}
	defer file.Close()

	scanner := bufio.NewScanner(file)
	for scanner.Scan() {
		allComment, transition := this.checkLine(scanner.Text())
		if *verbose > 4 {
			fmt.Printf("%v,%v: %s\n", allComment, transition, scanner.Text())
		}
		if inComment {
			if transition == 0 {
				continue
			} else if transition == 1 {
				inComment = false
			}
		} else {
			if transition == -1 {
				inComment = true
			}
		}
		if allComment != true {
			line++
		}
		if err != nil {
			log.Fatal(err)
		}
	}
	err = scanner.Err()
	if err != nil {
		log.Println(*fileName, ": ", err)
	}
	if *verbose > 1 {
		fmt.Printf("%s:%d\n", *fileName, line)
	}
	return line
}
<|endoftext|>


<commit_before>// Keywrap algorithms are a class of symmetric encryption algorithms designed to
// encapsulate (encrypt) cryptographic key material. The algorithms are not
// limited to key material and input data is therefore referred to as keydata.
package keywrap

// Identifies a particular key wrap algorithm which is implemented in another
// package
type WrapAlgorithm uint

const (
	RFC3394 WrapAlgorithm = iota // import github.com/basvk/keywrap/rfc3394
	maxAlgorithms
)

// holds builder functions for key wrap algorithm implementations
var wrapAlgorithms = make([]func(key []byte) (KeyWrapper, error), maxAlgorithms)

// register a builder function which can create a wrap algorithm implementation
// must be called from the init method in implementation packages
func RegisterWrapAlgorithm(a WrapAlgorithm, builder func(key []byte) (KeyWrapper, error)) {
	if a > maxAlgorithms {
		panic("register building function for unknown wrap algorithm")
	}
	wrapAlgorithms[a] = builder
}

// create a wrap algorithm implementation
func (a WrapAlgorithm) New(key []byte) (KeyWrapper, error) {
	return wrapAlgorithms[a](key)
}

// KeyWrapper is the interface implemented by an object that can wrap keydata.
type KeyWrapper interface {
	// Wraps the given keydata with the given key
	Wrap(keydata []byte) (ciphertext []byte, err error)

	// Unwraps the given ciphertext with the given key back to keydata
	Unwrap(ciphertext []byte) (keydata []byte, err error)
}
<commit_msg>fixed typo in comment<commit_after>// Keywrap algorithms are a class of symmetric encryption algorithms designed to
// encapsulate (encrypt) cryptographic key material. The algorithms are not
// limited to key material and input data is therefore referred to as keydata.
package keywrap

// Identifies a particular key wrap algorithm which is implemented in another
// package
type WrapAlgorithm uint

const (
	RFC3394 WrapAlgorithm = iota // import github.com/bas-vk/keywrap/rfc3394
	maxAlgorithms
)

// holds builder functions for key wrap algorithm implementations
var wrapAlgorithms = make([]func(key []byte) (KeyWrapper, error), maxAlgorithms)

// register a builder function which can create a wrap algorithm implementation
// must be called from the init method in implementation packages
func RegisterWrapAlgorithm(a WrapAlgorithm, builder func(key []byte) (KeyWrapper, error)) {
	if a > maxAlgorithms {
		panic("register building function for unknown wrap algorithm")
	}
	wrapAlgorithms[a] = builder
}

// create a wrap algorithm implementation
func (a WrapAlgorithm) New(key []byte) (KeyWrapper, error) {
	return wrapAlgorithms[a](key)
}

// KeyWrapper is the interface implemented by an object that can wrap keydata.
type KeyWrapper interface {
	// Wraps the given keydata with the given key
	Wrap(keydata []byte) (ciphertext []byte, err error)

	// Unwraps the given ciphertext with the given key back to keydata
	Unwrap(ciphertext []byte) (keydata []byte, err error)
}
<|endoftext|>


<commit_before>package kinesis

import (
	"log"
	"sync"
	"text/template"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/service/kinesis"
	"github.com/gliderlabs/logspout/router"
)

func init() {
	router.AdapterFactories.Register(NewKinesisAdapter, "kinesis")
}

type KinesisAdapter struct {
	Client     *kinesis.Kinesis
	Drainers   map[string]*Drainer
	Launched   map[string]bool
	StreamTmpl *template.Template
	mutex      sync.Mutex
}

func NewKinesisAdapter(route *router.Route) (router.LogAdapter, error) {
	drainers := make(map[string]*Drainer)
	launched := make(map[string]bool)
	client := kinesis.New(&aws.Config{})
	tmpl, err := compileTmpl("KINESIS_STREAM_TEMPLATE")
	if err != nil {
		return nil, err
	}

	return &KinesisAdapter{
		Client:     client,
		Drainers:   drainers,
		Launched:   launched,
		StreamTmpl: tmpl,
	}, nil
}

func (a *KinesisAdapter) Stream(logstream chan *router.Message) {
	for {
		m, open := <-logstream
		if !open {
			log.Println("kinesis: Channel is closed, flushing all the buffers")
			logErrs(a.FlushAll())
			break
		}

		streamName, err := executeTmpl(a.StreamTmpl, m)
		if err != nil {
			logErr(err)
			break
		}

		if d, ok := a.findDrainer(streamName); ok {
			logErr(d.Buffer.Add(m))
		} else {
			if _, ok := a.Launched[streamName]; !ok {
				go newDrainer(a, streamName)
				a.Launched[streamName] = true
			}
		}
	}
}

// FlushAll flushes all the kinesis buffers one by one.
func (a *KinesisAdapter) FlushAll() []error {
	var err []error

	for _, d := range a.Drainers {
		err = append(err, d.Buffer.Flush())
	}

	return err
}

func (a *KinesisAdapter) findDrainer(streamName string) (*Drainer, bool) {
	a.mutex.Lock()
	defer a.mutex.Unlock()

	d, ok := a.Drainers[streamName]

	return d, ok
}

func (a *KinesisAdapter) addDrainer(streamName string, d *Drainer) {
	a.mutex.Lock()
	defer a.mutex.Unlock()

	log.Printf("kinesis: added drainer %s\n", streamName)
	a.Drainers[streamName] = d
}
<commit_msg>Ignore log messages not fitting the defined stream template.<commit_after>package kinesis

import (
	"log"
	"sync"
	"text/template"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/service/kinesis"
	"github.com/gliderlabs/logspout/router"
)

func init() {
	router.AdapterFactories.Register(NewKinesisAdapter, "kinesis")
}

type KinesisAdapter struct {
	Client     *kinesis.Kinesis
	Drainers   map[string]*Drainer
	Launched   map[string]bool
	StreamTmpl *template.Template
	mutex      sync.Mutex
}

func NewKinesisAdapter(route *router.Route) (router.LogAdapter, error) {
	drainers := make(map[string]*Drainer)
	launched := make(map[string]bool)
	client := kinesis.New(&aws.Config{})
	tmpl, err := compileTmpl("KINESIS_STREAM_TEMPLATE")
	if err != nil {
		return nil, err
	}

	return &KinesisAdapter{
		Client:     client,
		Drainers:   drainers,
		Launched:   launched,
		StreamTmpl: tmpl,
	}, nil
}

func (a *KinesisAdapter) Stream(logstream chan *router.Message) {
	for {
		m, open := <-logstream
		if !open {
			log.Println("kinesis: Channel is closed, flushing all the buffers")
			logErrs(a.FlushAll())
			break
		}

		streamName, err := executeTmpl(a.StreamTmpl, m)
		if err != nil {
			logErr(err)
			break
		}

		if streamName == "" {
			continue
		}

		if d, ok := a.findDrainer(streamName); ok {
			logErr(d.Buffer.Add(m))
		} else {
			if _, ok := a.Launched[streamName]; !ok {
				go newDrainer(a, streamName)
				a.Launched[streamName] = true
			}
		}
	}
}

// FlushAll flushes all the kinesis buffers one by one.
func (a *KinesisAdapter) FlushAll() []error {
	var err []error

	for _, d := range a.Drainers {
		err = append(err, d.Buffer.Flush())
	}

	return err
}

func (a *KinesisAdapter) findDrainer(streamName string) (*Drainer, bool) {
	a.mutex.Lock()
	defer a.mutex.Unlock()

	d, ok := a.Drainers[streamName]

	return d, ok
}

func (a *KinesisAdapter) addDrainer(streamName string, d *Drainer) {
	a.mutex.Lock()
	defer a.mutex.Unlock()

	log.Printf("kinesis: added drainer %s\n", streamName)
	a.Drainers[streamName] = d
}
<|endoftext|>


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

import (
	"os"
	"testing"
)

func TestForestDBKVCrud(t *testing.T) {
	defer os.RemoveAll("test")

	dbfile, err := Open("test", nil)
	if err != nil {
		t.Fatal(err)
	}
	defer dbfile.Close()

	kvstore, err := dbfile.OpenKVStoreDefault(nil)
	if err != nil {
		t.Fatal(err)
	}
	defer kvstore.Close()

	// get a non-existant key
	var val []byte
	val, err = kvstore.GetKV([]byte("doesnotexist"))
	if val != nil {
		t.Error("expected nil value")
	}
	if err != RESULT_KEY_NOT_FOUND {
		t.Errorf("expected %v, got %v", RESULT_KEY_NOT_FOUND, err)
	}

	// put a new key
	err = kvstore.SetKV([]byte("key1"), []byte("value1"))
	if err != nil {
		t.Error(err)
	}

	// lookup that key
	val, err = kvstore.GetKV([]byte("key1"))
	if err != nil {
		t.Error(err)
	}
	if string(val) != "value1" {
		t.Errorf("expected value1, got %s", val)
	}

	// update it
	err = kvstore.SetKV([]byte("key1"), []byte("value1-updated"))
	if err != nil {
		t.Error(err)
	}

	// look it up again
	val, err = kvstore.GetKV([]byte("key1"))
	if err != nil {
		t.Error(err)
	}
	if string(val) != "value1-updated" {
		t.Errorf("expected value1-updated, got %s", val)
	}

	// delete it
	err = kvstore.DeleteKV([]byte("key1"))
	if err != nil {
		t.Error(err)
	}

	// look it up again
	val, err = kvstore.GetKV([]byte("key1"))
	if err != RESULT_KEY_NOT_FOUND {
		t.Error(err)
	}
	if val != nil {
		t.Error("expected nil value, got %#v", val)
	}

	// delete it again
	err = kvstore.DeleteKV([]byte("key1"))
	if err != nil {
		t.Error(err)
	}

	// dete non-existant key
	err = kvstore.DeleteKV([]byte("doesnotext"))
	if err != nil {
		t.Error(err)
	}
}
<commit_msg>fix formatting issue identified by go vet<commit_after>//  Copyright (c) 2014 Couchbase, Inc.
//  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
//  except in compliance with the License. You may obtain a copy of the License at
//    http://www.apache.org/licenses/LICENSE-2.0
//  Unless required by applicable law or agreed to in writing, software distributed under the
//  License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
//  either express or implied. See the License for the specific language governing permissions
//  and limitations under the License.
package forestdb

import (
	"os"
	"testing"
)

func TestForestDBKVCrud(t *testing.T) {
	defer os.RemoveAll("test")

	dbfile, err := Open("test", nil)
	if err != nil {
		t.Fatal(err)
	}
	defer dbfile.Close()

	kvstore, err := dbfile.OpenKVStoreDefault(nil)
	if err != nil {
		t.Fatal(err)
	}
	defer kvstore.Close()

	// get a non-existant key
	var val []byte
	val, err = kvstore.GetKV([]byte("doesnotexist"))
	if val != nil {
		t.Error("expected nil value")
	}
	if err != RESULT_KEY_NOT_FOUND {
		t.Errorf("expected %v, got %v", RESULT_KEY_NOT_FOUND, err)
	}

	// put a new key
	err = kvstore.SetKV([]byte("key1"), []byte("value1"))
	if err != nil {
		t.Error(err)
	}

	// lookup that key
	val, err = kvstore.GetKV([]byte("key1"))
	if err != nil {
		t.Error(err)
	}
	if string(val) != "value1" {
		t.Errorf("expected value1, got %s", val)
	}

	// update it
	err = kvstore.SetKV([]byte("key1"), []byte("value1-updated"))
	if err != nil {
		t.Error(err)
	}

	// look it up again
	val, err = kvstore.GetKV([]byte("key1"))
	if err != nil {
		t.Error(err)
	}
	if string(val) != "value1-updated" {
		t.Errorf("expected value1-updated, got %s", val)
	}

	// delete it
	err = kvstore.DeleteKV([]byte("key1"))
	if err != nil {
		t.Error(err)
	}

	// look it up again
	val, err = kvstore.GetKV([]byte("key1"))
	if err != RESULT_KEY_NOT_FOUND {
		t.Error(err)
	}
	if val != nil {
		t.Errorf("expected nil value, got %#v", val)
	}

	// delete it again
	err = kvstore.DeleteKV([]byte("key1"))
	if err != nil {
		t.Error(err)
	}

	// dete non-existant key
	err = kvstore.DeleteKV([]byte("doesnotext"))
	if err != nil {
		t.Error(err)
	}
}
<|endoftext|>


<commit_before>package main

import (
	"flag"
	"fmt"
	"html/template"
	"io/ioutil"
	"net/http"
	"os"
	"regexp"
	"strconv"
	"strings"
)

const (
	defaultPort  = 7171
	dataDir      = "notes/"
	templatesDir = "templates/"
	fileExt      = ".txt"
)

var (
	port = flag.Int("port", defaultPort, "Specify the listening port.")
)
var (
	templates = template.Must(template.ParseFiles(
		templatesDir+"index.html",
		templatesDir+"view.html",
		templatesDir+"edit.html",
		templatesDir+"error.html"))
	linkPattern  = regexp.MustCompile(`(\b)([a-zA-Z0-9\-\._]+)\.txt`)
	linkTemplate = `$1<a href="/view/$2">$2</a>`
	validPath    = regexp.MustCompile(`^/((view|edit|save|styles|error)/([a-zA-Z0-9\.\-_]*))?$`)
)

type Model map[string]interface{}

type Page struct {
	Title string
	Body  string
}

func main() {
	flag.Parse()
	checkEnvironment()
	registerHandlers()
	fmt.Println("Running... ( port", *port, ")")
	http.ListenAndServe(":"+strconv.Itoa(*port), nil)
}

func checkEnvironment() {
	_, err := os.Stat(dataDir)
	if err != nil {
		if os.IsNotExist(err) {
			err = os.Mkdir(dataDir, 0644)
			if err != nil {
				fmt.Println(`Unable to create "notes" directory:`, err)
				os.Exit(0)
			}
		} else {
			fmt.Println(`Unable to check existence of "notes" directory:`, err)
			os.Exit(0)
		}
	}
}

func registerHandlers() {
	http.HandleFunc("/", makeHandler(indexHandler))
	http.HandleFunc("/view/", makeHandler(viewHandler))
	http.HandleFunc("/edit/", makeHandler(editHandler))
	http.HandleFunc("/save/", makeHandler(saveHandler))
	http.HandleFunc("/styles/", makeHandler(styleHandler))
	http.HandleFunc("/error/", makeHandler(errorHandler))
}

func (p *Page) save() error {
	filename := p.Title + fileExt
	return ioutil.WriteFile(dataDir+filename, []byte(p.Body), 0644)
}

func (p *Page) load(title string) (*Page, error) {
	filename := title + fileExt
	filename = strings.Replace(filename, "%20", " ", -1)
	body, err := ioutil.ReadFile(dataDir + filename)
	if err != nil {
		return nil, err
	}
	p.Title = title
	p.Body = string(body)
	return p, nil
}

func buildModel(p *Page, asHtml bool) Model {
	b := p.Body
	if asHtml {
		b = parseText(b)
	}
	m := Model{
		"Title": p.Title,
		"Body":  template.HTML(b),
	}
	return m
}

func parseText(body string) string {
	b := strings.Replace(body, "\n", "
", -1)
	b = linkPattern.ReplaceAllString(b, linkTemplate)
	return b
}

func styleHandler(w http.ResponseWriter, r *http.Request, title string) {
	filename := "styles/" + title
	body, err := ioutil.ReadFile(filename)
	if err != nil {
		fmt.Println("style handler err", err)
		return
	}
	w.Header().Set("content-type", "text/css")
	w.Write(body)
}

func indexHandler(w http.ResponseWriter, req *http.Request, title string) {
	title = "index"
	p, err := new(Page).load(title)
	if err != nil {
		http.Redirect(w, req, "/edit/"+title, http.StatusFound)
		return
	}
	model := buildModel(p, true)
	renderTemplate(w, "index", model)
}

func errorHandler(w http.ResponseWriter, req *http.Request, title string) {
	//title = "error"
	//p := &Page{Title: title}
	p := &Page{}
	model := buildModel(p, true)
	renderTemplate(w, "error", model)
}

func viewHandler(w http.ResponseWriter, req *http.Request, title string) {
	if title == "index" {
		http.Redirect(w, req, "/", http.StatusFound)
		return
	}
	p, err := new(Page).load(title)
	if err != nil {
		http.Redirect(w, req, "/edit/"+title, http.StatusFound)
		return
	}
	model := buildModel(p, true)
	renderTemplate(w, "view", model)
}

func editHandler(w http.ResponseWriter, req *http.Request, title string) {
	p, err := new(Page).load(title)
	if err != nil {
		p = &Page{Title: title}
	}
	model := buildModel(p, false)
	renderTemplate(w, "edit", model)
}

func saveHandler(w http.ResponseWriter, req *http.Request, title string) {
	body := req.FormValue("body")
	p := &Page{Title: title, Body: body}
	err := p.save()
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	http.Redirect(w, req, "/view/"+title, http.StatusFound)
}

func renderTemplate(w http.ResponseWriter, tmpl string, model Model) {
	err := templates.ExecuteTemplate(w, tmpl+".html", model)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
}

func makeHandler(fn func(http.ResponseWriter, *http.Request, string)) http.HandlerFunc {
	return func(w http.ResponseWriter, req *http.Request) {
		pathParts := validPath.FindStringSubmatch(req.URL.Path)
		if pathParts == nil {
			//u := &url.URL{Path: req.URL.Path}
			//encodedPath := u.String()
			http.Redirect(w, req, "/error/", http.StatusFound)
			return
		}
		fn(w, req, pathParts[3])
	}
}
<commit_msg>Added no-such-file indicator to loose links.<commit_after>package main

import (
	"flag"
	"fmt"
	"html/template"
	"io/ioutil"
	"net/http"
	"os"
	"regexp"
	"strconv"
	"strings"
)

const (
	defaultPort  = 7171
	dataDir      = "notes/"
	templatesDir = "templates/"
	fileExt      = ".txt"
)

var (
	port = flag.Int("port", defaultPort, "Specify the listening port.")
)
var (
	templates = template.Must(template.ParseFiles(
		templatesDir+"index.html",
		templatesDir+"view.html",
		templatesDir+"edit.html",
		templatesDir+"error.html"))
	linkPattern      = regexp.MustCompile(`(\b)([a-zA-Z0-9\-\._]+)\.txt`)
	linkTemplate     = `$1<a href="/view/$2">$2</a>`
	deadLinkTemplate = `$1<a href="/view/$2">$2</a> [no such file] `
	validPath        = regexp.MustCompile(`^/((view|edit|save|styles|error)/([a-zA-Z0-9\.\-_]*))?$`)
)

type Model map[string]interface{}

type Page struct {
	Title string
	Body  string
}

func main() {
	flag.Parse()
	checkEnvironment()
	registerHandlers()
	fmt.Println("Running... ( port", *port, ")")
	http.ListenAndServe(":"+strconv.Itoa(*port), nil)
}

func checkEnvironment() {
	_, err := os.Stat(dataDir)
	if err != nil {
		if os.IsNotExist(err) {
			err = os.Mkdir(dataDir, 0644)
			if err != nil {
				fmt.Println(`Unable to create "notes" directory:`, err)
				os.Exit(0)
			}
		} else {
			fmt.Println(`Unable to check existence of "notes" directory:`, err)
			os.Exit(0)
		}
	}
}

func registerHandlers() {
	http.HandleFunc("/", makeHandler(indexHandler))
	http.HandleFunc("/view/", makeHandler(viewHandler))
	http.HandleFunc("/edit/", makeHandler(editHandler))
	http.HandleFunc("/save/", makeHandler(saveHandler))
	http.HandleFunc("/styles/", makeHandler(styleHandler))
	http.HandleFunc("/error/", makeHandler(errorHandler))
}

func (p *Page) save() error {
	filename := p.Title + fileExt
	return ioutil.WriteFile(dataDir+filename, []byte(p.Body), 0644)
}

func (p *Page) load(title string) (*Page, error) {
	filename := title + fileExt
	filename = strings.Replace(filename, "%20", " ", -1)
	body, err := ioutil.ReadFile(dataDir + filename)
	if err != nil {
		return nil, err
	}
	p.Title = title
	p.Body = string(body)
	return p, nil
}

func buildModel(p *Page, asHtml bool) Model {
	b := p.Body
	if asHtml {
		b = parseText(b)
	}
	m := Model{
		"Title": p.Title,
		"Body":  template.HTML(b),
	}
	return m
}

func parseText(body string) string {
	b := strings.Replace(body, "\n", "
", -1)
	b = linkPattern.ReplaceAllStringFunc(b, func(s string) string {
		_, err := os.Stat(dataDir + s)
		if err == nil {
			r := linkPattern.ReplaceAllString(s, linkTemplate)
			return r
		} else {
			r := linkPattern.ReplaceAllString(s, deadLinkTemplate)
			return r
		}
	})
	return b
}

func styleHandler(w http.ResponseWriter, r *http.Request, title string) {
	filename := "styles/" + title
	body, err := ioutil.ReadFile(filename)
	if err != nil {
		fmt.Println("style handler err", err)
		return
	}
	w.Header().Set("content-type", "text/css")
	w.Write(body)
}

func indexHandler(w http.ResponseWriter, req *http.Request, title string) {
	title = "index"
	p, err := new(Page).load(title)
	if err != nil {
		http.Redirect(w, req, "/edit/"+title, http.StatusFound)
		return
	}
	model := buildModel(p, true)
	renderTemplate(w, "index", model)
}

func errorHandler(w http.ResponseWriter, req *http.Request, title string) {
	//title = "error"
	//p := &Page{Title: title}
	p := &Page{}
	model := buildModel(p, true)
	renderTemplate(w, "error", model)
}

func viewHandler(w http.ResponseWriter, req *http.Request, title string) {
	if title == "index" {
		http.Redirect(w, req, "/", http.StatusFound)
		return
	}
	p, err := new(Page).load(title)
	if err != nil {
		http.Redirect(w, req, "/edit/"+title, http.StatusFound)
		return
	}
	model := buildModel(p, true)
	renderTemplate(w, "view", model)
}

func editHandler(w http.ResponseWriter, req *http.Request, title string) {
	p, err := new(Page).load(title)
	if err != nil {
		p = &Page{Title: title}
	}
	model := buildModel(p, false)
	renderTemplate(w, "edit", model)
}

func saveHandler(w http.ResponseWriter, req *http.Request, title string) {
	body := req.FormValue("body")
	p := &Page{Title: title, Body: body}
	err := p.save()
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	http.Redirect(w, req, "/view/"+title, http.StatusFound)
}

func renderTemplate(w http.ResponseWriter, tmpl string, model Model) {
	err := templates.ExecuteTemplate(w, tmpl+".html", model)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
}

func makeHandler(fn func(http.ResponseWriter, *http.Request, string)) http.HandlerFunc {
	return func(w http.ResponseWriter, req *http.Request) {
		pathParts := validPath.FindStringSubmatch(req.URL.Path)
		if pathParts == nil {
			//u := &url.URL{Path: req.URL.Path}
			//encodedPath := u.String()
			http.Redirect(w, req, "/error/", http.StatusFound)
			return
		}
		fn(w, req, pathParts[3])
	}
}
<|endoftext|>