<commit_before>package preparemedia

import (
	"errors"
	"fmt"
	"math"
	"path"
	"regexp"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/kevintavog/findaphoto/common"
	"github.com/kevintavog/findaphoto/indexer/steps/checkthumbnail"
	"github.com/kevintavog/findaphoto/indexer/steps/resolveplacename"
)

const numConsumers = 8

var queue = make(chan *common.CandidateFile, numConsumers)
var waitGroup sync.WaitGroup

func Start() {
	resolveplacename.Start()

	waitGroup.Add(numConsumers)
	for idx := 0; idx < numConsumers; idx++ {
		go func() {
			dequeue()
			waitGroup.Done()
		}()
	}
}

func Done() {
	close(queue)
}

func Wait() {
	waitGroup.Wait()
	resolveplacename.Done()
	resolveplacename.Wait()
}

func Enqueue(candidate *common.CandidateFile) {
	queue <- candidate
}

func dequeue() {
	for candidate := range queue {
		media := populate(candidate)
		resolveplacename.Enqueue(media)
		checkthumbnail.Enqueue(candidate.FullPath, candidate.AliasedPath, media.MimeType)
	}
}

func populate(candidate *common.CandidateFile) *common.Media {
	media := &common.Media{
		Filename:      path.Base(candidate.FullPath),
		Path:          candidate.AliasedPath,
		Signature:     candidate.Signature,
		LengthInBytes: candidate.LengthInBytes,

		MimeType: candidate.Exif.File.MIMEType,

		ApertureValue:   strconv.FormatFloat(float64(candidate.Exif.EXIF.ApertureValue), 'f', -1, 32),
		ExposureProgram: candidate.Exif.EXIF.ExposureProgram,
		Flash:           candidate.Exif.EXIF.Flash,
		FNumber:         strconv.FormatFloat(float64(candidate.Exif.EXIF.FNumber), 'f', -1, 32),
		FocalLength:     candidate.Exif.EXIF.FocalLength,
		WhiteBalance:    candidate.Exif.EXIF.WhiteBalance,
		LensInfo:        candidate.Exif.EXIF.LensInfo,
		LensModel:       candidate.Exif.EXIF.LensModel,
		CameraMake:      candidate.Exif.EXIF.Make,
		CameraModel:     candidate.Exif.EXIF.Model,
	}

	populateIso(media, candidate)
	populateExposureTime(media, candidate)
	populateKeywords(media, candidate)
	populateDateTime(media, candidate)
	populateLocation(media, candidate)
	populateDimensions(media, candidate)

	media.Warnings = candidate.Warnings

	return media
}

func populateDimensions(media *common.Media, candidate *common.CandidateFile) {
	if candidate.Exif.File.ImageWidth != 0 && candidate.Exif.File.ImageHeight != 0 {
		media.Width = candidate.Exif.File.ImageWidth
		media.Height = candidate.Exif.File.ImageHeight
	} else if candidate.Exif.Quicktime.ImageWidth != 0 && candidate.Exif.Quicktime.ImageHeight != 0 {
		media.Width = candidate.Exif.Quicktime.ImageWidth
		media.Height = candidate.Exif.Quicktime.ImageHeight
	}

	if candidate.Exif.Quicktime.Duration != "" {
		// 10.15 s
		tokens := strings.Split(candidate.Exif.Quicktime.Duration, " ")
		if len(tokens) >= 1 {
			v, err := strconv.ParseFloat(tokens[0], 32)
			if err == nil {
				media.DurationSeconds = float32(v)
			}
		}
	}
}

func populateKeywords(media *common.Media, candidate *common.CandidateFile) {
	switch keyType := candidate.Exif.IPTC.Keywords.(type) {
	default:
		candidate.AddWarning(fmt.Sprintf("Unexpected keyword type %T (%q)", keyType, candidate.Exif.IPTC.Keywords))
	case []interface{}:
		for _, s := range candidate.Exif.IPTC.Keywords.([]interface{}) {
			media.Keywords = append(media.Keywords, s.(string))
		}
	case interface{}:
		media.Keywords = []string{candidate.Exif.IPTC.Keywords.(string)}
	case nil:
		// Nothing to do, keywords not present
	}
}

func populateIso(media *common.Media, candidate *common.CandidateFile) {
	switch isoType := candidate.Exif.EXIF.ISO.(type) {
	default:
		candidate.AddWarning(fmt.Sprintf("Unexpected ISO type: %T (%q)", isoType, candidate.Exif.EXIF.ISO))
	case int:
		media.Iso = strconv.FormatInt(int64(candidate.Exif.EXIF.ISO.(int)), 10)
	case float64:
		media.Iso = strconv.FormatFloat(candidate.Exif.EXIF.ISO.(float64), 'f', -1, 64)
	case string:
		s := candidate.Exif.EXIF.ISO.(string)
		re := regexp.MustCompile("[0-9]+")
		media.Iso = re.FindString(s)
	case nil:
		// Nothing to do, no value present
	}
}

func populateExposureTime(media *common.Media, candidate *common.CandidateFile) {
	switch etType := candidate.Exif.EXIF.ExposureTime.(type) {
	default:
		candidate.AddWarning(fmt.Sprintf("Unexpected ExposureTime type: %T", etType))
	case float64:
		media.ExposureTime = strconv.FormatFloat(candidate.Exif.EXIF.ExposureTime.(float64), 'f', -1, 64)
	case string:
		media.ExposureTime = candidate.Exif.EXIF.ExposureTime.(string)
	case nil:
		// Nothing to do, no value present (videos, for instance)
	}
}

func populateDateTime(media *common.Media, candidate *common.CandidateFile) {
	var dateTime time.Time
	var err error

	if candidate.Exif.Quicktime.ContentCreateDate != "" {
		dateTime, err = time.Parse("2006:01:02 15:04:05-07:00", candidate.Exif.Quicktime.ContentCreateDate)
		if err != nil {
			candidate.AddWarning(fmt.Sprintf(
				"Failed parsing ContentCreateDate '%s': %s (in %s)", candidate.Exif.Quicktime.ContentCreateDate, err.Error(), candidate.FullPath))
		}
	}
	if dateTime.IsZero() && len(candidate.Exif.Quicktime.CreateDate) > 0 {
		// UTC according to spec - no timezone like there is for 'ContentCreateDate'
		dateTime, err = time.Parse("2006:01:02 15:04:05", candidate.Exif.Quicktime.CreateDate)
		if err != nil {
			candidate.AddWarning(fmt.Sprintf(
				"Failed parsing CreateDate '%s': %s (in %s)", candidate.Exif.Quicktime.CreateDate, err.Error(), candidate.FullPath))
		}
	}

	if dateTime.IsZero() {
		exifDateTime := candidate.Exif.EXIF.CreateDate
		if exifDateTime == "" {
			exifDateTime = candidate.Exif.EXIF.DateTimeOriginal
		}
		if exifDateTime == "" {
			exifDateTime = candidate.Exif.EXIF.ModifyDate
		}
		if exifDateTime != "" {
			// No timezone - and the spec doesn't specify.
			dateTime, err = time.ParseInLocation("2006:01:02 15:04:05", exifDateTime, time.Local)
			if err != nil {
				candidate.AddWarning(fmt.Sprintf(
					"Failed parsing '%s': %s (in %s)", exifDateTime, err.Error(), candidate.FullPath))
			}
		}
	}

	if dateTime.IsZero() {
		candidate.AddWarning("No usable date in EXIF, using file timestamp")
		dateTime, err = time.Parse("2006:01:02 15:04:05-07:00", candidate.Exif.File.FileModifyDate)
		if err != nil {
			candidate.AddWarning(fmt.Sprintf(
				"Failed parsing File.FileModifyDate '%s': %s (in %s)", candidate.Exif.File.FileModifyDate, err.Error(), candidate.FullPath))
		}
	}

	if candidate.Exif.File.FileModifyDate != "" {
		fileModifyDateTime, err := time.Parse("2006:01:02 15:04:05-07:00", candidate.Exif.File.FileModifyDate)
		if err != nil {
			candidate.AddWarning(fmt.Sprintf(
				"Failed parsing File.FileModifyDate '%s': %s (in %s)", candidate.Exif.File.FileModifyDate, err.Error(), candidate.FullPath))
		} else {
			// Allow a small amount of difference to account for somefile systems (FAT) that have poor timestamp granularity
			if math.Abs(fileModifyDateTime.Sub(dateTime).Seconds()) > 2 {
				candidate.AddWarning(fmt.Sprintf(
					"File modify date does not match media date (%q - %q)", fileModifyDateTime, dateTime))
			}
		}
	}

	media.Date = dateTime.Format("20060102")
	media.DateTime = dateTime
	media.MonthName = dateTime.Month().String() + " " + dateTime.Month().String()[:3]
	media.DayName = dateTime.Weekday().String() + " " + dateTime.Weekday().String()[:3]
	media.DayOfYear = common.DayOfYearFromDate(dateTime)
}

func populateLocation(media *common.Media, candidate *common.CandidateFile) {
	if candidate.Exif.Composite.GPSPosition != "" {
		if populateWithGpsPosition(media, candidate, candidate.Exif.Composite.GPSPosition) {
			return
		}
	}

	populateWithGpsAndRef(media, candidate, candidate.Exif.EXIF.GPSLatitude, candidate.Exif.EXIF.GPSLatitudeRef, candidate.Exif.EXIF.GPSLongitude, candidate.Exif.EXIF.GPSLongitudeRef)
}

func populateWithGpsPosition(media *common.Media, candidate *common.CandidateFile, gpsPosition string) bool {
	// 47 deg 37' 23.06" N, 122 deg 20' 59.08" W
	// 47 deg 35' 50.66" N, 122 deg 19' 59.50" W == 47.597389 -122.333194
	latAndLongTokens := strings.Split(gpsPosition, ",")
	if len(latAndLongTokens) != 2 {
		candidate.AddWarning(fmt.Sprintf("Unsupported GPSPosition: '%s'", gpsPosition))
		return false
	}

	latitudeValue := strings.Trim(latAndLongTokens[0], " ")
	latitudeTokens := strings.Split(latitudeValue, " ")
	if len(latitudeTokens) != 5 {
		candidate.AddWarning(fmt.Sprintf("Unsupported GPSPosition (latitude): '%s'", gpsPosition))
		return false
	}

	longitudeValue := strings.Trim(latAndLongTokens[1], " ")
	longitudeTokens := strings.Split(longitudeValue, " ")
	if len(longitudeTokens) != 5 {
		candidate.AddWarning(fmt.Sprintf(
			"Unsupported GPSPosition (longitude): '%s' - %s - %s", gpsPosition, latAndLongTokens[1], strings.Join(longitudeTokens, ", ")))
		return false
	}

	var latRef string
	switch latitudeTokens[4] {
	case "N":
		latRef = "North"
	case "S":
		latRef = "South"
	default:
		candidate.AddWarning(fmt.Sprintf("Unsupported GPSPosition (latitude ref): '%s'", gpsPosition))
		return false
	}
	var lonRef string
	switch longitudeTokens[4] {
	case "W":
		lonRef = "West"
	case "E":
		lonRef = "East"
	default:
		candidate.AddWarning(fmt.Sprintf("Unsupported GPSPosition (longitude ref): '%s'", gpsPosition))
		return false
	}

	return populateWithGpsAndRef(media, candidate, strings.TrimRight(latitudeValue, "NSEW "), latRef, strings.TrimRight(longitudeValue, "NSEW "), lonRef)
}

func populateWithGpsAndRef(media *common.Media, candidate *common.CandidateFile, gpsLatitude, gpsLatitudeRef, gpsLongitude, gpsLongitudeRef string) bool {
	// all or nothing for location
	if gpsLatitude == "" && gpsLatitudeRef == "" && gpsLongitude == "" && gpsLongitudeRef == "" {
		return false
	}

	location := fmt.Sprintf("%s %s, %s %s", gpsLatitude, gpsLatitudeRef, gpsLongitude, gpsLongitudeRef)
	if gpsLatitude == "" || gpsLatitudeRef == "" || gpsLongitude == "" || gpsLongitudeRef == "" {
		candidate.AddWarning(fmt.Sprintf("Ignoring poorly formed location: %s", location))
		return false
	}
	if (gpsLatitudeRef != "North" && gpsLatitudeRef != "South") || (gpsLongitudeRef != "West" && gpsLongitudeRef != "East") {
		candidate.AddWarning(fmt.Sprintf("Ignoring poorly formed location - invalid reference: '%s', '%s' (%s)", gpsLatitudeRef, gpsLongitudeRef, location))
		return false
	}

	latFloat, laErr := dmsToFloat(gpsLatitude)
	lonFloat, loErr := dmsToFloat(gpsLongitude)
	if laErr != nil || loErr != nil {
		candidate.AddWarning(fmt.Sprintf("Ignoring location, unable to parse lat/lon %q, %q (%s)", laErr, loErr, location))
		return false
	}

	if gpsLatitudeRef == "South" {
		latFloat = latFloat * -1.0
	}
	if gpsLongitudeRef == "West" {
		lonFloat = lonFloat * -1.0
	}

	media.Location = &common.GeoPoint{Latitude: latFloat, Longitude: lonFloat}
	return true
}

func dmsToFloat(dms string) (float64, error) {
	// 47 deg 37' 23.06"
	// 122 deg 20' 59.08"

	tokens := strings.Split(dms, " ")
	if len(tokens) == 4 {
		strMinutes := tokens[2][:len(tokens[2])-1]
		strSeconds := tokens[3][:len(tokens[3])-1]

		degrees, dErr := strconv.Atoi(tokens[0])
		minutes, mErr := strconv.Atoi(strMinutes)
		seconds, sErr := strconv.ParseFloat(strSeconds, 64)

		if dErr == nil && mErr == nil && sErr == nil {
			return float64(degrees) + (float64(minutes) / 60.0) + (seconds / 3600.0), nil
		} else {
			return math.NaN(), errors.New(fmt.Sprintf("Unable to convert: %q, %q, %q", dErr, mErr, sErr))
		}
	}
	return math.NaN(), errors.New(fmt.Sprintf("Invalid DMS (wrong number of tokens): %s", dms))
}
<commit_msg>For videos, prefer QuickTime.ContentDate over QuickTime.ContentCreateDate - exiftool doesn't seem to be able to update ContentCreateDate<commit_after>package preparemedia

import (
	"errors"
	"fmt"
	"math"
	"path"
	"regexp"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/kevintavog/findaphoto/common"
	"github.com/kevintavog/findaphoto/indexer/steps/checkthumbnail"
	"github.com/kevintavog/findaphoto/indexer/steps/resolveplacename"
)

const numConsumers = 8

var queue = make(chan *common.CandidateFile, numConsumers)
var waitGroup sync.WaitGroup

func Start() {
	resolveplacename.Start()

	waitGroup.Add(numConsumers)
	for idx := 0; idx < numConsumers; idx++ {
		go func() {
			dequeue()
			waitGroup.Done()
		}()
	}
}

func Done() {
	close(queue)
}

func Wait() {
	waitGroup.Wait()
	resolveplacename.Done()
	resolveplacename.Wait()
}

func Enqueue(candidate *common.CandidateFile) {
	queue <- candidate
}

func dequeue() {
	for candidate := range queue {
		media := populate(candidate)
		resolveplacename.Enqueue(media)
		checkthumbnail.Enqueue(candidate.FullPath, candidate.AliasedPath, media.MimeType)
	}
}

func populate(candidate *common.CandidateFile) *common.Media {
	media := &common.Media{
		Filename:      path.Base(candidate.FullPath),
		Path:          candidate.AliasedPath,
		Signature:     candidate.Signature,
		LengthInBytes: candidate.LengthInBytes,

		MimeType: candidate.Exif.File.MIMEType,

		ApertureValue:   strconv.FormatFloat(float64(candidate.Exif.EXIF.ApertureValue), 'f', -1, 32),
		ExposureProgram: candidate.Exif.EXIF.ExposureProgram,
		Flash:           candidate.Exif.EXIF.Flash,
		FNumber:         strconv.FormatFloat(float64(candidate.Exif.EXIF.FNumber), 'f', -1, 32),
		FocalLength:     candidate.Exif.EXIF.FocalLength,
		WhiteBalance:    candidate.Exif.EXIF.WhiteBalance,
		LensInfo:        candidate.Exif.EXIF.LensInfo,
		LensModel:       candidate.Exif.EXIF.LensModel,
		CameraMake:      candidate.Exif.EXIF.Make,
		CameraModel:     candidate.Exif.EXIF.Model,
	}

	populateIso(media, candidate)
	populateExposureTime(media, candidate)
	populateKeywords(media, candidate)
	populateDateTime(media, candidate)
	populateLocation(media, candidate)
	populateDimensions(media, candidate)

	media.Warnings = candidate.Warnings

	return media
}

func populateDimensions(media *common.Media, candidate *common.CandidateFile) {
	if candidate.Exif.File.ImageWidth != 0 && candidate.Exif.File.ImageHeight != 0 {
		media.Width = candidate.Exif.File.ImageWidth
		media.Height = candidate.Exif.File.ImageHeight
	} else if candidate.Exif.Quicktime.ImageWidth != 0 && candidate.Exif.Quicktime.ImageHeight != 0 {
		media.Width = candidate.Exif.Quicktime.ImageWidth
		media.Height = candidate.Exif.Quicktime.ImageHeight
	}

	if candidate.Exif.Quicktime.Duration != "" {
		// 10.15 s
		tokens := strings.Split(candidate.Exif.Quicktime.Duration, " ")
		if len(tokens) >= 1 {
			v, err := strconv.ParseFloat(tokens[0], 32)
			if err == nil {
				media.DurationSeconds = float32(v)
			}
		}
	}
}

func populateKeywords(media *common.Media, candidate *common.CandidateFile) {
	switch keyType := candidate.Exif.IPTC.Keywords.(type) {
	default:
		candidate.AddWarning(fmt.Sprintf("Unexpected keyword type %T (%q)", keyType, candidate.Exif.IPTC.Keywords))
	case []interface{}:
		for _, s := range candidate.Exif.IPTC.Keywords.([]interface{}) {
			media.Keywords = append(media.Keywords, s.(string))
		}
	case interface{}:
		media.Keywords = []string{candidate.Exif.IPTC.Keywords.(string)}
	case nil:
		// Nothing to do, keywords not present
	}
}

func populateIso(media *common.Media, candidate *common.CandidateFile) {
	switch isoType := candidate.Exif.EXIF.ISO.(type) {
	default:
		candidate.AddWarning(fmt.Sprintf("Unexpected ISO type: %T (%q)", isoType, candidate.Exif.EXIF.ISO))
	case int:
		media.Iso = strconv.FormatInt(int64(candidate.Exif.EXIF.ISO.(int)), 10)
	case float64:
		media.Iso = strconv.FormatFloat(candidate.Exif.EXIF.ISO.(float64), 'f', -1, 64)
	case string:
		s := candidate.Exif.EXIF.ISO.(string)
		re := regexp.MustCompile("[0-9]+")
		media.Iso = re.FindString(s)
	case nil:
		// Nothing to do, no value present
	}
}

func populateExposureTime(media *common.Media, candidate *common.CandidateFile) {
	switch etType := candidate.Exif.EXIF.ExposureTime.(type) {
	default:
		candidate.AddWarning(fmt.Sprintf("Unexpected ExposureTime type: %T", etType))
	case float64:
		media.ExposureTime = strconv.FormatFloat(candidate.Exif.EXIF.ExposureTime.(float64), 'f', -1, 64)
	case string:
		media.ExposureTime = candidate.Exif.EXIF.ExposureTime.(string)
	case nil:
		// Nothing to do, no value present (videos, for instance)
	}
}

func populateDateTime(media *common.Media, candidate *common.CandidateFile) {
	var dateTime time.Time
	var err error

	if dateTime.IsZero() && candidate.Exif.Quicktime.CreateDate != "" {
		// UTC according to spec - no timezone like there is for 'ContentCreateDate'
		dateTime, err = time.Parse("2006:01:02 15:04:05", candidate.Exif.Quicktime.CreateDate)
		if err != nil {
			candidate.AddWarning(fmt.Sprintf(
				"Failed parsing CreateDate '%s': %s (in %s)", candidate.Exif.Quicktime.CreateDate, err.Error(), candidate.FullPath))
		}
	}

	if dateTime.IsZero() && candidate.Exif.Quicktime.ContentCreateDate != "" {
		dateTime, err = time.Parse("2006:01:02 15:04:05-07:00", candidate.Exif.Quicktime.ContentCreateDate)
		if err != nil {
			candidate.AddWarning(fmt.Sprintf(
				"Failed parsing ContentCreateDate '%s': %s (in %s)", candidate.Exif.Quicktime.ContentCreateDate, err.Error(), candidate.FullPath))
		}
	}

	if dateTime.IsZero() {
		exifDateTime := candidate.Exif.EXIF.CreateDate
		if exifDateTime == "" {
			exifDateTime = candidate.Exif.EXIF.DateTimeOriginal
		}
		if exifDateTime == "" {
			exifDateTime = candidate.Exif.EXIF.ModifyDate
		}
		if exifDateTime != "" {
			// No timezone - and the spec doesn't specify.
			dateTime, err = time.ParseInLocation("2006:01:02 15:04:05", exifDateTime, time.Local)
			if err != nil {
				candidate.AddWarning(fmt.Sprintf(
					"Failed parsing '%s': %s (in %s)", exifDateTime, err.Error(), candidate.FullPath))
			}
		}
	}

	if dateTime.IsZero() {
		candidate.AddWarning("No usable date in EXIF, using file timestamp")
		dateTime, err = time.Parse("2006:01:02 15:04:05-07:00", candidate.Exif.File.FileModifyDate)
		if err != nil {
			candidate.AddWarning(fmt.Sprintf(
				"Failed parsing File.FileModifyDate '%s': %s (in %s)", candidate.Exif.File.FileModifyDate, err.Error(), candidate.FullPath))
		}
	}

	if candidate.Exif.File.FileModifyDate != "" {
		fileModifyDateTime, err := time.Parse("2006:01:02 15:04:05-07:00", candidate.Exif.File.FileModifyDate)
		if err != nil {
			candidate.AddWarning(fmt.Sprintf(
				"Failed parsing File.FileModifyDate '%s': %s (in %s)", candidate.Exif.File.FileModifyDate, err.Error(), candidate.FullPath))
		} else {
			// Allow a small amount of difference to account for somefile systems (FAT) that have poor timestamp granularity
			if math.Abs(fileModifyDateTime.Sub(dateTime).Seconds()) > 2 {
				candidate.AddWarning(fmt.Sprintf(
					"File modify date does not match media date (%q - %q)", fileModifyDateTime, dateTime))
			}
		}
	}

	media.Date = dateTime.Format("20060102")
	media.DateTime = dateTime
	media.MonthName = dateTime.Month().String() + " " + dateTime.Month().String()[:3]
	media.DayName = dateTime.Weekday().String() + " " + dateTime.Weekday().String()[:3]
	media.DayOfYear = common.DayOfYearFromDate(dateTime)
}

func populateLocation(media *common.Media, candidate *common.CandidateFile) {
	if candidate.Exif.Composite.GPSPosition != "" {
		if populateWithGpsPosition(media, candidate, candidate.Exif.Composite.GPSPosition) {
			return
		}
	}

	populateWithGpsAndRef(media, candidate, candidate.Exif.EXIF.GPSLatitude, candidate.Exif.EXIF.GPSLatitudeRef, candidate.Exif.EXIF.GPSLongitude, candidate.Exif.EXIF.GPSLongitudeRef)
}

func populateWithGpsPosition(media *common.Media, candidate *common.CandidateFile, gpsPosition string) bool {
	// 47 deg 37' 23.06" N, 122 deg 20' 59.08" W
	// 47 deg 35' 50.66" N, 122 deg 19' 59.50" W == 47.597389 -122.333194
	latAndLongTokens := strings.Split(gpsPosition, ",")
	if len(latAndLongTokens) != 2 {
		candidate.AddWarning(fmt.Sprintf("Unsupported GPSPosition: '%s'", gpsPosition))
		return false
	}

	latitudeValue := strings.Trim(latAndLongTokens[0], " ")
	latitudeTokens := strings.Split(latitudeValue, " ")
	if len(latitudeTokens) != 5 {
		candidate.AddWarning(fmt.Sprintf("Unsupported GPSPosition (latitude): '%s'", gpsPosition))
		return false
	}

	longitudeValue := strings.Trim(latAndLongTokens[1], " ")
	longitudeTokens := strings.Split(longitudeValue, " ")
	if len(longitudeTokens) != 5 {
		candidate.AddWarning(fmt.Sprintf(
			"Unsupported GPSPosition (longitude): '%s' - %s - %s", gpsPosition, latAndLongTokens[1], strings.Join(longitudeTokens, ", ")))
		return false
	}

	var latRef string
	switch latitudeTokens[4] {
	case "N":
		latRef = "North"
	case "S":
		latRef = "South"
	default:
		candidate.AddWarning(fmt.Sprintf("Unsupported GPSPosition (latitude ref): '%s'", gpsPosition))
		return false
	}
	var lonRef string
	switch longitudeTokens[4] {
	case "W":
		lonRef = "West"
	case "E":
		lonRef = "East"
	default:
		candidate.AddWarning(fmt.Sprintf("Unsupported GPSPosition (longitude ref): '%s'", gpsPosition))
		return false
	}

	return populateWithGpsAndRef(media, candidate, strings.TrimRight(latitudeValue, "NSEW "), latRef, strings.TrimRight(longitudeValue, "NSEW "), lonRef)
}

func populateWithGpsAndRef(media *common.Media, candidate *common.CandidateFile, gpsLatitude, gpsLatitudeRef, gpsLongitude, gpsLongitudeRef string) bool {
	// all or nothing for location
	if gpsLatitude == "" && gpsLatitudeRef == "" && gpsLongitude == "" && gpsLongitudeRef == "" {
		return false
	}

	location := fmt.Sprintf("%s %s, %s %s", gpsLatitude, gpsLatitudeRef, gpsLongitude, gpsLongitudeRef)
	if gpsLatitude == "" || gpsLatitudeRef == "" || gpsLongitude == "" || gpsLongitudeRef == "" {
		candidate.AddWarning(fmt.Sprintf("Ignoring poorly formed location: %s", location))
		return false
	}
	if (gpsLatitudeRef != "North" && gpsLatitudeRef != "South") || (gpsLongitudeRef != "West" && gpsLongitudeRef != "East") {
		candidate.AddWarning(fmt.Sprintf("Ignoring poorly formed location - invalid reference: '%s', '%s' (%s)", gpsLatitudeRef, gpsLongitudeRef, location))
		return false
	}

	latFloat, laErr := dmsToFloat(gpsLatitude)
	lonFloat, loErr := dmsToFloat(gpsLongitude)
	if laErr != nil || loErr != nil {
		candidate.AddWarning(fmt.Sprintf("Ignoring location, unable to parse lat/lon %q, %q (%s)", laErr, loErr, location))
		return false
	}

	if gpsLatitudeRef == "South" {
		latFloat = latFloat * -1.0
	}
	if gpsLongitudeRef == "West" {
		lonFloat = lonFloat * -1.0
	}

	media.Location = &common.GeoPoint{Latitude: latFloat, Longitude: lonFloat}
	return true
}

func dmsToFloat(dms string) (float64, error) {
	// 47 deg 37' 23.06"
	// 122 deg 20' 59.08"

	tokens := strings.Split(dms, " ")
	if len(tokens) == 4 {
		strMinutes := tokens[2][:len(tokens[2])-1]
		strSeconds := tokens[3][:len(tokens[3])-1]

		degrees, dErr := strconv.Atoi(tokens[0])
		minutes, mErr := strconv.Atoi(strMinutes)
		seconds, sErr := strconv.ParseFloat(strSeconds, 64)

		if dErr == nil && mErr == nil && sErr == nil {
			return float64(degrees) + (float64(minutes) / 60.0) + (seconds / 3600.0), nil
		} else {
			return math.NaN(), errors.New(fmt.Sprintf("Unable to convert: %q, %q, %q", dErr, mErr, sErr))
		}
	}
	return math.NaN(), errors.New(fmt.Sprintf("Invalid DMS (wrong number of tokens): %s", dms))
}
<|endoftext|>


<commit_before>// Copyright 2017 gf Author(https://github.com/gogf/gf). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.

// Package gfile provides easy-to-use operations for file system.
package gfile

import (
	"github.com/gogf/gf/text/gstr"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"time"

	"github.com/gogf/gf/container/gtype"
	"github.com/gogf/gf/util/gconv"
)

const (
	// Separator for file system.
	Separator = string(filepath.Separator)
)

var (
	// DefaultPerm is the default perm for file opening.
	DefaultPermOpen = os.FileMode(0666)

	// DefaultPermCopy is the default perm for file/folder copy.
	DefaultPermCopy = os.FileMode(0777)

	// The absolute file path for main package.
	// It can be only checked and set once.
	mainPkgPath = gtype.NewString()

	// selfPath is the current running binary path.
	// As it is most commonly used, it is so defined as an internal package variable.
	selfPath = ""

	// Temporary directory of system.
	tempDir = "/tmp"
)

func init() {
	// Initialize internal package variable: tempDir.
	if Separator != "/" || !Exists(tempDir) {
		tempDir = os.TempDir()
	}
	// Initialize internal package variable: selfPath.
	selfPath, _ = exec.LookPath(os.Args[0])
	if selfPath != "" {
		selfPath, _ = filepath.Abs(selfPath)
	}
	if selfPath == "" {
		selfPath, _ = filepath.Abs(os.Args[0])
	}
}

// Mkdir creates directories recursively with given <path>.
// The parameter <path> is suggested to be absolute path.
func Mkdir(path string) error {
	err := os.MkdirAll(path, os.ModePerm)
	if err != nil {
		return err
	}
	return nil
}

// Create creates file with given <path> recursively.
// The parameter <path> is suggested to be absolute path.
func Create(path string) (*os.File, error) {
	dir := Dir(path)
	if !Exists(dir) {
		if err := Mkdir(dir); err != nil {
			return nil, err
		}
	}
	return os.Create(path)
}

// Open opens file/directory READONLY.
func Open(path string) (*os.File, error) {
	return os.Open(path)
}

// OpenFile opens file/directory with custom <flag> and <perm>.
// The parameter <flag> is like: O_RDONLY, O_RDWR, O_RDWR|O_CREATE|O_TRUNC, etc.
func OpenFile(path string, flag int, perm os.FileMode) (*os.File, error) {
	return os.OpenFile(path, flag, perm)
}

// OpenWithFlag opens file/directory with default perm and custom <flag>.
// The default <perm> is 0666.
// The parameter <flag> is like: O_RDONLY, O_RDWR, O_RDWR|O_CREATE|O_TRUNC, etc.
func OpenWithFlag(path string, flag int) (*os.File, error) {
	f, err := os.OpenFile(path, flag, DefaultPermOpen)
	if err != nil {
		return nil, err
	}
	return f, nil
}

// OpenWithFlagPerm opens file/directory with custom <flag> and <perm>.
// The parameter <flag> is like: O_RDONLY, O_RDWR, O_RDWR|O_CREATE|O_TRUNC, etc.
// The parameter <perm> is like: 0600, 0666, 0777, etc.
func OpenWithFlagPerm(path string, flag int, perm os.FileMode) (*os.File, error) {
	f, err := os.OpenFile(path, flag, perm)
	if err != nil {
		return nil, err
	}
	return f, nil
}

// Join joins string array paths with file separator of current system.
func Join(paths ...string) string {
	var s string
	for _, path := range paths {
		if s != "" {
			s += Separator
		}
		s += gstr.TrimRight(path, Separator)
	}
	return s
}

// Exists checks whether given <path> exist.
func Exists(path string) bool {
	if stat, err := os.Stat(path); stat != nil && !os.IsNotExist(err) {
		return true
	}
	return false
}

// IsDir checks whether given <path> a directory.
// Note that it returns false if the <path> does not exist.
func IsDir(path string) bool {
	s, err := os.Stat(path)
	if err != nil {
		return false
	}
	return s.IsDir()
}

// Pwd returns absolute path of current working directory.
// Note that it returns an empty string if retrieving current
// working directory failed.
func Pwd() string {
	path, err := os.Getwd()
	if err != nil {
		return ""
	}
	return path
}

// Chdir changes the current working directory to the named directory.
// If there is an error, it will be of type *PathError.
func Chdir(dir string) error {
	return os.Chdir(dir)
}

// IsFile checks whether given <path> a file, which means it's not a directory.
// Note that it returns false if the <path> does not exist.
func IsFile(path string) bool {
	s, err := os.Stat(path)
	if err != nil {
		return false
	}
	return !s.IsDir()
}

// Alias of Stat.
// See Stat.
func Info(path string) (os.FileInfo, error) {
	return Stat(path)
}

// Stat returns a FileInfo describing the named file.
// If there is an error, it will be of type *PathError.
func Stat(path string) (os.FileInfo, error) {
	return os.Stat(path)
}

// Move renames (moves) <src> to <dst> path.
// If <dst> already exists and is not a directory, it'll be replaced.
func Move(src string, dst string) error {
	return os.Rename(src, dst)
}

// Rename is alias of Move.
// See Move.
func Rename(src string, dst string) error {
	return Move(src, dst)
}

// DirNames returns sub-file names of given directory <path>.
// Note that the returned names are NOT absolute paths.
func DirNames(path string) ([]string, error) {
	f, err := os.Open(path)
	if err != nil {
		return nil, err
	}
	list, err := f.Readdirnames(-1)
	f.Close()
	if err != nil {
		return nil, err
	}
	return list, nil
}

// Glob returns the names of all files matching pattern or nil
// if there is no matching file. The syntax of patterns is the same
// as in Match. The pattern may describe hierarchical names such as
// /usr/*/bin/ed (assuming the Separator is '/').
//
// Glob ignores file system errors such as I/O errors reading directories.
// The only possible returned error is ErrBadPattern, when pattern
// is malformed.
func Glob(pattern string, onlyNames ...bool) ([]string, error) {
	if list, err := filepath.Glob(pattern); err == nil {
		if len(onlyNames) > 0 && onlyNames[0] && len(list) > 0 {
			array := make([]string, len(list))
			for k, v := range list {
				array[k] = Basename(v)
			}
			return array, nil
		}
		return list, nil
	} else {
		return nil, err
	}
}

// Remove deletes all file/directory with <path> parameter.
// If parameter <path> is directory, it deletes it recursively.
func Remove(path string) error {
	return os.RemoveAll(path)
}

// IsReadable checks whether given <path> is readable.
func IsReadable(path string) bool {
	result := true
	file, err := os.OpenFile(path, os.O_RDONLY, DefaultPermOpen)
	if err != nil {
		result = false
	}
	file.Close()
	return result
}

// IsWritable checks whether given <path> is writable.
//
// TODO improve performance; use golang.org/x/sys to cross-plat-form
func IsWritable(path string) bool {
	result := true
	if IsDir(path) {
		// If it's a directory, create a temporary file to test whether it's writable.
		tmpFile := strings.TrimRight(path, Separator) + Separator + gconv.String(time.Now().UnixNano())
		if f, err := Create(tmpFile); err != nil || !Exists(tmpFile) {
			result = false
		} else {
			f.Close()
			Remove(tmpFile)
		}
	} else {
		// 如果是文件，那么判断文件是否可打开
		file, err := os.OpenFile(path, os.O_WRONLY, DefaultPermOpen)
		if err != nil {
			result = false
		}
		file.Close()
	}
	return result
}

// See os.Chmod.
func Chmod(path string, mode os.FileMode) error {
	return os.Chmod(path, mode)
}

// Abs returns an absolute representation of path.
// If the path is not absolute it will be joined with the current
// working directory to turn it into an absolute path. The absolute
// path name for a given file is not guaranteed to be unique.
// Abs calls Clean on the result.
func Abs(path string) string {
	p, _ := filepath.Abs(path)
	return p
}

// RealPath converts the given <path> to its absolute path
// and checks if the file path exists.
// If the file does not exist, return an empty string.
func RealPath(path string) string {
	p, err := filepath.Abs(path)
	if err != nil {
		return ""
	}
	if !Exists(p) {
		return ""
	}
	return p
}

// SelfPath returns absolute file path of current running process(binary).
func SelfPath() string {
	return selfPath
}

// SelfName returns file name of current running process(binary).
func SelfName() string {
	return Basename(SelfPath())
}

// SelfDir returns absolute directory path of current running process(binary).
func SelfDir() string {
	return filepath.Dir(SelfPath())
}

// Basename returns the last element of path, which contains file extension.
// Trailing path separators are removed before extracting the last element.
// If the path is empty, Base returns ".".
// If the path consists entirely of separators, Basename returns a single separator.
// Example:
// /var/www/file.js -> file.js
// file.js          -> file.js
func Basename(path string) string {
	return filepath.Base(path)
}

// Name returns the last element of path without file extension.
// Example:
// /var/www/file.js -> file
// file.js          -> file
func Name(path string) string {
	base := filepath.Base(path)
	if i := strings.LastIndexByte(base, '.'); i != -1 {
		return base[:i]
	}
	return base
}

// Dir returns all but the last element of path, typically the path's directory.
// After dropping the final element, Dir calls Clean on the path and trailing
// slashes are removed.
// If the path is empty, Dir returns ".".
// If the path consists entirely of separators, Dir returns a single separator.
// The returned path does not end in a separator unless it is the root directory.
func Dir(path string) string {
	return filepath.Dir(path)
}

// IsEmpty checks whether the given <path> is empty.
// If <path> is a folder, it checks if there's any file under it.
// If <path> is a file, it checks if the file size is zero.
//
// Note that it returns true if <path> does not exist.
func IsEmpty(path string) bool {
	stat, err := Stat(path)
	if err != nil {
		return true
	}
	if stat.IsDir() {
		file, err := os.Open(path)
		if err != nil {
			return true
		}
		defer file.Close()
		names, err := file.Readdirnames(-1)
		if err != nil {
			return true
		}
		return len(names) == 0
	} else {
		return stat.Size() == 0
	}
}

// Ext returns the file name extension used by path.
// The extension is the suffix beginning at the final dot
// in the final element of path; it is empty if there is
// no dot.
//
// Note: the result contains symbol '.'.
func Ext(path string) string {
	ext := filepath.Ext(path)
	if p := strings.IndexByte(ext, '?'); p != -1 {
		ext = ext[0:p]
	}
	return ext
}

// ExtName is like function Ext, which returns the file name extension used by path,
// but the result does not contains symbol '.'.
func ExtName(path string) string {
	return strings.TrimLeft(Ext(path), ".")
}

// TempDir retrieves and returns the temporary directory of current system.
// It return "/tmp" is current in *nix system, or else it returns os.TempDir().
//
// The optional parameter <names> specifies the its sub-folders/sub-files,
// which will be joined with current system separator and returned with the path.
func TempDir(names ...string) string {
	path := tempDir
	for _, name := range names {
		path += Separator + name
	}
	return path
}
<commit_msg>move Separator from const to variable for package gfile<commit_after>// Copyright 2017 gf Author(https://github.com/gogf/gf). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.

// Package gfile provides easy-to-use operations for file system.
package gfile

import (
	"github.com/gogf/gf/text/gstr"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"time"

	"github.com/gogf/gf/container/gtype"
	"github.com/gogf/gf/util/gconv"
)

var (
	// Separator for file system.
	// It here defines the separator as variable
	// to allow it modified by developer if necessary.
	Separator = string(filepath.Separator)

	// DefaultPerm is the default perm for file opening.
	DefaultPermOpen = os.FileMode(0666)

	// DefaultPermCopy is the default perm for file/folder copy.
	DefaultPermCopy = os.FileMode(0777)

	// The absolute file path for main package.
	// It can be only checked and set once.
	mainPkgPath = gtype.NewString()

	// selfPath is the current running binary path.
	// As it is most commonly used, it is so defined as an internal package variable.
	selfPath = ""

	// Temporary directory of system.
	tempDir = "/tmp"
)

func init() {
	// Initialize internal package variable: tempDir.
	if Separator != "/" || !Exists(tempDir) {
		tempDir = os.TempDir()
	}
	// Initialize internal package variable: selfPath.
	selfPath, _ = exec.LookPath(os.Args[0])
	if selfPath != "" {
		selfPath, _ = filepath.Abs(selfPath)
	}
	if selfPath == "" {
		selfPath, _ = filepath.Abs(os.Args[0])
	}
}

// Mkdir creates directories recursively with given <path>.
// The parameter <path> is suggested to be absolute path.
func Mkdir(path string) error {
	err := os.MkdirAll(path, os.ModePerm)
	if err != nil {
		return err
	}
	return nil
}

// Create creates file with given <path> recursively.
// The parameter <path> is suggested to be absolute path.
func Create(path string) (*os.File, error) {
	dir := Dir(path)
	if !Exists(dir) {
		if err := Mkdir(dir); err != nil {
			return nil, err
		}
	}
	return os.Create(path)
}

// Open opens file/directory READONLY.
func Open(path string) (*os.File, error) {
	return os.Open(path)
}

// OpenFile opens file/directory with custom <flag> and <perm>.
// The parameter <flag> is like: O_RDONLY, O_RDWR, O_RDWR|O_CREATE|O_TRUNC, etc.
func OpenFile(path string, flag int, perm os.FileMode) (*os.File, error) {
	return os.OpenFile(path, flag, perm)
}

// OpenWithFlag opens file/directory with default perm and custom <flag>.
// The default <perm> is 0666.
// The parameter <flag> is like: O_RDONLY, O_RDWR, O_RDWR|O_CREATE|O_TRUNC, etc.
func OpenWithFlag(path string, flag int) (*os.File, error) {
	f, err := os.OpenFile(path, flag, DefaultPermOpen)
	if err != nil {
		return nil, err
	}
	return f, nil
}

// OpenWithFlagPerm opens file/directory with custom <flag> and <perm>.
// The parameter <flag> is like: O_RDONLY, O_RDWR, O_RDWR|O_CREATE|O_TRUNC, etc.
// The parameter <perm> is like: 0600, 0666, 0777, etc.
func OpenWithFlagPerm(path string, flag int, perm os.FileMode) (*os.File, error) {
	f, err := os.OpenFile(path, flag, perm)
	if err != nil {
		return nil, err
	}
	return f, nil
}

// Join joins string array paths with file separator of current system.
func Join(paths ...string) string {
	var s string
	for _, path := range paths {
		if s != "" {
			s += Separator
		}
		s += gstr.TrimRight(path, Separator)
	}
	return s
}

// Exists checks whether given <path> exist.
func Exists(path string) bool {
	if stat, err := os.Stat(path); stat != nil && !os.IsNotExist(err) {
		return true
	}
	return false
}

// IsDir checks whether given <path> a directory.
// Note that it returns false if the <path> does not exist.
func IsDir(path string) bool {
	s, err := os.Stat(path)
	if err != nil {
		return false
	}
	return s.IsDir()
}

// Pwd returns absolute path of current working directory.
// Note that it returns an empty string if retrieving current
// working directory failed.
func Pwd() string {
	path, err := os.Getwd()
	if err != nil {
		return ""
	}
	return path
}

// Chdir changes the current working directory to the named directory.
// If there is an error, it will be of type *PathError.
func Chdir(dir string) error {
	return os.Chdir(dir)
}

// IsFile checks whether given <path> a file, which means it's not a directory.
// Note that it returns false if the <path> does not exist.
func IsFile(path string) bool {
	s, err := os.Stat(path)
	if err != nil {
		return false
	}
	return !s.IsDir()
}

// Alias of Stat.
// See Stat.
func Info(path string) (os.FileInfo, error) {
	return Stat(path)
}

// Stat returns a FileInfo describing the named file.
// If there is an error, it will be of type *PathError.
func Stat(path string) (os.FileInfo, error) {
	return os.Stat(path)
}

// Move renames (moves) <src> to <dst> path.
// If <dst> already exists and is not a directory, it'll be replaced.
func Move(src string, dst string) error {
	return os.Rename(src, dst)
}

// Rename is alias of Move.
// See Move.
func Rename(src string, dst string) error {
	return Move(src, dst)
}

// DirNames returns sub-file names of given directory <path>.
// Note that the returned names are NOT absolute paths.
func DirNames(path string) ([]string, error) {
	f, err := os.Open(path)
	if err != nil {
		return nil, err
	}
	list, err := f.Readdirnames(-1)
	f.Close()
	if err != nil {
		return nil, err
	}
	return list, nil
}

// Glob returns the names of all files matching pattern or nil
// if there is no matching file. The syntax of patterns is the same
// as in Match. The pattern may describe hierarchical names such as
// /usr/*/bin/ed (assuming the Separator is '/').
//
// Glob ignores file system errors such as I/O errors reading directories.
// The only possible returned error is ErrBadPattern, when pattern
// is malformed.
func Glob(pattern string, onlyNames ...bool) ([]string, error) {
	if list, err := filepath.Glob(pattern); err == nil {
		if len(onlyNames) > 0 && onlyNames[0] && len(list) > 0 {
			array := make([]string, len(list))
			for k, v := range list {
				array[k] = Basename(v)
			}
			return array, nil
		}
		return list, nil
	} else {
		return nil, err
	}
}

// Remove deletes all file/directory with <path> parameter.
// If parameter <path> is directory, it deletes it recursively.
func Remove(path string) error {
	return os.RemoveAll(path)
}

// IsReadable checks whether given <path> is readable.
func IsReadable(path string) bool {
	result := true
	file, err := os.OpenFile(path, os.O_RDONLY, DefaultPermOpen)
	if err != nil {
		result = false
	}
	file.Close()
	return result
}

// IsWritable checks whether given <path> is writable.
//
// TODO improve performance; use golang.org/x/sys to cross-plat-form
func IsWritable(path string) bool {
	result := true
	if IsDir(path) {
		// If it's a directory, create a temporary file to test whether it's writable.
		tmpFile := strings.TrimRight(path, Separator) + Separator + gconv.String(time.Now().UnixNano())
		if f, err := Create(tmpFile); err != nil || !Exists(tmpFile) {
			result = false
		} else {
			f.Close()
			Remove(tmpFile)
		}
	} else {
		// 如果是文件，那么判断文件是否可打开
		file, err := os.OpenFile(path, os.O_WRONLY, DefaultPermOpen)
		if err != nil {
			result = false
		}
		file.Close()
	}
	return result
}

// See os.Chmod.
func Chmod(path string, mode os.FileMode) error {
	return os.Chmod(path, mode)
}

// Abs returns an absolute representation of path.
// If the path is not absolute it will be joined with the current
// working directory to turn it into an absolute path. The absolute
// path name for a given file is not guaranteed to be unique.
// Abs calls Clean on the result.
func Abs(path string) string {
	p, _ := filepath.Abs(path)
	return p
}

// RealPath converts the given <path> to its absolute path
// and checks if the file path exists.
// If the file does not exist, return an empty string.
func RealPath(path string) string {
	p, err := filepath.Abs(path)
	if err != nil {
		return ""
	}
	if !Exists(p) {
		return ""
	}
	return p
}

// SelfPath returns absolute file path of current running process(binary).
func SelfPath() string {
	return selfPath
}

// SelfName returns file name of current running process(binary).
func SelfName() string {
	return Basename(SelfPath())
}

// SelfDir returns absolute directory path of current running process(binary).
func SelfDir() string {
	return filepath.Dir(SelfPath())
}

// Basename returns the last element of path, which contains file extension.
// Trailing path separators are removed before extracting the last element.
// If the path is empty, Base returns ".".
// If the path consists entirely of separators, Basename returns a single separator.
// Example:
// /var/www/file.js -> file.js
// file.js          -> file.js
func Basename(path string) string {
	return filepath.Base(path)
}

// Name returns the last element of path without file extension.
// Example:
// /var/www/file.js -> file
// file.js          -> file
func Name(path string) string {
	base := filepath.Base(path)
	if i := strings.LastIndexByte(base, '.'); i != -1 {
		return base[:i]
	}
	return base
}

// Dir returns all but the last element of path, typically the path's directory.
// After dropping the final element, Dir calls Clean on the path and trailing
// slashes are removed.
// If the path is empty, Dir returns ".".
// If the path consists entirely of separators, Dir returns a single separator.
// The returned path does not end in a separator unless it is the root directory.
func Dir(path string) string {
	return filepath.Dir(path)
}

// IsEmpty checks whether the given <path> is empty.
// If <path> is a folder, it checks if there's any file under it.
// If <path> is a file, it checks if the file size is zero.
//
// Note that it returns true if <path> does not exist.
func IsEmpty(path string) bool {
	stat, err := Stat(path)
	if err != nil {
		return true
	}
	if stat.IsDir() {
		file, err := os.Open(path)
		if err != nil {
			return true
		}
		defer file.Close()
		names, err := file.Readdirnames(-1)
		if err != nil {
			return true
		}
		return len(names) == 0
	} else {
		return stat.Size() == 0
	}
}

// Ext returns the file name extension used by path.
// The extension is the suffix beginning at the final dot
// in the final element of path; it is empty if there is
// no dot.
//
// Note: the result contains symbol '.'.
func Ext(path string) string {
	ext := filepath.Ext(path)
	if p := strings.IndexByte(ext, '?'); p != -1 {
		ext = ext[0:p]
	}
	return ext
}

// ExtName is like function Ext, which returns the file name extension used by path,
// but the result does not contains symbol '.'.
func ExtName(path string) string {
	return strings.TrimLeft(Ext(path), ".")
}

// TempDir retrieves and returns the temporary directory of current system.
// It return "/tmp" is current in *nix system, or else it returns os.TempDir().
//
// The optional parameter <names> specifies the its sub-folders/sub-files,
// which will be joined with current system separator and returned with the path.
func TempDir(names ...string) string {
	path := tempDir
	for _, name := range names {
		path += Separator + name
	}
	return path
}
<|endoftext|>


<commit_before>package buffalo

import (
	"io"
	"io/fs"
	"testing"

	"github.com/gobuffalo/buffalo/internal/testdata/embedded"
	"github.com/stretchr/testify/require"
)

func Test_FS_Disallows_Parent_Folders(t *testing.T) {
	r := require.New(t)

	fsys := NewFS(embedded.FS(), "internal/testdata/disk")
	r.NotNil(fsys)

	f, err := fsys.Open("../panic.txt")
	r.ErrorIs(err, fs.ErrNotExist)
	r.Nil(f)

	f, err = fsys.Open("try/../to/../trick/../panic.txt")
	r.ErrorIs(err, fs.ErrNotExist)
	r.Nil(f)
}

func Test_FS_Hides_embed_go(t *testing.T) {
	r := require.New(t)

	fsys := NewFS(embedded.FS(), "internal/testdata/disk")
	r.NotNil(fsys)

	f, err := fsys.Open("embed.go")
	r.ErrorIs(err, fs.ErrNotExist)
	r.Nil(f)
}

func Test_FS_Prioritizes_Disk(t *testing.T) {
	r := require.New(t)

	fsys := NewFS(embedded.FS(), "internal/testdata/disk")
	r.NotNil(fsys)

	f, err := fsys.Open("file.txt")
	r.NoError(err)

	b, err := io.ReadAll(f)
	r.NoError(err)

	r.Equal("This file is on disk.", string(b))
}

func Test_FS_Uses_Embed_If_No_Disk(t *testing.T) {
	r := require.New(t)

	fsys := NewFS(embedded.FS(), "internal/testdata/empty")
	r.NotNil(fsys)

	f, err := fsys.Open("file.txt")
	r.NoError(err)

	b, err := io.ReadAll(f)
	r.NoError(err)

	r.Equal("This file is embedded.", string(b))
}

func Test_FS_ReadDirFile(t *testing.T) {
	r := require.New(t)

	fsys := NewFS(embedded.FS(), "internal/testdata/disk")
	r.NotNil(fsys)

	f, err := fsys.Open(".")
	r.NoError(err)

	dir, ok := f.(fs.ReadDirFile)
	r.True(ok, "folder does not implement fs.ReadDirFile interface")

	// First read should return at most 1 file
	entries, err := dir.ReadDir(1)
	r.NoError(err)

	// The actual len will be 0 because the first file read is the embed.go file
	// this is counter-intuitive, but it's how the fs.ReadDirFile interface is specified;
	// if err == nil, just continue to call ReadDir until io.EOF is returned.
	r.LessOrEqual(len(entries), 1, "a call to ReadDir must at most return n entries")

	// First read should return at most 2 files
	entries, err = dir.ReadDir(2)
	r.NoError(err)

	// The actual len will be 2 (file.txt & file2.txt)
	r.LessOrEqual(len(entries), 2, "a call to ReadDir must at most return n entries")

	// trying to read next 2 files (none left)
	entries, err = dir.ReadDir(2)
	r.ErrorIs(err, io.EOF)
	r.Empty(entries)
}
<commit_msg>Fix another typo<commit_after>package buffalo

import (
	"io"
	"io/fs"
	"testing"

	"github.com/gobuffalo/buffalo/internal/testdata/embedded"
	"github.com/stretchr/testify/require"
)

func Test_FS_Disallows_Parent_Folders(t *testing.T) {
	r := require.New(t)

	fsys := NewFS(embedded.FS(), "internal/testdata/disk")
	r.NotNil(fsys)

	f, err := fsys.Open("../panic.txt")
	r.ErrorIs(err, fs.ErrNotExist)
	r.Nil(f)

	f, err = fsys.Open("try/../to/../trick/../panic.txt")
	r.ErrorIs(err, fs.ErrNotExist)
	r.Nil(f)
}

func Test_FS_Hides_embed_go(t *testing.T) {
	r := require.New(t)

	fsys := NewFS(embedded.FS(), "internal/testdata/disk")
	r.NotNil(fsys)

	f, err := fsys.Open("embed.go")
	r.ErrorIs(err, fs.ErrNotExist)
	r.Nil(f)
}

func Test_FS_Prioritizes_Disk(t *testing.T) {
	r := require.New(t)

	fsys := NewFS(embedded.FS(), "internal/testdata/disk")
	r.NotNil(fsys)

	f, err := fsys.Open("file.txt")
	r.NoError(err)

	b, err := io.ReadAll(f)
	r.NoError(err)

	r.Equal("This file is on disk.", string(b))
}

func Test_FS_Uses_Embed_If_No_Disk(t *testing.T) {
	r := require.New(t)

	fsys := NewFS(embedded.FS(), "internal/testdata/empty")
	r.NotNil(fsys)

	f, err := fsys.Open("file.txt")
	r.NoError(err)

	b, err := io.ReadAll(f)
	r.NoError(err)

	r.Equal("This file is embedded.", string(b))
}

func Test_FS_ReadDirFile(t *testing.T) {
	r := require.New(t)

	fsys := NewFS(embedded.FS(), "internal/testdata/disk")
	r.NotNil(fsys)

	f, err := fsys.Open(".")
	r.NoError(err)

	dir, ok := f.(fs.ReadDirFile)
	r.True(ok, "folder does not implement fs.ReadDirFile interface")

	// First read should return at most 1 file
	entries, err := dir.ReadDir(1)
	r.NoError(err)

	// The actual len will be 0 because the first file read is the embed.go file
	// this is counter-intuitive, but it's how the fs.ReadDirFile interface is specified;
	// if err == nil, just continue to call ReadDir until io.EOF is returned.
	r.LessOrEqual(len(entries), 1, "a call to ReadDir must at most return n entries")

	// Second read should return at most 2 files
	entries, err = dir.ReadDir(2)
	r.NoError(err)

	// The actual len will be 2 (file.txt & file2.txt)
	r.LessOrEqual(len(entries), 2, "a call to ReadDir must at most return n entries")

	// trying to read next 2 files (none left)
	entries, err = dir.ReadDir(2)
	r.ErrorIs(err, io.EOF)
	r.Empty(entries)
}
<|endoftext|>


<commit_before>package modbusone

import (
	"fmt"
	"io"
	"os"
	"testing"
	"time"
)

var _ = os.Stdout

func connectToMockServer(slaveID byte) io.ReadWriter {

	r1, w1 := io.Pipe() //pipe from client to server
	r2, w2 := io.Pipe() //pipe from server to client

	cc := newMockSerial(r2, w1) //client connection
	sc := newMockSerial(r1, w2) //server connection

	server := NewRTUServer(sc, slaveID)

	sh := &SimpleHandler{
		WriteHoldingRegisters: func(address uint16, values []uint16) error {
			return nil
		},
	}
	go server.Serve(sh)
	return cc
}

func TestOverSize(t *testing.T) {
	//DebugOut = os.Stdout
	slaveID := byte(0x11)
	cc := connectToMockServer(slaveID)
	pdu := PDU(
		append([]byte{byte(FcWriteMultipleRegisters),
			0, 0, 0, 200, 0}, make([]byte, 400)...))
	rtu := MakeRTU(slaveID, pdu)
	cc.Write([]byte(rtu))

	rep := make([]byte, 1000)
	nchan := make(chan int)
	go func() {
		n, _ := cc.Read(rep)
		nchan <- n
	}()
	timeout := time.NewTimer(time.Second / 20)
	var n int
	select {
	case n = <-nchan:
		t.Fatalf("should not complete read %x", rep[:n])
	case <-timeout.C:
	}

	OverSizeSupport = true
	OverSizeMaxRTU = 512
	defer func() {
		OverSizeSupport = false
	}()

	//New server with OverSizeSupport
	cc = connectToMockServer(slaveID)
	cc.Write([]byte(rtu))
	go func() {
		n, _ := cc.Read(rep)
		nchan <- n
	}()
	timeout.Reset(time.Second)
	select {
	case n = <-nchan:
		if "1110000000c8c30f" != fmt.Sprintf("%x", rep[:n]) {
			t.Fatalf("got unexpected read %x", rep[:n])
		}
	case <-timeout.C:
		t.Fatalf("should not time out")
	}
}
<commit_msg>add a test case oversized fc 03<commit_after>package modbusone

import (
	"fmt"
	"io"
	"os"
	"testing"
	"time"
)

var _ = os.Stdout

func connectToMockServer(slaveID byte) io.ReadWriter {

	r1, w1 := io.Pipe() //pipe from client to server
	r2, w2 := io.Pipe() //pipe from server to client

	cc := newMockSerial(r2, w1) //client connection
	sc := newMockSerial(r1, w2) //server connection

	server := NewRTUServer(sc, slaveID)

	sh := &SimpleHandler{
		WriteHoldingRegisters: func(address uint16, values []uint16) error {
			return nil
		},
		ReadHoldingRegisters: func(address, quantity uint16) ([]uint16, error) {
			return make([]uint16, quantity), nil
		},
	}
	go server.Serve(sh)
	return cc
}

func TestOverSize(t *testing.T) {
	//DebugOut = os.Stdout
	slaveID := byte(0x11)
	cc := connectToMockServer(slaveID)
	pdu := PDU(
		append([]byte{byte(FcWriteMultipleRegisters),
			0, 0, 0, 200, 0}, make([]byte, 400)...))
	rtu := MakeRTU(slaveID, pdu)
	cc.Write([]byte(rtu))

	rep := make([]byte, 1000)
	nchan := make(chan int)
	go func() {
		n, _ := cc.Read(rep)
		nchan <- n
	}()
	timeout := time.NewTimer(time.Second / 20)
	var n int
	select {
	case n = <-nchan:
		t.Fatalf("should not complete read %x", rep[:n])
	case <-timeout.C:
	}

	OverSizeSupport = true
	OverSizeMaxRTU = 512
	defer func() {
		OverSizeSupport = false
	}()

	//New server with OverSizeSupport
	cc = connectToMockServer(slaveID)
	cc.Write([]byte(rtu))
	go func() {
		for {
			n, _ := cc.Read(rep)
			nchan <- n
		}
	}()
	timeout.Reset(time.Second)
	select {
	case n = <-nchan:
		if "1110000000c8c30f" != fmt.Sprintf("%x", rep[:n]) {
			t.Fatalf("got unexpected read %x", rep[:n])
		}
	case <-timeout.C:
		t.Fatalf("should not time out")
	}

	pdu = PDU([]byte{byte(FcReadHoldingRegisters),
		0, 0, 0, 200})
	rtu = MakeRTU(slaveID, pdu)
	cc.Write([]byte(rtu))

	select {
	case n = <-nchan:
		//0x90 is from 200 * 2 = 0x0190
		if "1103900000" != fmt.Sprintf("%x", rep[:5]) {
			t.Fatalf("got unexpected read %x", rep[:n])
		}
	case <-timeout.C:
		t.Fatalf("should not time out")
	}
}
<|endoftext|>


<commit_before>package iosupport

import (
	"bufio"
	"errors"
	"fmt"
	"regexp"
	"sort"
	"strconv"
	"strings"
)

const COMPARABLE_SEPARATOR = "\u0000"

// TsvLine describes the line's details from a TSV.
type (
	TsvLine struct {
		Comparable string
		Offset     uint64
		Limit      uint32
	}
	tsvLines []TsvLine

	// Internal use
	seeker struct {
		*Scanner
		offset uint64
	}

	// TsvIndexer contains all stuff for indexing and sorting columns from a TSV.
	TsvIndexer struct {
		*Options
		parser          *TsvParser
		FieldsIndex     map[string]int
		Lines           tsvLines
		seekers         []seeker
		scannerFunc     func() *Scanner
		blankComparable string
	}
)

// NewTsvIndexer instanciates a new TsvIndexer.
func NewTsvIndexer(scannerFunc func() *Scanner, setters ...Option) *TsvIndexer {
	sc := scannerFunc()
	sc.Reset()
	sc.KeepNewlineSequence(true)

	options := &Options{
		Separator:     ',',
		LineThreshold: 2500000,
	}
	for _, setter := range setters {
		setter(options)
	}

	return &TsvIndexer{
		parser:          NewTsvParser(sc, options.Separator),
		Options:         options,
		FieldsIndex:     make(map[string]int),
		scannerFunc:     scannerFunc,
		seekers:         []seeker{seeker{sc, 0}},
		blankComparable: strings.Repeat(COMPARABLE_SEPARATOR, len(options.Fields)),
	}
}

// CloseIO closes all opened IO.
func (ti *TsvIndexer) CloseIO() {
	ti.parser.Scanner.f.Close()
	ti.releaseSeekers()
}

// Analyze parses the TSV and generates the indexes.
func (ti *TsvIndexer) Analyze() error {
	if !ti.Header {
		re := regexp.MustCompile(`var(\d+)`)
		for _, field := range ti.Fields {
			if len(re.FindStringSubmatch(field)) < 2 {
				return errors.New("Field " + field + " do not match with pattern /var\\d+/")
			}
		}
	}
	for ti.parser.ScanRow() {
		if ti.parser.Err() != nil {
			return ti.parser.Err()
		}
		err := ti.tsvLineAppender(ti.parser.Row(), len(ti.Lines), ti.parser.Offset(), ti.parser.Limit())
		if err != nil {
			return err
		}
	}
	ti.parser.Reset()
	ti.createSeekers()
	return nil
}

// Sort sorts TsvLine on its comparables.
func (ti *TsvIndexer) Sort() {
	sort.Sort(ti.Lines)
}

// Transfer writes sorted TSV into a new file.
func (ti *TsvIndexer) Transfer(output FileWriter) error {
	w := bufio.NewWriter(output)
	ns := ti.parser.NewlineSequence()
	n := ns[len(ns)-1]

	// For all sorted lines contained in the TSV
	for _, line := range ti.Lines {
		token, err := ti.selectSeeker(line.Offset).ReadAt(int64(line.Offset), int(line.Limit)) // Reads the current line
		if err != nil {
			return err
		}

		if token[len(token)-1] != n {
			token = append(token, ns...) // Appends newline sequence when missing
		}

		if _, err := w.Write(token); err != nil { // writes the current line into the sorted TSV output
			return err
		}
	}
	if err := w.Flush(); err != nil {
		return err
	}
	ti.releaseSeekers()
	return nil
}

// ------------------ //
// Sort stuff         //
// ------------------ //

func (slice tsvLines) Len() int {
	return len(slice)
}

func (slice tsvLines) Less(i, j int) bool {
	return slice[i].Comparable < slice[j].Comparable
}

func (slice tsvLines) Swap(i, j int) {
	slice[i], slice[j] = slice[j], slice[i]
}

// ------------------ //
// Transfer stuff     //
// ------------------ //

// createSeekers creates seekers for increase the speed of random accesses of the read file
func (ti *TsvIndexer) createSeekers() {
	nol := len(ti.Lines)
	if nol < ti.LineThreshold {
		return
	}

	nbOfThresholds := nol / ti.LineThreshold
	lineOffset := nol / (nbOfThresholds + 1)
	lineIndex := 0
	for i := 0; i < nbOfThresholds; i++ {
		lineIndex += lineOffset
		ti.appendSeeker(ti.Lines[lineIndex].Offset)
	}
}

// appendSeeker appends a new seeker based on the given offset. Seekers must be appened ordering by the offset
func (ti *TsvIndexer) appendSeeker(offset uint64) {
	sc := ti.scannerFunc()
	ti.seekers = append(ti.seekers, seeker{Scanner: sc, offset: offset})
}

// releaseSeekers closes internal opened file
func (ti *TsvIndexer) releaseSeekers() {
	for _, seeker := range ti.seekers {
		seeker.f.Close()
	}
}

// selectSeeker returns the nearest inferior seeker
// e.g. A file with 10,000,000 lines
//    s0 -> offset 0
//    s1 -> offset 2,500,000
//    s2 -> offset 5,000,000
//    s3 -> offset 7,500,000
// offset 666 returns seeker s0
// offset 9,999,999 returns seeker s3
func (ti *TsvIndexer) selectSeeker(offset uint64) seeker {
	for i, seeker := range ti.seekers {
		if seeker.offset > offset {
			return ti.seekers[i-1]
		}
	}
	return ti.seekers[len(ti.seekers)-1]
}

// ------------------ //
// Analyze stuff      //
// ------------------ //

func (ti *TsvIndexer) tsvLineAppender(row [][]byte, index int, offset uint64, limit uint32) error {
	if !ti.isValidRow(row) {
		// Discard mal-formatted lines
		return nil
	}

	ti.Lines = append(ti.Lines, TsvLine{"", offset, limit})
	if index == 0 && ti.Header {
		if err := ti.findFieldsIndex(row); err != nil {
			return err
		}
		// Build empty comparable
		// When comparables are sorted, this one (the header) remains the first line
		for i := 0; i < len(ti.Fields); i++ {
			ti.Lines[index].Comparable = ""
		}
	} else if index == 0 {
		// Without header, fields are named like the following pattern /var\d+/
		// \d+ is used for the index of the variable
		//
		// e.g. `var1,var2,var3` with `var1` had the index 0
		re := regexp.MustCompile(`var(\d+)`)
		for _, field := range ti.Fields {
			i, err := strconv.Atoi(re.FindStringSubmatch(field)[1])
			if err != nil {
				return err
			}
			ti.FieldsIndex[field] = i - 1
			ti.appendComparable(row[i-1], index) // The first row contains data (/!\ it is not an header)
		}
		ti.dropLastLineIfEmptyComparable()
	} else {
		for _, field := range ti.Fields {
			ti.appendComparable(row[ti.FieldsIndex[field]], index)
		}
		ti.dropLastLineIfEmptyComparable()
	}
	return nil
}

// It concats the given comparable to the existing comparable
func (ti *TsvIndexer) appendComparable(comparable []byte, index int) {
	cp := make([]byte, len(comparable), len(comparable))
	copy(cp, comparable) // Freeing the underlying array (https://blog.golang.org/go-slices-usage-and-internals - chapter: A possible "gotcha")
	ti.Lines[index].Comparable += fmt.Sprintf("%s%s", cp, COMPARABLE_SEPARATOR)
}

func (ti *TsvIndexer) dropLastLineIfEmptyComparable() {
	if !ti.DropEmptyIndexedFields {
		return
	}

	i := len(ti.Lines) - 1
	if ti.Lines[i].Comparable == ti.blankComparable {
		ti.Lines = ti.Lines[:i]
	}
}

// Append to TsvIndexer.FieldsIndex the index in the row of all TsvIndexer.Fields
func (ti *TsvIndexer) findFieldsIndex(row [][]byte) error {
	for i, head := range row {
		for _, field := range ti.Fields {
			if string(head) == field {
				ti.FieldsIndex[field] = i
				break
			}
		}
	}
	if len(ti.Fields) != len(ti.FieldsIndex) {
		return errors.New("Invalid separator or sort fields")
	}
	return nil
}

func (ti *TsvIndexer) isValidRow(row [][]byte) bool {
	if !ti.SkipMalformattedLines {
		return true
	}

	for _, index := range ti.FieldsIndex {
		if index >= len(row) {
			// avoid `panic: runtime error: index out of range`
			return false
		}
	}

	return true
}
<commit_msg>Improved row validation<commit_after>package iosupport

import (
	"bufio"
	"errors"
	"fmt"
	"regexp"
	"sort"
	"strconv"
	"strings"
)

const COMPARABLE_SEPARATOR = "\u0000"

// TsvLine describes the line's details from a TSV.
type (
	TsvLine struct {
		Comparable string
		Offset     uint64
		Limit      uint32
	}
	tsvLines []TsvLine

	// Internal use
	seeker struct {
		*Scanner
		offset uint64
	}

	// TsvIndexer contains all stuff for indexing and sorting columns from a TSV.
	TsvIndexer struct {
		*Options
		parser          *TsvParser
		FieldsIndex     map[string]int
		Lines           tsvLines
		nbOfFields      int
		seekers         []seeker
		scannerFunc     func() *Scanner
		blankComparable string
	}
)

// NewTsvIndexer instanciates a new TsvIndexer.
func NewTsvIndexer(scannerFunc func() *Scanner, setters ...Option) *TsvIndexer {
	sc := scannerFunc()
	sc.Reset()
	sc.KeepNewlineSequence(true)

	options := &Options{
		Separator:     ',',
		LineThreshold: 2500000,
	}
	for _, setter := range setters {
		setter(options)
	}

	return &TsvIndexer{
		parser:          NewTsvParser(sc, options.Separator),
		Options:         options,
		FieldsIndex:     make(map[string]int),
		scannerFunc:     scannerFunc,
		nbOfFields:      -1,
		seekers:         []seeker{seeker{sc, 0}},
		blankComparable: strings.Repeat(COMPARABLE_SEPARATOR, len(options.Fields)),
	}
}

// CloseIO closes all opened IO.
func (ti *TsvIndexer) CloseIO() {
	ti.parser.Scanner.f.Close()
	ti.releaseSeekers()
}

// Analyze parses the TSV and generates the indexes.
func (ti *TsvIndexer) Analyze() error {
	if !ti.Header {
		// Validate provided Fields with the generated header.
		re := regexp.MustCompile(`var(\d+)`)
		for _, field := range ti.Fields {
			if len(re.FindStringSubmatch(field)) < 2 {
				return errors.New("Field " + field + " do not match with pattern /var\\d+/")
			}
		}
	}
	for ti.parser.ScanRow() {
		if ti.parser.Err() != nil {
			return ti.parser.Err()
		}
		err := ti.tsvLineAppender(ti.parser.Row(), len(ti.Lines), ti.parser.Offset(), ti.parser.Limit())
		if err != nil {
			return err
		}
	}
	ti.parser.Reset()
	ti.createSeekers()
	return nil
}

// Sort sorts TsvLine on its comparables.
func (ti *TsvIndexer) Sort() {
	sort.Sort(ti.Lines)
}

// Transfer writes sorted TSV into a new file.
func (ti *TsvIndexer) Transfer(output FileWriter) error {
	w := bufio.NewWriter(output)
	ns := ti.parser.NewlineSequence()
	n := ns[len(ns)-1]

	// For all sorted lines contained in the TSV
	for _, line := range ti.Lines {
		token, err := ti.selectSeeker(line.Offset).ReadAt(int64(line.Offset), int(line.Limit)) // Reads the current line
		if err != nil {
			return err
		}

		if token[len(token)-1] != n {
			token = append(token, ns...) // Appends newline sequence when missing
		}

		if _, err := w.Write(token); err != nil { // writes the current line into the sorted TSV output
			return err
		}
	}
	if err := w.Flush(); err != nil {
		return err
	}
	ti.releaseSeekers()
	return nil
}

// ------------------ //
// Sort stuff         //
// ------------------ //

func (slice tsvLines) Len() int {
	return len(slice)
}

func (slice tsvLines) Less(i, j int) bool {
	return slice[i].Comparable < slice[j].Comparable
}

func (slice tsvLines) Swap(i, j int) {
	slice[i], slice[j] = slice[j], slice[i]
}

// ------------------ //
// Transfer stuff     //
// ------------------ //

// createSeekers creates seekers for increase the speed of random accesses of the read file
func (ti *TsvIndexer) createSeekers() {
	nol := len(ti.Lines)
	if nol < ti.LineThreshold {
		return
	}

	nbOfThresholds := nol / ti.LineThreshold
	lineOffset := nol / (nbOfThresholds + 1)
	lineIndex := 0
	for i := 0; i < nbOfThresholds; i++ {
		lineIndex += lineOffset
		ti.appendSeeker(ti.Lines[lineIndex].Offset)
	}
}

// appendSeeker appends a new seeker based on the given offset. Seekers must be appened ordering by the offset
func (ti *TsvIndexer) appendSeeker(offset uint64) {
	sc := ti.scannerFunc()
	ti.seekers = append(ti.seekers, seeker{Scanner: sc, offset: offset})
}

// releaseSeekers closes internal opened file
func (ti *TsvIndexer) releaseSeekers() {
	for _, seeker := range ti.seekers {
		seeker.f.Close()
	}
}

// selectSeeker returns the nearest inferior seeker
// e.g. A file with 10,000,000 lines
//    s0 -> offset 0
//    s1 -> offset 2,500,000
//    s2 -> offset 5,000,000
//    s3 -> offset 7,500,000
// offset 666 returns seeker s0
// offset 9,999,999 returns seeker s3
func (ti *TsvIndexer) selectSeeker(offset uint64) seeker {
	for i, seeker := range ti.seekers {
		if seeker.offset > offset {
			return ti.seekers[i-1]
		}
	}
	return ti.seekers[len(ti.seekers)-1]
}

// ------------------ //
// Analyze stuff      //
// ------------------ //

func (ti *TsvIndexer) tsvLineAppender(row [][]byte, index int, offset uint64, limit uint32) error {
	if !ti.isValidRow(row) {
		// Discard mal-formatted lines
		return nil
	}

	ti.Lines = append(ti.Lines, TsvLine{"", offset, limit})
	if index == 0 && ti.Header {
		if err := ti.findFieldsIndex(row); err != nil {
			return err
		}
		// Build empty comparable
		// When comparables are sorted, this one (the header) remains the first line
		for i := 0; i < len(ti.Fields); i++ {
			ti.Lines[index].Comparable = ""
		}
		ti.nbOfFields = len(row)
	} else if index == 0 {
		// Without header, fields are named like the following pattern /var\d+/
		// \d+ is used for the index of the variable
		//
		// e.g. `var1,var2,var3` with `var1` had the index 0
		re := regexp.MustCompile(`var(\d+)`)
		for _, field := range ti.Fields {
			i, err := strconv.Atoi(re.FindStringSubmatch(field)[1])
			if err != nil {
				return err
			}
			ti.FieldsIndex[field] = i - 1
			ti.appendComparable(row[i-1], index) // The first row contains data (/!\ it is not an header)
		}
		ti.dropLastLineIfEmptyComparable()
		ti.nbOfFields = len(row)
	} else {
		for _, field := range ti.Fields {
			ti.appendComparable(row[ti.FieldsIndex[field]], index)
		}
		ti.dropLastLineIfEmptyComparable()
	}
	return nil
}

// It concats the given comparable to the existing comparable
func (ti *TsvIndexer) appendComparable(comparable []byte, index int) {
	cp := make([]byte, len(comparable), len(comparable))
	copy(cp, comparable) // Freeing the underlying array (https://blog.golang.org/go-slices-usage-and-internals - chapter: A possible "gotcha")
	ti.Lines[index].Comparable += fmt.Sprintf("%s%s", cp, COMPARABLE_SEPARATOR)
}

func (ti *TsvIndexer) dropLastLineIfEmptyComparable() {
	if !ti.DropEmptyIndexedFields {
		return
	}

	i := len(ti.Lines) - 1
	if ti.Lines[i].Comparable == ti.blankComparable {
		ti.Lines = ti.Lines[:i]
	}
}

// Append to TsvIndexer.FieldsIndex the index in the row of all TsvIndexer.Fields
func (ti *TsvIndexer) findFieldsIndex(row [][]byte) error {
	for i, head := range row {
		for _, field := range ti.Fields {
			if string(head) == field {
				ti.FieldsIndex[field] = i
				break
			}
		}
	}
	if len(ti.Fields) != len(ti.FieldsIndex) {
		return errors.New("Invalid separator or sort fields")
	}
	return nil
}

func (ti *TsvIndexer) isValidRow(row [][]byte) bool {
	return !ti.SkipMalformattedLines || ti.nbOfFields == len(row) || ti.nbOfFields == -1
}
<|endoftext|>


<commit_before>package tutum
<commit_msg>Add Stack struct<commit_after>package tutum

import "encoding/json"

type StackListResponse struct {
	Objects []Stack `json: "objects"`
}

type Stack struct {
	Deployed_datetime  string   `json:"deployed_datetime"`
	Destroyed_datetime string   `json:"destroyed_datetime"`
	Name               string   `json:"name"`
	Resource_uri       string   `json:"resource_uri"`
	Service            []string `json:"services"`
	State              string   `json:"state"`
	Synchronized       bool     `json:"synchronized"`
	Uuid               string   `json:"uuid"`
}

func ListStacks() ([]Stack, error) {
	url := "stack/"
	request := "GET"

	//Empty Body Request
	body := []byte(`{}`)
	var response StackListResponse

	data, err := TutumCall(url, request, body)
	if err != nil {
		return nil, err
	}

	err = json.Unmarshal(data, &response)
	if err != nil {
		return nil, err
	}

	return response.Objects, nil
}
<|endoftext|>


<commit_before>package handler

import (
	"encoding/json"
	"errors"
	"fmt"
	"net/http"

	log "github.com/cihub/seelog"

	"github.com/mattheath/phosphor/store"
)

// DefaultStore is a reference to our persistence layer which we can query
var DefaultStore store.Store

func Index(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Hi there, I'm Phosphor")
}

func TraceLookup(w http.ResponseWriter, r *http.Request) {
	traceId := r.URL.Query().Get("traceId")
	if traceId == "" {
		errorResponse(w, http.StatusBadRequest, errors.New("traceId param not provided"))
		return
	}

	log.Debugf("Trace lookup - TraceId: %s", traceId)
	t, err := DefaultStore.ReadTrace(traceId)
	if err != nil {
		log.Errorf("Trace lookup failed: %s", err)
		errorResponse(w, http.StatusInternalServerError, fmt.Errorf("could not load trace: %s", err))
		return
	}

	// If we don't find the trace return 404
	if t == nil {
		log.Debugf("Trace not found: %s", traceId)
		errorResponse(w, http.StatusNotFound, errors.New("traceId not found"))
		return
	}

	// Return trace
	response(
		w,
		map[string]interface{}{
			"trace": t,
		},
	)
}

// response sends the response back to the client, marshaling to JSON
func response(w http.ResponseWriter, resp interface{}) {
	writeResponse(w, http.StatusOK, resp)
}

// errorResponse marshals an error to JSON and returns this to the client
func errorResponse(w http.ResponseWriter, code int, err error) {
	resp := map[string]interface{}{
		"error": err.Error(),
	}

	writeResponse(w, code, resp)
}

// response marshals a response to json and returns to the client
func writeResponse(w http.ResponseWriter, code int, resp interface{}) {
	w.Header().Set("Content-Type", "text/plain; charset=utf-8")

	b, err := json.Marshal(resp)
	if err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		fmt.Fprintln(w, `{"error":"failed to marshal json"}`)
		return
	}

	w.WriteHeader(code)
	fmt.Fprintln(w, string(b))
}
<commit_msg>Moar godoc<commit_after>package handler

import (
	"encoding/json"
	"errors"
	"fmt"
	"net/http"

	log "github.com/cihub/seelog"

	"github.com/mattheath/phosphor/store"
)

// DefaultStore is a reference to our persistence layer which we can query
var DefaultStore store.Store

// Index
// @todo return version information etc
func Index(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Hi there, I'm Phosphor")
}

// TraceLookup retrieves a trace from the persistence layer
func TraceLookup(w http.ResponseWriter, r *http.Request) {
	traceId := r.URL.Query().Get("traceId")
	if traceId == "" {
		errorResponse(w, http.StatusBadRequest, errors.New("traceId param not provided"))
		return
	}

	log.Debugf("Trace lookup - TraceId: %s", traceId)
	t, err := DefaultStore.ReadTrace(traceId)
	if err != nil {
		log.Errorf("Trace lookup failed: %s", err)
		errorResponse(w, http.StatusInternalServerError, fmt.Errorf("could not load trace: %s", err))
		return
	}

	// If we don't find the trace return 404
	if t == nil {
		log.Debugf("Trace not found: %s", traceId)
		errorResponse(w, http.StatusNotFound, errors.New("traceId not found"))
		return
	}

	// Return trace
	response(
		w,
		map[string]interface{}{
			"trace": t,
		},
	)
}

// response sends the response back to the client, marshaling to JSON
func response(w http.ResponseWriter, resp interface{}) {
	writeResponse(w, http.StatusOK, resp)
}

// errorResponse marshals an error to JSON and returns this to the client
func errorResponse(w http.ResponseWriter, code int, err error) {
	resp := map[string]interface{}{
		"error": err.Error(),
	}

	writeResponse(w, code, resp)
}

// response marshals a response to json and returns to the client
func writeResponse(w http.ResponseWriter, code int, resp interface{}) {
	w.Header().Set("Content-Type", "text/plain; charset=utf-8")

	b, err := json.Marshal(resp)
	if err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		fmt.Fprintln(w, `{"error":"failed to marshal json"}`)
		return
	}

	w.WriteHeader(code)
	fmt.Fprintln(w, string(b))
}
<|endoftext|>


<commit_before>package isolated

import (
	"fmt"
	"io/ioutil"
	"path/filepath"

	"code.cloudfoundry.org/cli/integration/helpers"

	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
	. "github.com/onsi/gomega/gbytes"
	. "github.com/onsi/gomega/gexec"
)

var _ = Describe("start command", func() {
	Describe("help", func() {
		Context("when --help flag is set", func() {
			It("Displays command usage to output", func() {
				session := helpers.CF("start", "--help")
				Eventually(session).Should(Say("NAME:"))
				Eventually(session).Should(Say("start - Start an app"))
				Eventually(session).Should(Say("USAGE:"))
				Eventually(session).Should(Say("cf start APP_NAME"))
				Eventually(session).Should(Say("ALIAS:"))
				Eventually(session).Should(Say("st"))
				Eventually(session).Should(Say("ENVIRONMENT:"))
				Eventually(session).Should(Say("CF_STAGING_TIMEOUT=15\\s+Max wait time for buildpack staging, in minutes"))
				Eventually(session).Should(Say("CF_STARTUP_TIMEOUT=5\\s+Max wait time for app instance startup, in minutes"))
				Eventually(session).Should(Say("SEE ALSO:"))
				Eventually(session).Should(Say("apps, logs, restart, run-task, scale, ssh, stop"))
				Eventually(session).Should(Exit(0))
			})
		})
	})

	Context("when the environment is not setup correctly", func() {
		Context("when no API endpoint is set", func() {
			BeforeEach(func() {
				helpers.UnsetAPI()
			})

			It("fails with no API endpoint set message", func() {
				session := helpers.CF("start", "wut")
				Eventually(session.Out).Should(Say("FAILED"))
				Eventually(session.Err).Should(Say("No API endpoint set. Use 'cf login' or 'cf api' to target an endpoint."))
				Eventually(session).Should(Exit(1))
			})
		})

		Context("when not logged in", func() {
			BeforeEach(func() {
				helpers.LogoutCF()
			})

			It("fails with not logged in message", func() {
				session := helpers.CF("start", "wut")
				Eventually(session.Out).Should(Say("FAILED"))
				Eventually(session.Err).Should(Say("Not logged in. Use 'cf login' to log in."))
				Eventually(session).Should(Exit(1))
			})
		})

		Context("when there is no org set", func() {
			BeforeEach(func() {
				helpers.LogoutCF()
				helpers.LoginCF()
			})

			It("fails with no targeted org error message", func() {
				session := helpers.CF("start", "wut")
				Eventually(session.Out).Should(Say("FAILED"))
				Eventually(session.Err).Should(Say("No org targeted, use 'cf target -o ORG' to target an org."))
				Eventually(session).Should(Exit(1))
			})
		})

		Context("when there is no space set", func() {
			BeforeEach(func() {
				helpers.LogoutCF()
				helpers.LoginCF()
				helpers.TargetOrg(ReadOnlyOrg)
			})

			It("fails with no targeted space error message", func() {
				session := helpers.CF("start", "wut")
				Eventually(session.Out).Should(Say("FAILED"))
				Eventually(session.Err).Should(Say("No space targeted, use 'cf target -s SPACE' to target a space."))
				Eventually(session).Should(Exit(1))
			})
		})
	})

	Context("when the environment is set up correctly", func() {
		var (
			orgName   string
			spaceName string
		)

		BeforeEach(func() {
			orgName = helpers.NewOrgName()
			spaceName = helpers.NewSpaceName()

			setupCF(orgName, spaceName)
		})

		AfterEach(func() {
			helpers.QuickDeleteOrg(orgName)
		})

		Context("when the app does not exist", func() {
			It("tells the user that the start is not found and exits 1", func() {
				appName := helpers.PrefixedRandomName("app")
				session := helpers.CF("start", appName)

				Eventually(session.Out).Should(Say("FAILED"))
				Eventually(session.Err).Should(Say("App %s not found", appName))
				Eventually(session).Should(Exit(1))
			})
		})

		Context("when the app does exist", func() {
			var (
				domainName string
				appName    string
			)

			Context("when the app is started", func() {
				BeforeEach(func() {
					appName = helpers.PrefixedRandomName("app")
					domainName = defaultSharedDomain()
					helpers.WithHelloWorldApp(func(appDir string) {
						Eventually(helpers.CF("push", appName, "-p", appDir, "-b", "staticfile_buildpack")).Should(Exit(0))
					})
				})

				It("only displays the app already started message", func() {
					userName, _ := helpers.GetCredentials()
					session := helpers.CF("start", appName)
					Eventually(session).Should(Say("Starting app %s in org %s / space %s as %s...", appName, orgName, spaceName, userName))
					Eventually(session).Should(Say("App %s is already started", appName))
					Eventually(session).Should(Exit(0))
				})
			})

			Context("when the app is stopped", func() {
				Context("when the app has been staged", func() {
					BeforeEach(func() {
						appName = helpers.PrefixedRandomName("app")
						domainName = defaultSharedDomain()
						helpers.WithHelloWorldApp(func(appDir string) {
							manifestContents := []byte(fmt.Sprintf(`
---
applications:
- name: %s
  memory: 128M
  instances: 2
  disk_quota: 128M
  routes:
  - route: %s.%s
`, appName, appName, domainName))
							manifestPath := filepath.Join(appDir, "manifest.yml")
							err := ioutil.WriteFile(manifestPath, manifestContents, 0666)
							Expect(err).ToNot(HaveOccurred())

							Eventually(helpers.CF("push", appName, "-p", appDir, "-f", manifestPath, "-b", "staticfile_buildpack")).Should(Exit(0))
						})
						Eventually(helpers.CF("stop", appName)).Should(Exit(0))
					})

					It("displays the app information with instances table", func() {
						userName, _ := helpers.GetCredentials()
						session := helpers.CF("start", appName)
						Eventually(session).Should(Say("Starting app %s in org %s / space %s as %s...", appName, orgName, spaceName, userName))
						Eventually(session).Should(Say("Waiting for app to start..."))

						Eventually(session).Should(Say("name:\\s+%s", appName))
						Eventually(session).Should(Say("requested state:\\s+started"))
						Eventually(session).Should(Say("instances:\\s+2/2"))
						Eventually(session).Should(Say("isolation segment:\\s+"))
						Eventually(session).Should(Say("usage:\\s+128M x 2 instances"))
						Eventually(session).Should(Say("routes:\\s+%s.%s", appName, domainName))
						Eventually(session).Should(Say("last uploaded:"))
						Eventually(session).Should(Say("stack:\\s+cflinuxfs2"))
						Eventually(session).Should(Say("buildpack:\\s+staticfile_buildpack"))
						Eventually(session).Should(Say("start command:\\s+sh boot.sh"))

						Eventually(session).Should(Say("state\\s+since\\s+cpu\\s+memory\\s+disk\\s+details"))
						Eventually(session).Should(Say("#0\\s+running\\s+.*\\d+\\.\\d+%.*of 128M.*of 128M"))
						Eventually(session).Should(Say("#1\\s+running\\s+.*\\d+\\.\\d+%.*of 128M.*of 128M"))
						Eventually(session).Should(Exit(0))
					})
				})

				Context("when the app has *not* yet been staged", func() {
					Context("when the app does *not* stage properly because the app was not detected by any buildpacks", func() {
						BeforeEach(func() {
							appName = helpers.PrefixedRandomName("app")
							domainName = defaultSharedDomain()
							helpers.WithHelloWorldApp(func(appDir string) {
								Eventually(helpers.CF("push", appName, "-p", appDir, "--no-start")).Should(Exit(0))
							})
						})

						It("fails and displays the staging failure message", func() {
							userName, _ := helpers.GetCredentials()
							session := helpers.CF("start", appName)
							Eventually(session).Should(Say("Starting app %s in org %s / space %s as %s...", appName, orgName, spaceName, userName))

							// The staticfile_buildback does compile an index.html file. However, it requires a "Staticfile" during buildpack detection.
							Eventually(session.Err).Should(Say("Error staging application: An app was not successfully detected by any available buildpack"))
							Eventually(session.Err).Should(Say(`TIP: Use 'cf buildpacks' to see a list of supported buildpacks.`))
							Eventually(session).Should(Exit(1))
						})
					})

					Context("when the app stages properly", func() {
						Context("when the app does *not* start properly", func() {
							BeforeEach(func() {
								appName = helpers.PrefixedRandomName("app")
								helpers.WithHelloWorldApp(func(appDir string) {
									Eventually(helpers.CF("push", appName, "-p", appDir, "--no-start", "-b", "staticfile_buildpack", "-c", "gibberish")).Should(Exit(0))
								})
							})

							It("fails and displays the start failure message", func() {
								userName, _ := helpers.GetCredentials()
								session := helpers.CF("start", appName)
								Eventually(session).Should(Say("Starting app %s in org %s / space %s as %s...", appName, orgName, spaceName, userName))

								Eventually(session.Err).Should(Say("Start unsuccessful"))
								Eventually(session.Err).Should(Say("TIP: use 'cf logs .* --recent' for more information"))
								Eventually(session).Should(Exit(1))
							})
						})

						Context("when the app starts properly", func() {
							var isoSegName string
							BeforeEach(func() {
								isoSegName = "isolated-1"
								Eventually(helpers.CF("create-isolation-segment", isoSegName)).Should(Exit(0))
								Eventually(helpers.CF("enable-org-isolation", orgName, isoSegName)).Should(Exit(0))
								Eventually(helpers.CF("set-space-isolation-segment", spaceName, isoSegName)).Should(Exit(0))
								appName = helpers.PrefixedRandomName("app")
								domainName = defaultSharedDomain()
								helpers.WithHelloWorldApp(func(appDir string) {
									manifestContents := []byte(fmt.Sprintf(`
---
applications:
- name: %s
  memory: 128M
  instances: 2
  disk_quota: 128M
  routes:
  - route: %s.%s
`, appName, appName, domainName))
									manifestPath := filepath.Join(appDir, "manifest.yml")
									err := ioutil.WriteFile(manifestPath, manifestContents, 0666)
									Expect(err).ToNot(HaveOccurred())

									Eventually(helpers.CF("push", appName, "-p", appDir, "-f", manifestPath, "-b", "staticfile_buildpack", "--no-start")).Should(Exit(0))
								})
								Eventually(helpers.CF("stop", appName)).Should(Exit(0))
							})

							It("displays the app logs and information with instances table", func() {
								userName, _ := helpers.GetCredentials()
								session := helpers.CF("start", appName)
								Eventually(session).Should(Say("Starting app %s in org %s / space %s as %s...", appName, orgName, spaceName, userName))

								// Display Staging Logs
								Eventually(session).Should(Say("Uploading droplet..."))
								Eventually(session).Should(Say("Waiting for app to start..."))

								Eventually(session).Should(Say("name:\\s+%s", appName))
								Eventually(session).Should(Say("requested state:\\s+started"))
								Eventually(session).Should(Say("instances:\\s+2/2"))
								Eventually(session).Should(Say("isolation segment:\\s+%s", isoSegName))
								Eventually(session).Should(Say("usage:\\s+128M x 2 instances"))
								Eventually(session).Should(Say("routes:\\s+%s.%s", appName, domainName))
								Eventually(session).Should(Say("last uploaded:"))
								Eventually(session).Should(Say("stack:\\s+cflinuxfs2"))
								Eventually(session).Should(Say("buildpack:\\s+staticfile_buildpack"))
								Eventually(session).Should(Say("start command:\\s+sh boot.sh"))

								Eventually(session).Should(Say("state\\s+since\\s+cpu\\s+memory\\s+disk\\s+details"))

								Eventually(session).Should(Say("#0\\s+running\\s+.*\\d+\\.\\d+%.*of 128M.*of 128M"))
								Eventually(session).Should(Say("#1\\s+running\\s+.*\\d+\\.\\d+%.*of 128M.*of 128M"))
								Eventually(session).Should(Exit(0))
							})
						})
					})
				})
			})
		})
	})
})
<commit_msg>edit cf start integration test to not check for isolation segment row<commit_after>package isolated

import (
	"fmt"
	"io/ioutil"
	"path/filepath"

	"code.cloudfoundry.org/cli/integration/helpers"

	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
	. "github.com/onsi/gomega/gbytes"
	. "github.com/onsi/gomega/gexec"
)

var _ = Describe("start command", func() {
	Describe("help", func() {
		Context("when --help flag is set", func() {
			It("Displays command usage to output", func() {
				session := helpers.CF("start", "--help")
				Eventually(session).Should(Say("NAME:"))
				Eventually(session).Should(Say("start - Start an app"))
				Eventually(session).Should(Say("USAGE:"))
				Eventually(session).Should(Say("cf start APP_NAME"))
				Eventually(session).Should(Say("ALIAS:"))
				Eventually(session).Should(Say("st"))
				Eventually(session).Should(Say("ENVIRONMENT:"))
				Eventually(session).Should(Say("CF_STAGING_TIMEOUT=15\\s+Max wait time for buildpack staging, in minutes"))
				Eventually(session).Should(Say("CF_STARTUP_TIMEOUT=5\\s+Max wait time for app instance startup, in minutes"))
				Eventually(session).Should(Say("SEE ALSO:"))
				Eventually(session).Should(Say("apps, logs, restart, run-task, scale, ssh, stop"))
				Eventually(session).Should(Exit(0))
			})
		})
	})

	Context("when the environment is not setup correctly", func() {
		Context("when no API endpoint is set", func() {
			BeforeEach(func() {
				helpers.UnsetAPI()
			})

			It("fails with no API endpoint set message", func() {
				session := helpers.CF("start", "wut")
				Eventually(session.Out).Should(Say("FAILED"))
				Eventually(session.Err).Should(Say("No API endpoint set. Use 'cf login' or 'cf api' to target an endpoint."))
				Eventually(session).Should(Exit(1))
			})
		})

		Context("when not logged in", func() {
			BeforeEach(func() {
				helpers.LogoutCF()
			})

			It("fails with not logged in message", func() {
				session := helpers.CF("start", "wut")
				Eventually(session.Out).Should(Say("FAILED"))
				Eventually(session.Err).Should(Say("Not logged in. Use 'cf login' to log in."))
				Eventually(session).Should(Exit(1))
			})
		})

		Context("when there is no org set", func() {
			BeforeEach(func() {
				helpers.LogoutCF()
				helpers.LoginCF()
			})

			It("fails with no targeted org error message", func() {
				session := helpers.CF("start", "wut")
				Eventually(session.Out).Should(Say("FAILED"))
				Eventually(session.Err).Should(Say("No org targeted, use 'cf target -o ORG' to target an org."))
				Eventually(session).Should(Exit(1))
			})
		})

		Context("when there is no space set", func() {
			BeforeEach(func() {
				helpers.LogoutCF()
				helpers.LoginCF()
				helpers.TargetOrg(ReadOnlyOrg)
			})

			It("fails with no targeted space error message", func() {
				session := helpers.CF("start", "wut")
				Eventually(session.Out).Should(Say("FAILED"))
				Eventually(session.Err).Should(Say("No space targeted, use 'cf target -s SPACE' to target a space."))
				Eventually(session).Should(Exit(1))
			})
		})
	})

	Context("when the environment is set up correctly", func() {
		var (
			orgName   string
			spaceName string
		)

		BeforeEach(func() {
			orgName = helpers.NewOrgName()
			spaceName = helpers.NewSpaceName()

			setupCF(orgName, spaceName)
		})

		AfterEach(func() {
			helpers.QuickDeleteOrg(orgName)
		})

		Context("when the app does not exist", func() {
			It("tells the user that the start is not found and exits 1", func() {
				appName := helpers.PrefixedRandomName("app")
				session := helpers.CF("start", appName)

				Eventually(session.Out).Should(Say("FAILED"))
				Eventually(session.Err).Should(Say("App %s not found", appName))
				Eventually(session).Should(Exit(1))
			})
		})

		Context("when the app does exist", func() {
			var (
				domainName string
				appName    string
			)

			Context("when the app is started", func() {
				BeforeEach(func() {
					appName = helpers.PrefixedRandomName("app")
					domainName = defaultSharedDomain()
					helpers.WithHelloWorldApp(func(appDir string) {
						Eventually(helpers.CF("push", appName, "-p", appDir, "-b", "staticfile_buildpack")).Should(Exit(0))
					})
				})

				It("only displays the app already started message", func() {
					userName, _ := helpers.GetCredentials()
					session := helpers.CF("start", appName)
					Eventually(session).Should(Say("Starting app %s in org %s / space %s as %s...", appName, orgName, spaceName, userName))
					Eventually(session).Should(Say("App %s is already started", appName))
					Eventually(session).Should(Exit(0))
				})
			})

			Context("when the app is stopped", func() {
				Context("when the app has been staged", func() {
					BeforeEach(func() {
						appName = helpers.PrefixedRandomName("app")
						domainName = defaultSharedDomain()
						helpers.WithHelloWorldApp(func(appDir string) {
							manifestContents := []byte(fmt.Sprintf(`
---
applications:
- name: %s
  memory: 128M
  instances: 2
  disk_quota: 128M
  routes:
  - route: %s.%s
`, appName, appName, domainName))
							manifestPath := filepath.Join(appDir, "manifest.yml")
							err := ioutil.WriteFile(manifestPath, manifestContents, 0666)
							Expect(err).ToNot(HaveOccurred())

							Eventually(helpers.CF("push", appName, "-p", appDir, "-f", manifestPath, "-b", "staticfile_buildpack")).Should(Exit(0))
						})
						Eventually(helpers.CF("stop", appName)).Should(Exit(0))
					})

					It("displays the app information with instances table", func() {
						userName, _ := helpers.GetCredentials()
						session := helpers.CF("start", appName)
						Eventually(session).Should(Say("Starting app %s in org %s / space %s as %s...", appName, orgName, spaceName, userName))
						Eventually(session).Should(Say("Waiting for app to start..."))

						Eventually(session).Should(Say("name:\\s+%s", appName))
						Eventually(session).Should(Say("requested state:\\s+started"))
						Eventually(session).Should(Say("instances:\\s+2/2"))
						Eventually(session).Should(Say("usage:\\s+128M x 2 instances"))
						Eventually(session).Should(Say("routes:\\s+%s.%s", appName, domainName))
						Eventually(session).Should(Say("last uploaded:"))
						Eventually(session).Should(Say("stack:\\s+cflinuxfs2"))
						Eventually(session).Should(Say("buildpack:\\s+staticfile_buildpack"))
						Eventually(session).Should(Say("start command:\\s+sh boot.sh"))

						Eventually(session).Should(Say("state\\s+since\\s+cpu\\s+memory\\s+disk\\s+details"))
						Eventually(session).Should(Say("#0\\s+running\\s+.*\\d+\\.\\d+%.*of 128M.*of 128M"))
						Eventually(session).Should(Say("#1\\s+running\\s+.*\\d+\\.\\d+%.*of 128M.*of 128M"))
						Eventually(session).Should(Exit(0))
					})
				})

				Context("when the app has *not* yet been staged", func() {
					Context("when the app does *not* stage properly because the app was not detected by any buildpacks", func() {
						BeforeEach(func() {
							appName = helpers.PrefixedRandomName("app")
							domainName = defaultSharedDomain()
							helpers.WithHelloWorldApp(func(appDir string) {
								Eventually(helpers.CF("push", appName, "-p", appDir, "--no-start")).Should(Exit(0))
							})
						})

						It("fails and displays the staging failure message", func() {
							userName, _ := helpers.GetCredentials()
							session := helpers.CF("start", appName)
							Eventually(session).Should(Say("Starting app %s in org %s / space %s as %s...", appName, orgName, spaceName, userName))

							// The staticfile_buildback does compile an index.html file. However, it requires a "Staticfile" during buildpack detection.
							Eventually(session.Err).Should(Say("Error staging application: An app was not successfully detected by any available buildpack"))
							Eventually(session.Err).Should(Say(`TIP: Use 'cf buildpacks' to see a list of supported buildpacks.`))
							Eventually(session).Should(Exit(1))
						})
					})

					Context("when the app stages properly", func() {
						Context("when the app does *not* start properly", func() {
							BeforeEach(func() {
								appName = helpers.PrefixedRandomName("app")
								helpers.WithHelloWorldApp(func(appDir string) {
									Eventually(helpers.CF("push", appName, "-p", appDir, "--no-start", "-b", "staticfile_buildpack", "-c", "gibberish")).Should(Exit(0))
								})
							})

							It("fails and displays the start failure message", func() {
								userName, _ := helpers.GetCredentials()
								session := helpers.CF("start", appName)
								Eventually(session).Should(Say("Starting app %s in org %s / space %s as %s...", appName, orgName, spaceName, userName))

								Eventually(session.Err).Should(Say("Start unsuccessful"))
								Eventually(session.Err).Should(Say("TIP: use 'cf logs .* --recent' for more information"))
								Eventually(session).Should(Exit(1))
							})
						})

						Context("when the app starts properly", func() {
							var isoSegName string
							BeforeEach(func() {
								isoSegName = "isolated-1"
								Eventually(helpers.CF("create-isolation-segment", isoSegName)).Should(Exit(0))
								Eventually(helpers.CF("enable-org-isolation", orgName, isoSegName)).Should(Exit(0))
								Eventually(helpers.CF("set-space-isolation-segment", spaceName, isoSegName)).Should(Exit(0))
								appName = helpers.PrefixedRandomName("app")
								domainName = defaultSharedDomain()
								helpers.WithHelloWorldApp(func(appDir string) {
									manifestContents := []byte(fmt.Sprintf(`
---
applications:
- name: %s
  memory: 128M
  instances: 2
  disk_quota: 128M
  routes:
  - route: %s.%s
`, appName, appName, domainName))
									manifestPath := filepath.Join(appDir, "manifest.yml")
									err := ioutil.WriteFile(manifestPath, manifestContents, 0666)
									Expect(err).ToNot(HaveOccurred())

									Eventually(helpers.CF("push", appName, "-p", appDir, "-f", manifestPath, "-b", "staticfile_buildpack", "--no-start")).Should(Exit(0))
								})
								Eventually(helpers.CF("stop", appName)).Should(Exit(0))
							})

							It("displays the app logs and information with instances table", func() {
								userName, _ := helpers.GetCredentials()
								session := helpers.CF("start", appName)
								Eventually(session).Should(Say("Starting app %s in org %s / space %s as %s...", appName, orgName, spaceName, userName))

								// Display Staging Logs
								Eventually(session).Should(Say("Uploading droplet..."))
								Eventually(session).Should(Say("Waiting for app to start..."))

								Eventually(session).Should(Say("name:\\s+%s", appName))
								Eventually(session).Should(Say("requested state:\\s+started"))
								Eventually(session).Should(Say("instances:\\s+2/2"))
								Eventually(session).Should(Say("isolation segment:\\s+%s", isoSegName))
								Eventually(session).Should(Say("usage:\\s+128M x 2 instances"))
								Eventually(session).Should(Say("routes:\\s+%s.%s", appName, domainName))
								Eventually(session).Should(Say("last uploaded:"))
								Eventually(session).Should(Say("stack:\\s+cflinuxfs2"))
								Eventually(session).Should(Say("buildpack:\\s+staticfile_buildpack"))
								Eventually(session).Should(Say("start command:\\s+sh boot.sh"))

								Eventually(session).Should(Say("state\\s+since\\s+cpu\\s+memory\\s+disk\\s+details"))

								Eventually(session).Should(Say("#0\\s+running\\s+.*\\d+\\.\\d+%.*of 128M.*of 128M"))
								Eventually(session).Should(Say("#1\\s+running\\s+.*\\d+\\.\\d+%.*of 128M.*of 128M"))
								Eventually(session).Should(Exit(0))
							})
						})
					})
				})
			})
		})
	})
})
<|endoftext|>