<commit_before>/*
Copyright © 2014–5 Brad Ackerman.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

*/

package market

import (
	"encoding/xml"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"net/url"
	"time"

	"github.com/backerman/evego"
)

type eveCentral struct {
	db        evego.Database
	router    evego.Router
	xmlAPI    evego.XMLAPI
	endpoint  *url.URL
	http      http.Client
	respCache evego.Cache
}

// EveCentral returns an interface to the EVE-Central API.
// It takes as input an EveDatabase object and an HTTP endpoint;
// the latter should be http://api.eve-central.com/api/quicklook
// for the production EVE-Central instance.
func EveCentral(db evego.Database, router evego.Router, xmlAPI evego.XMLAPI, endpoint string,
	aCache evego.Cache) evego.Market {
	epURL, err := url.Parse(endpoint)
	if err != nil {
		log.Fatalf("Invalid URL %v passed for Eve-Central endpoint: %v", endpoint, err)
	}
	ec := eveCentral{db: db, router: router, endpoint: epURL, xmlAPI: xmlAPI, respCache: aCache}
	return &ec
}

func (e *eveCentral) getURL(u string) ([]byte, error) {
	req, err := http.NewRequest("GET", u, nil)
	if err != nil {
		return nil, err
	}
	req.Header.Add("User-Agent", "evego (https://github.com/backerman/evego)")
	resp, err := e.http.Do(req)
	defer resp.Body.Close()
	if err != nil {
		return nil, err
	}
	body, err := ioutil.ReadAll(resp.Body)
	return body, err
}

type order struct {
	RegionID          int     `xml:"region"`
	StationID         int     `xml:"station"`
	StationName       string  `xml:"station_name"`
	Security          float64 `xml:"security"`
	Range             int     `xml:"range"`
	Price             float64 `xml:"price"`
	QuantityAvailable int     `xml:"vol_remain"`
	MinimumVolume     int     `xml:"min_volume"`
	ExpirationDate    string  `xml:"expires"`
	ReportedTime      string  `xml:"reported_time"`
}

type quicklook struct {
	SellOrders []order `xml:"quicklook>sell_orders>order"`
	BuyOrders  []order `xml:"quicklook>buy_orders>order"`
}

func (e *eveCentral) processOrders(data *quicklook, item *evego.Item, t evego.OrderType) []evego.Order {
	var toProcess *[]order
	stationCache := make(map[int]*evego.Station)
	switch t {
	case evego.Buy:
		toProcess = &data.BuyOrders
	case evego.Sell:
		toProcess = &data.SellOrders
	}
	results := []evego.Order{}
	for _, o := range *toProcess {
		if stationCache[o.StationID] == nil {
			sta, err := e.db.StationForID(o.StationID)
			if err != nil {
				// If it's not in the static databse, it's an outpost.
				sta, err = e.xmlAPI.OutpostForID(o.StationID)
				if err != nil {
					// Make a dummy station.
					sta = &evego.Station{
						Name: fmt.Sprintf("Unknown Station (ID %d)", o.StationID),
						ID:   o.StationID,
					}
				}
			}
			stationCache[o.StationID] = sta
		}
		oTime, _ := time.Parse("2006-01-02", o.ExpirationDate)
		newOrder := evego.Order{
			Type:       t,
			Item:       item,
			Quantity:   o.QuantityAvailable,
			Station:    stationCache[o.StationID],
			Price:      o.Price,
			Expiration: oTime,
		}
		if t == evego.Buy {
			// Set the fields specific to buy orders.
			newOrder.MinQuantity = o.MinimumVolume
			switch o.Range {
			case 32767, 65535:
				newOrder.JumpRange = evego.BuyRegion
			case -1:
				newOrder.JumpRange = evego.BuyStation
			case 0:
				newOrder.JumpRange = evego.BuySystem
			default:
				newOrder.JumpRange = evego.BuyNumberJumps
				newOrder.NumJumps = o.Range
			}
		}
		results = append(results, newOrder)
	}
	return results
}

func (e *eveCentral) OrdersForItem(item *evego.Item, location string, orderType evego.OrderType) (*[]evego.Order, error) {
	var (
		system *evego.SolarSystem
		region *evego.Region
		err    error
	)
	system, err = e.db.SolarSystemForName(location)
	if err != nil {
		// Not a system or unable to look up. Try region.
		region, err = e.db.RegionForName(location)
		if err != nil {
			// Still can't find it. Return an error.
			return nil, err
		}
	}
	query := url.Values{}
	if region != nil {
		query.Set("regionlimit", fmt.Sprintf("%d", region.ID))
	} else {
		query.Set("usesystem", fmt.Sprintf("%d", system.ID))
	}
	query.Set("typeid", fmt.Sprintf("%d", item.ID))
	e.endpoint.RawQuery = query.Encode()
	orderXML, found := e.respCache.Get(query.Encode())
	if !found {
		var err error
		orderXML, err = e.getURL(e.endpoint.String())
		if err != nil {
			return nil, err
		}
		// EVE-Central doesn't specify a caching time to use, so we're picking
		// five minutes at random.
		e.respCache.Put(query.Encode(), orderXML, time.Now().Add(5*time.Minute))
	}
	orders := &quicklook{}

	err = xml.Unmarshal(orderXML, orders)
	if err != nil {
		return nil, err
	}

	// Convert returned XML struct into what we present to rest of library.
	results := []evego.Order{}
	switch orderType {
	case evego.AllOrders:
		// The order here matters, if only because it's the order that the
		// orders are presented by EVE Central and therefore the order in which
		// the test cases expect results.
		results = append(results, e.processOrders(orders, item, evego.Sell)...)
		results = append(results, e.processOrders(orders, item, evego.Buy)...)
	default:
		results = e.processOrders(orders, item, orderType)
	}
	return &results, nil
}

func (e *eveCentral) BuyInStation(item *evego.Item, location *evego.Station) (*[]evego.Order, error) {
	system, err := e.db.SolarSystemForID(location.SystemID)
	if err != nil {
		return nil, err
	}
	regionalOrders, err := e.OrdersForItem(item, system.Region, evego.Buy)
	if err != nil {
		return nil, err
	}
	orders := []evego.Order{}
	for _, o := range *regionalOrders {
		switch o.JumpRange {
		case evego.BuyRegion:
			orders = append(orders, o)
		case evego.BuyNumberJumps:
			numJumps, err := e.router.NumJumpsID(o.Station.SystemID, location.SystemID)
			if err != nil {
				return nil, err
			}
			if numJumps <= o.NumJumps {
				orders = append(orders, o)
			}
		case evego.BuySystem:
			if o.Station.SystemID == location.SystemID {
				orders = append(orders, o)
			}
		case evego.BuyStation:
			if o.Station.ID == location.ID {
				orders = append(orders, o)
			}
		}
	}

	return &orders, nil
}

func (e *eveCentral) OrdersInStation(item *evego.Item, location *evego.Station) (*[]evego.Order, error) {
	orders, err := e.BuyInStation(item, location)
	if err != nil {
		return nil, err
	}
	// Get the sell orders for the entire system, then append the ones for this
	// station to the returned array.
	orderSystem, err := e.db.SolarSystemForID(location.SystemID)
	if err != nil {
		return nil, err
	}
	sellInSystem, err := e.OrdersForItem(item, orderSystem.Name, evego.Sell)
	if err != nil {
		return nil, err
	}
	for _, o := range *sellInSystem {
		if o.Station.ID == location.ID {
			*orders = append(*orders, o)
		}
	}
	return orders, nil
}

func (e *eveCentral) Close() error {
	return nil
}
<commit_msg>Make cache keys use URL of accessed information.<commit_after>/*
Copyright © 2014–5 Brad Ackerman.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

*/

package market

import (
	"encoding/xml"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"net/url"
	"time"

	"github.com/backerman/evego"
)

type eveCentral struct {
	db        evego.Database
	router    evego.Router
	xmlAPI    evego.XMLAPI
	endpoint  *url.URL
	http      http.Client
	respCache evego.Cache
}

// EveCentral returns an interface to the EVE-Central API.
// It takes as input an EveDatabase object and an HTTP endpoint;
// the latter should be http://api.eve-central.com/api/quicklook
// for the production EVE-Central instance.
func EveCentral(db evego.Database, router evego.Router, xmlAPI evego.XMLAPI, endpoint string,
	aCache evego.Cache) evego.Market {
	epURL, err := url.Parse(endpoint)
	if err != nil {
		log.Fatalf("Invalid URL %v passed for Eve-Central endpoint: %v", endpoint, err)
	}
	ec := eveCentral{db: db, router: router, endpoint: epURL, xmlAPI: xmlAPI, respCache: aCache}
	return &ec
}

func (e *eveCentral) getURL(u string) ([]byte, error) {
	// Start by checking cache.
	body, found := e.respCache.Get(u)
	if found {
		return body, nil
	}
	req, err := http.NewRequest("GET", u, nil)
	if err != nil {
		return nil, err
	}
	req.Header.Add("User-Agent", "evego (https://github.com/backerman/evego)")
	resp, err := e.http.Do(req)
	defer resp.Body.Close()
	if err != nil {
		return nil, err
	}
	body, err = ioutil.ReadAll(resp.Body)
	if err != nil {
		// EVE-Central doesn't specify a caching time to use, so we're picking
		// ten minutes at random.
		e.respCache.Put(u, body, time.Now().Add(10*time.Minute))
	}
	return body, err
}

type order struct {
	RegionID          int     `xml:"region"`
	StationID         int     `xml:"station"`
	StationName       string  `xml:"station_name"`
	Security          float64 `xml:"security"`
	Range             int     `xml:"range"`
	Price             float64 `xml:"price"`
	QuantityAvailable int     `xml:"vol_remain"`
	MinimumVolume     int     `xml:"min_volume"`
	ExpirationDate    string  `xml:"expires"`
	ReportedTime      string  `xml:"reported_time"`
}

type quicklook struct {
	SellOrders []order `xml:"quicklook>sell_orders>order"`
	BuyOrders  []order `xml:"quicklook>buy_orders>order"`
}

func (e *eveCentral) processOrders(data *quicklook, item *evego.Item, t evego.OrderType) []evego.Order {
	var toProcess *[]order
	// Set up a temporary cache so that we only get each station's object once.
	stationCache := make(map[int]*evego.Station)
	switch t {
	case evego.Buy:
		toProcess = &data.BuyOrders
	case evego.Sell:
		toProcess = &data.SellOrders
	}
	results := []evego.Order{}
	for _, o := range *toProcess {
		if stationCache[o.StationID] == nil {
			sta, err := e.db.StationForID(o.StationID)
			if err != nil {
				// If it's not in the static databse, it's an outpost.
				sta, err = e.xmlAPI.OutpostForID(o.StationID)
				if err != nil {
					// Make a dummy station.
					sta = &evego.Station{
						Name: fmt.Sprintf("Unknown Station (ID %d)", o.StationID),
						ID:   o.StationID,
					}
				}
			}
			stationCache[o.StationID] = sta
		}
		oTime, _ := time.Parse("2006-01-02", o.ExpirationDate)
		newOrder := evego.Order{
			Type:       t,
			Item:       item,
			Quantity:   o.QuantityAvailable,
			Station:    stationCache[o.StationID],
			Price:      o.Price,
			Expiration: oTime,
		}
		if t == evego.Buy {
			// Set the fields specific to buy orders.
			newOrder.MinQuantity = o.MinimumVolume
			switch o.Range {
			case 32767, 65535:
				newOrder.JumpRange = evego.BuyRegion
			case -1:
				newOrder.JumpRange = evego.BuyStation
			case 0:
				newOrder.JumpRange = evego.BuySystem
			default:
				newOrder.JumpRange = evego.BuyNumberJumps
				newOrder.NumJumps = o.Range
			}
		}
		results = append(results, newOrder)
	}
	return results
}

func (e *eveCentral) OrdersForItem(item *evego.Item, location string, orderType evego.OrderType) (*[]evego.Order, error) {
	var (
		system *evego.SolarSystem
		region *evego.Region
		err    error
	)
	system, err = e.db.SolarSystemForName(location)
	if err != nil {
		// Not a system or unable to look up. Try region.
		region, err = e.db.RegionForName(location)
		if err != nil {
			// Still can't find it. Return an error.
			return nil, err
		}
	}
	query := url.Values{}
	if region != nil {
		query.Set("regionlimit", fmt.Sprintf("%d", region.ID))
	} else {
		query.Set("usesystem", fmt.Sprintf("%d", system.ID))
	}
	query.Set("typeid", fmt.Sprintf("%d", item.ID))
	e.endpoint.RawQuery = query.Encode()
	orderXML, err := e.getURL(e.endpoint.String())
	if err != nil {
		return nil, err
	}

	// We received a quicklook XML document from EVE-Central. Unmarshal it.
	orders := &quicklook{}
	err = xml.Unmarshal(orderXML, orders)
	if err != nil {
		return nil, err
	}

	// Convert returned XML struct into what we present to rest of library.
	results := []evego.Order{}
	switch orderType {
	case evego.AllOrders:
		// The order here matters, if only because it's the order that the
		// orders are presented by EVE Central and therefore the order in which
		// the test cases expect results.
		results = append(results, e.processOrders(orders, item, evego.Sell)...)
		results = append(results, e.processOrders(orders, item, evego.Buy)...)
	default:
		results = e.processOrders(orders, item, orderType)
	}
	return &results, nil
}

func (e *eveCentral) BuyInStation(item *evego.Item, location *evego.Station) (*[]evego.Order, error) {
	system, err := e.db.SolarSystemForID(location.SystemID)
	if err != nil {
		return nil, err
	}
	regionalOrders, err := e.OrdersForItem(item, system.Region, evego.Buy)
	if err != nil {
		return nil, err
	}
	orders := []evego.Order{}
	for _, o := range *regionalOrders {
		switch o.JumpRange {
		case evego.BuyRegion:
			orders = append(orders, o)
		case evego.BuyNumberJumps:
			numJumps, err := e.router.NumJumpsID(o.Station.SystemID, location.SystemID)
			if err != nil {
				return nil, err
			}
			if numJumps <= o.NumJumps {
				orders = append(orders, o)
			}
		case evego.BuySystem:
			if o.Station.SystemID == location.SystemID {
				orders = append(orders, o)
			}
		case evego.BuyStation:
			if o.Station.ID == location.ID {
				orders = append(orders, o)
			}
		}
	}

	return &orders, nil
}

func (e *eveCentral) OrdersInStation(item *evego.Item, location *evego.Station) (*[]evego.Order, error) {
	orders, err := e.BuyInStation(item, location)
	if err != nil {
		return nil, err
	}
	// Get the sell orders for the entire system, then append the ones for this
	// station to the returned array.
	orderSystem, err := e.db.SolarSystemForID(location.SystemID)
	if err != nil {
		return nil, err
	}
	sellInSystem, err := e.OrdersForItem(item, orderSystem.Name, evego.Sell)
	if err != nil {
		return nil, err
	}
	for _, o := range *sellInSystem {
		if o.Station.ID == location.ID {
			*orders = append(*orders, o)
		}
	}
	return orders, nil
}

func (e *eveCentral) Close() error {
	return nil
}
<|endoftext|>


<commit_before>// metainfo a package for dealing with '.torrent' files
package metainfo

import (
	"fmt"
	"os"
	"time"

	"github.com/marksamman/bencode"
)

// MetaInfo a mapping of a .torrent file to a struct
type MetaInfo struct {
	Info
	Announce     string
	AnnounceList [][]string
	CreationDate time.Time
	Comment      string
	CreatedBy    string
	Encoding     string
	Files        []File
	Name         string // Single File
	DirName      string // Multi File
}

// Info fields common to both single and multi file info dictionary
type Info struct {
	PieceLength int32
	Pieces      []byte
	Private     int32
}

// File a struct for files in multifileinfo dicts
type File struct {
	Length int64
	MD5Sum []byte
	Path   []string
}

func NewFromFilename(fn string) (*MetaInfo, error) {
	file, err := os.Open(fn)
	if err != nil {
		return nil, err
	}
	defer file.Close()

	data, err := bencode.Decode(file)
	if err != nil {
		return nil, err
	}

	m := &MetaInfo{}

	// Populate Announce or AnnounceList
	annLists, ok := data["announce-list"].([]interface{})
	lists := [][]string{}
	if !ok {
		m.Announce = data["announce"].(string)
	}
	for _, list := range annLists {
		al := []string{}
		for _, URL := range list.([]interface{}) {
			al = append(al, URL.(string))
		}
		lists = append(lists, al)
	}
	m.AnnounceList = lists

	// parse additional optional fields
	if cd, ok := data["creation date"]; ok {
		m.CreationDate = time.Unix(cd.(int64), 0)
	}
	if c, ok := data["comment"]; ok {
		m.Comment = c.(string)
	}
	if cb, ok := data["created by"]; ok {
		m.CreatedBy = cb.(string)
	}
	if enc, ok := data["encoding"]; ok {
		m.Encoding = enc.(string)
	}

	// begin populating the Info dict
	info := data["info"].(map[string]interface{})

	if files, exists := info["files"]; !exists {
		//TODO name and length arent optional, maybe return an err here
		f := File{}

		if name, ok := info["name"]; ok {
			m.Name = name.(string)
		}
		if length, ok := info["length"]; ok {
			f.Length = length.(int64)
		}
		if md5, ok := info["md5sum"]; ok {
			f.MD5Sum = md5.([]byte)
		}

		m.Files = append(m.Files, f)
	} else {
		//do multifile stuff
		_ = files
	}

	return nil, nil
}

func (m *MetaInfo) String() string {
	ret := fmt.Sprintf("Announce: %v\n", m.Announce)
	ret += fmt.Sprintf("AnnounceList(opt): %v\n", m.AnnounceList)
	ret += fmt.Sprintf("Creation Date(opt): %v\n", m.CreationDate)
	ret += fmt.Sprintf("Comment(opt): %v\n", m.Comment)
	ret += fmt.Sprintf("Created By(opt): %v\n", m.CreatedBy)
	ret += fmt.Sprintf("Encoding(opt): %v\n", m.Encoding)
	ret += fmt.Sprintf("Info: \n")
	if l := len(m.Files); l == 1 {
		ret += fmt.Sprintf("    Filename: %v\n", m.Name)
		ret += fmt.Sprintf("    Length: %v\n", m.Files[0].Length)
		ret += fmt.Sprintf("    MD5(opt): %v\n", m.Files[0].MD5Sum)
	} else if l > 1 {
		// do multifile stuff
	}
	return ret
}
<commit_msg>woops<commit_after>// metainfo a package for dealing with '.torrent' files
package metainfo

import (
	"fmt"
	"os"
	"time"

	"github.com/marksamman/bencode"
)

// MetaInfo a mapping of a .torrent file to a struct
type MetaInfo struct {
	Info
	Announce     string
	AnnounceList [][]string
	CreationDate time.Time
	Comment      string
	CreatedBy    string
	Encoding     string
	Files        []File
	Name         string // Single File
	DirName      string // Multi File
}

// Info fields common to both single and multi file info dictionary
type Info struct {
	PieceLength int32
	Pieces      []byte
	Private     int32
}

// File a struct for files in multifileinfo dicts
type File struct {
	Length int64
	MD5Sum []byte
	Path   []string
}

func NewFromFilename(fn string) (*MetaInfo, error) {
	file, err := os.Open(fn)
	if err != nil {
		return nil, err
	}
	defer file.Close()

	data, err := bencode.Decode(file)
	if err != nil {
		return nil, err
	}

	m := &MetaInfo{}

	// Populate Announce or AnnounceList
	annLists, ok := data["announce-list"].([]interface{})
	lists := [][]string{}
	if !ok {
		m.Announce = data["announce"].(string)
	}
	for _, list := range annLists {
		al := []string{}
		for _, URL := range list.([]interface{}) {
			al = append(al, URL.(string))
		}
		lists = append(lists, al)
	}
	m.AnnounceList = lists

	// parse additional optional fields
	if cd, ok := data["creation date"]; ok {
		m.CreationDate = time.Unix(cd.(int64), 0)
	}
	if c, ok := data["comment"]; ok {
		m.Comment = c.(string)
	}
	if cb, ok := data["created by"]; ok {
		m.CreatedBy = cb.(string)
	}
	if enc, ok := data["encoding"]; ok {
		m.Encoding = enc.(string)
	}

	// begin populating the Info dict
	info := data["info"].(map[string]interface{})

	if files, exists := info["files"]; !exists {
		//TODO name and length arent optional, maybe return an err here
		f := File{}

		if name, ok := info["name"]; ok {
			m.Name = name.(string)
		}
		if length, ok := info["length"]; ok {
			f.Length = length.(int64)
		}
		if md5, ok := info["md5sum"]; ok {
			f.MD5Sum = md5.([]byte)
		}

		m.Files = append(m.Files, f)
	} else {
		//do multifile stuff
		_ = files
	}

	return m, nil
}

func (m *MetaInfo) String() string {
	ret := fmt.Sprintf("Announce: %v\n", m.Announce)
	ret += fmt.Sprintf("AnnounceList(opt): %v\n", m.AnnounceList)
	ret += fmt.Sprintf("Creation Date(opt): %v\n", m.CreationDate)
	ret += fmt.Sprintf("Comment(opt): %v\n", m.Comment)
	ret += fmt.Sprintf("Created By(opt): %v\n", m.CreatedBy)
	ret += fmt.Sprintf("Encoding(opt): %v\n", m.Encoding)
	ret += fmt.Sprintf("Info: \n")
	if l := len(m.Files); l == 1 {
		ret += fmt.Sprintf("    Filename: %v\n", m.Name)
		ret += fmt.Sprintf("    Length: %v\n", m.Files[0].Length)
		ret += fmt.Sprintf("    MD5(opt): %v\n", m.Files[0].MD5Sum)
	} else if l > 1 {
		// do multifile stuff
	}
	return ret
}
<|endoftext|>


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

package minikube

import (
	"fmt"
	"os"
	"os/exec"
	"regexp"
	"runtime"
	"strconv"
	"strings"
	"time"

	"knative.dev/kn-plugin-quickstart/pkg/install"
)

var clusterName string
var kubernetesVersion = "1.22.4"
var minikubeVersion = 1.23

// SetUp creates a local Minikube cluster and installs all the relevant Knative components
func SetUp(name string) error {
	start := time.Now()
	clusterName = name

	if err := createMinikubeCluster(); err != nil {
		return fmt.Errorf("creating cluster: %w", err)
	}
	if runtime.GOOS == "darwin" || runtime.GOOS == "windows" {
		fmt.Print("\n")
		fmt.Println("To finish setting up networking for minikube, run the following command in a separate terminal window:")
		fmt.Println("    minikube tunnel --profile knative")
		fmt.Println("The tunnel window must remain open until you are done with the quickstart environment.")
		fmt.Println("\nPress the Enter key to continue")
		fmt.Scanln()
	}
	if err := install.Serving(); err != nil {
		return fmt.Errorf("install serving: %w", err)
	}
	if err := install.Kourier(); err != nil {
		return fmt.Errorf("install kourier: %w", err)
	}
	if err := install.KourierMinikube(); err != nil {
		return fmt.Errorf("configure kourier: %w", err)
	}
	if err := install.Eventing(); err != nil {
		return fmt.Errorf("install eventing: %w", err)
	}

	finish := time.Since(start).Round(time.Second)
	fmt.Printf("🚀 Knative install took: %s \n", finish)
	fmt.Println("🎉 Now have some fun with Serverless and Event Driven Apps!")

	return nil
}

func createMinikubeCluster() error {
	if err := checkMinikubeVersion(); err != nil {
		return fmt.Errorf("minikube version: %w", err)
	}
	if err := checkForExistingCluster(); err != nil {
		return fmt.Errorf("existing cluster: %w", err)
	}
	return nil
}

// checkMinikubeVersion validates that the user has the correct version of Minikube installed.
// If not, it prompts the user to download a newer version before continuing.
func checkMinikubeVersion() error {
	versionCheck := exec.Command("minikube", "version", "--short")
	out, err := versionCheck.CombinedOutput()
	if err != nil {
		return fmt.Errorf("minikube version: %w", err)
	}
	fmt.Printf("Minikube version is: %s\n", string(out))

	userMinikubeVersion, err := parseMinikubeVersion(string(out))
	if err != nil {
		return fmt.Errorf("parsing minikube version: %w", err)
	}
	if userMinikubeVersion < minikubeVersion {
		var resp string
		fmt.Printf("WARNING: We require at least Minikube v%.2f, while you are using v%.2f\n", minikubeVersion, userMinikubeVersion)
		fmt.Println("You can download a newer version from https://github.com/kubernetes/minikube/releases/")
		fmt.Print("Continue anyway? (not recommended) [y/N]: ")
		fmt.Scanf("%s", &resp)
		if strings.ToLower(resp) != "y" {
			fmt.Println("Installation stopped. Please upgrade minikube and run again")
			os.Exit(0)
		}
	}

	return nil
}

// checkForExistingCluster checks if the user already has a Minikube cluster. If so, it provides
// the option of deleting the existing cluster and recreating it. If not, it proceeds to
// creating a new cluster
func checkForExistingCluster() error {
	getClusters := exec.Command("minikube", "profile", "list")
	out, err := getClusters.CombinedOutput()
	if err != nil {
		// there are no existing minikube profiles, the listing profiles command will error
		// if there were no profiles, we simply want to create a new one and not stop the install
		// so if the error is the "MK_USAGE_NO_PROFILE" error, we ignore it and continue onwards
		if !strings.Contains(string(out), "MK_USAGE_NO_PROFILE") {
			return fmt.Errorf("check cluster: %w", err)
		}
	}
	// TODO Add tests for regex
	r := regexp.MustCompile(clusterName)
	matches := r.Match(out)
	if matches {
		var resp string
		fmt.Print("Knative Cluster " + clusterName + " already installed.\nDelete and recreate [y/N]: ")
		fmt.Scanf("%s", &resp)
		if strings.ToLower(resp) != "y" {
			fmt.Println("Installation skipped")
			return nil
		}
		fmt.Println("deleting cluster...")
		deleteCluster := exec.Command("minikube", "delete", "--profile", clusterName)
		if err := deleteCluster.Run(); err != nil {
			return fmt.Errorf("delete cluster: %w", err)
		}
		if err := createNewCluster(); err != nil {
			return fmt.Errorf("new cluster: %w", err)
		}
		return nil
	}

	if err := createNewCluster(); err != nil {
		return fmt.Errorf("new cluster: %w", err)
	}

	return nil
}

// createNewCluster creates a new Minikube cluster
func createNewCluster() error {
	fmt.Println("☸ Creating Minikube cluster...")
	fmt.Println("\nBy default, using the standard minikube driver for your system")
	fmt.Println("If you wish to use a different driver, please configure minikube using")
	fmt.Print("    minikube config set driver <your-driver>\n\n")

	// create cluster and wait until ready
	createCluster := exec.Command("minikube", "start", "--kubernetes-version", kubernetesVersion, "--cpus", "3", "--profile", clusterName, "--wait", "all")
	if err := runCommandWithOutput(createCluster); err != nil {
		return fmt.Errorf("minikube create: %w", err)
	}

	return nil
}

func runCommandWithOutput(c *exec.Cmd) error {
	c.Stdout = os.Stdout
	c.Stderr = os.Stderr
	if err := c.Run(); err != nil {
		return fmt.Errorf("piping output: %w", err)
	}
	fmt.Print("\n")
	return nil
}

func parseMinikubeVersion(v string) (float64, error) {
	strippedVersion := strings.TrimLeft(strings.TrimRight(v, "\n"), "v")
	dotVersion := strings.Split(strippedVersion, ".")
	floatVersion, err := strconv.ParseFloat(dotVersion[0]+"."+dotVersion[1], 64)
	if err != nil {
		return 0, err
	}

	return floatVersion, nil
}
<commit_msg>updates minikube tunnel display prompt (#189)<commit_after>// Copyright © 2021 The Knative Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package minikube

import (
	"fmt"
	"os"
	"os/exec"
	"regexp"
	"runtime"
	"strconv"
	"strings"
	"time"

	"knative.dev/kn-plugin-quickstart/pkg/install"
)

var clusterName string
var kubernetesVersion = "1.22.4"
var minikubeVersion = 1.23

// SetUp creates a local Minikube cluster and installs all the relevant Knative components
func SetUp(name string) error {
	start := time.Now()
	clusterName = name

	if err := createMinikubeCluster(); err != nil {
		return fmt.Errorf("creating cluster: %w", err)
	}
	if runtime.GOOS == "darwin" || runtime.GOOS == "windows" {
		fmt.Print("\n")
		fmt.Println("To finish setting up networking for minikube, run the following command in a separate terminal window:")
		fmt.Println("    minikube tunnel --profile knative")
		fmt.Println("The tunnel command must be running in a terminal window any time when using the knative quickstart environment.")
		fmt.Println("\nPress the Enter key to continue")
		fmt.Scanln()
	}
	if err := install.Serving(); err != nil {
		return fmt.Errorf("install serving: %w", err)
	}
	if err := install.Kourier(); err != nil {
		return fmt.Errorf("install kourier: %w", err)
	}
	if err := install.KourierMinikube(); err != nil {
		return fmt.Errorf("configure kourier: %w", err)
	}
	if err := install.Eventing(); err != nil {
		return fmt.Errorf("install eventing: %w", err)
	}

	finish := time.Since(start).Round(time.Second)
	fmt.Printf("🚀 Knative install took: %s \n", finish)
	fmt.Println("🎉 Now have some fun with Serverless and Event Driven Apps!")

	return nil
}

func createMinikubeCluster() error {
	if err := checkMinikubeVersion(); err != nil {
		return fmt.Errorf("minikube version: %w", err)
	}
	if err := checkForExistingCluster(); err != nil {
		return fmt.Errorf("existing cluster: %w", err)
	}
	return nil
}

// checkMinikubeVersion validates that the user has the correct version of Minikube installed.
// If not, it prompts the user to download a newer version before continuing.
func checkMinikubeVersion() error {
	versionCheck := exec.Command("minikube", "version", "--short")
	out, err := versionCheck.CombinedOutput()
	if err != nil {
		return fmt.Errorf("minikube version: %w", err)
	}
	fmt.Printf("Minikube version is: %s\n", string(out))

	userMinikubeVersion, err := parseMinikubeVersion(string(out))
	if err != nil {
		return fmt.Errorf("parsing minikube version: %w", err)
	}
	if userMinikubeVersion < minikubeVersion {
		var resp string
		fmt.Printf("WARNING: We require at least Minikube v%.2f, while you are using v%.2f\n", minikubeVersion, userMinikubeVersion)
		fmt.Println("You can download a newer version from https://github.com/kubernetes/minikube/releases/")
		fmt.Print("Continue anyway? (not recommended) [y/N]: ")
		fmt.Scanf("%s", &resp)
		if strings.ToLower(resp) != "y" {
			fmt.Println("Installation stopped. Please upgrade minikube and run again")
			os.Exit(0)
		}
	}

	return nil
}

// checkForExistingCluster checks if the user already has a Minikube cluster. If so, it provides
// the option of deleting the existing cluster and recreating it. If not, it proceeds to
// creating a new cluster
func checkForExistingCluster() error {
	getClusters := exec.Command("minikube", "profile", "list")
	out, err := getClusters.CombinedOutput()
	if err != nil {
		// there are no existing minikube profiles, the listing profiles command will error
		// if there were no profiles, we simply want to create a new one and not stop the install
		// so if the error is the "MK_USAGE_NO_PROFILE" error, we ignore it and continue onwards
		if !strings.Contains(string(out), "MK_USAGE_NO_PROFILE") {
			return fmt.Errorf("check cluster: %w", err)
		}
	}
	// TODO Add tests for regex
	r := regexp.MustCompile(clusterName)
	matches := r.Match(out)
	if matches {
		var resp string
		fmt.Print("Knative Cluster " + clusterName + " already installed.\nDelete and recreate [y/N]: ")
		fmt.Scanf("%s", &resp)
		if strings.ToLower(resp) != "y" {
			fmt.Println("Installation skipped")
			return nil
		}
		fmt.Println("deleting cluster...")
		deleteCluster := exec.Command("minikube", "delete", "--profile", clusterName)
		if err := deleteCluster.Run(); err != nil {
			return fmt.Errorf("delete cluster: %w", err)
		}
		if err := createNewCluster(); err != nil {
			return fmt.Errorf("new cluster: %w", err)
		}
		return nil
	}

	if err := createNewCluster(); err != nil {
		return fmt.Errorf("new cluster: %w", err)
	}

	return nil
}

// createNewCluster creates a new Minikube cluster
func createNewCluster() error {
	fmt.Println("☸ Creating Minikube cluster...")
	fmt.Println("\nBy default, using the standard minikube driver for your system")
	fmt.Println("If you wish to use a different driver, please configure minikube using")
	fmt.Print("    minikube config set driver <your-driver>\n\n")

	// create cluster and wait until ready
	createCluster := exec.Command("minikube", "start", "--kubernetes-version", kubernetesVersion, "--cpus", "3", "--profile", clusterName, "--wait", "all")
	if err := runCommandWithOutput(createCluster); err != nil {
		return fmt.Errorf("minikube create: %w", err)
	}

	return nil
}

func runCommandWithOutput(c *exec.Cmd) error {
	c.Stdout = os.Stdout
	c.Stderr = os.Stderr
	if err := c.Run(); err != nil {
		return fmt.Errorf("piping output: %w", err)
	}
	fmt.Print("\n")
	return nil
}

func parseMinikubeVersion(v string) (float64, error) {
	strippedVersion := strings.TrimLeft(strings.TrimRight(v, "\n"), "v")
	dotVersion := strings.Split(strippedVersion, ".")
	floatVersion, err := strconv.ParseFloat(dotVersion[0]+"."+dotVersion[1], 64)
	if err != nil {
		return 0, err
	}

	return floatVersion, nil
}
<|endoftext|>


<commit_before>package models

import (
	"errors"
	"time"

	"github.com/grafana/grafana/pkg/components/securejsondata"
	"github.com/grafana/grafana/pkg/components/simplejson"
)

const (
	DS_GRAPHITE      = "graphite"
	DS_INFLUXDB      = "influxdb"
	DS_INFLUXDB_08   = "influxdb_08"
	DS_ES            = "elasticsearch"
	DS_OPENTSDB      = "opentsdb"
	DS_CLOUDWATCH    = "cloudwatch"
	DS_KAIROSDB      = "kairosdb"
	DS_PROMETHEUS    = "prometheus"
	DS_POSTGRES      = "postgres"
	DS_MYSQL         = "mysql"
	DS_MSSQL         = "mssql"
	DS_ACCESS_DIRECT = "direct"
	DS_ACCESS_PROXY  = "proxy"
	// Stackdriver was renamed Google Cloud monitoring 2020-05 but we keep
	// "stackdriver" to avoid breaking changes in reporting.
	DS_CLOUD_MONITORING = "stackdriver"
	DS_AZURE_MONITOR    = "grafana-azure-monitor-datasource"
	DS_LOKI             = "loki"
)

var (
	ErrDataSourceNotFound                = errors.New("data source not found")
	ErrDataSourceNameExists              = errors.New("data source with the same name already exists")
	ErrDataSourceUidExists               = errors.New("data source with the same uid already exists")
	ErrDataSourceUpdatingOldVersion      = errors.New("trying to update old version of datasource")
	ErrDatasourceIsReadOnly              = errors.New("data source is readonly, can only be updated from configuration")
	ErrDataSourceAccessDenied            = errors.New("data source access denied")
	ErrDataSourceFailedGenerateUniqueUid = errors.New("failed to generate unique datasource ID")
)

type DsAccess string

type DataSource struct {
	Id      int64 `json:"id"`
	OrgId   int64 `json:"orgId"`
	Version int   `json:"version"`

	Name              string                        `json:"name"`
	Type              string                        `json:"type"`
	Access            DsAccess                      `json:"access"`
	Url               string                        `json:"url"`
	Password          string                        `json:"password"`
	User              string                        `json:"user"`
	Database          string                        `json:"database"`
	BasicAuth         bool                          `json:"basicAuth"`
	BasicAuthUser     string                        `json:"basicAuthUser"`
	BasicAuthPassword string                        `json:"basicAuthPassword"`
	WithCredentials   bool                          `json:"withCredentials"`
	IsDefault         bool                          `json:"isDefault"`
	JsonData          *simplejson.Json              `json:"jsonData"`
	SecureJsonData    securejsondata.SecureJsonData `json:"secureJsonData"`
	ReadOnly          bool                          `json:"readOnly"`
	Uid               string                        `json:"uid"`

	Created time.Time `json:"created"`
	Updated time.Time `json:"updated"`
}

// DecryptedBasicAuthPassword returns data source basic auth password in plain text. It uses either deprecated
// basic_auth_password field or encrypted secure_json_data[basicAuthPassword] variable.
func (ds *DataSource) DecryptedBasicAuthPassword() string {
	return ds.decryptedValue("basicAuthPassword", ds.BasicAuthPassword)
}

// DecryptedPassword returns data source password in plain text. It uses either deprecated password field
// or encrypted secure_json_data[password] variable.
func (ds *DataSource) DecryptedPassword() string {
	return ds.decryptedValue("password", ds.Password)
}

// decryptedValue returns decrypted value from secureJsonData
func (ds *DataSource) decryptedValue(field string, fallback string) string {
	if value, ok := ds.DecryptedValue(field); ok {
		return value
	}
	return fallback
}

var knownDatasourcePlugins = map[string]bool{
	DS_ES:                                    true,
	DS_GRAPHITE:                              true,
	DS_INFLUXDB:                              true,
	DS_INFLUXDB_08:                           true,
	DS_KAIROSDB:                              true,
	DS_CLOUDWATCH:                            true,
	DS_PROMETHEUS:                            true,
	DS_OPENTSDB:                              true,
	DS_POSTGRES:                              true,
	DS_MYSQL:                                 true,
	DS_MSSQL:                                 true,
	DS_CLOUD_MONITORING:                      true,
	DS_AZURE_MONITOR:                         true,
	DS_LOKI:                                  true,
	"opennms":                                true,
	"abhisant-druid-datasource":              true,
	"dalmatinerdb-datasource":                true,
	"gnocci":                                 true,
	"zabbix":                                 true,
	"newrelic-app":                           true,
	"grafana-datadog-datasource":             true,
	"grafana-simple-json":                    true,
	"grafana-splunk-datasource":              true,
	"udoprog-heroic-datasource":              true,
	"grafana-openfalcon-datasource":          true,
	"opennms-datasource":                     true,
	"rackerlabs-blueflood-datasource":        true,
	"crate-datasource":                       true,
	"ayoungprogrammer-finance-datasource":    true,
	"monasca-datasource":                     true,
	"vertamedia-clickhouse-datasource":       true,
	"alexanderzobnin-zabbix-datasource":      true,
	"grafana-influxdb-flux-datasource":       true,
	"doitintl-bigquery-datasource":           true,
	"grafana-azure-data-explorer-datasource": true,
}

func IsKnownDataSourcePlugin(dsType string) bool {
	_, exists := knownDatasourcePlugins[dsType]
	return exists
}

// ----------------------
// COMMANDS

// Also acts as api DTO
type AddDataSourceCommand struct {
	Name              string            `json:"name" binding:"Required"`
	Type              string            `json:"type" binding:"Required"`
	Access            DsAccess          `json:"access" binding:"Required"`
	Url               string            `json:"url"`
	Password          string            `json:"password"`
	Database          string            `json:"database"`
	User              string            `json:"user"`
	BasicAuth         bool              `json:"basicAuth"`
	BasicAuthUser     string            `json:"basicAuthUser"`
	BasicAuthPassword string            `json:"basicAuthPassword"`
	WithCredentials   bool              `json:"withCredentials"`
	IsDefault         bool              `json:"isDefault"`
	JsonData          *simplejson.Json  `json:"jsonData"`
	SecureJsonData    map[string]string `json:"secureJsonData"`
	Uid               string            `json:"uid"`

	OrgId    int64 `json:"-"`
	ReadOnly bool  `json:"-"`

	Result *DataSource
}

// Also acts as api DTO
type UpdateDataSourceCommand struct {
	Name              string            `json:"name" binding:"Required"`
	Type              string            `json:"type" binding:"Required"`
	Access            DsAccess          `json:"access" binding:"Required"`
	Url               string            `json:"url"`
	Password          string            `json:"password"`
	User              string            `json:"user"`
	Database          string            `json:"database"`
	BasicAuth         bool              `json:"basicAuth"`
	BasicAuthUser     string            `json:"basicAuthUser"`
	BasicAuthPassword string            `json:"basicAuthPassword"`
	WithCredentials   bool              `json:"withCredentials"`
	IsDefault         bool              `json:"isDefault"`
	JsonData          *simplejson.Json  `json:"jsonData"`
	SecureJsonData    map[string]string `json:"secureJsonData"`
	Version           int               `json:"version"`
	Uid               string            `json:"uid"`

	OrgId    int64 `json:"-"`
	Id       int64 `json:"-"`
	ReadOnly bool  `json:"-"`

	Result *DataSource
}

type DeleteDataSourceByIdCommand struct {
	Id    int64
	OrgId int64

	DeletedDatasourcesCount int64
}

type DeleteDataSourceByNameCommand struct {
	Name  string
	OrgId int64

	DeletedDatasourcesCount int64
}

// ---------------------
// QUERIES

type GetDataSourcesQuery struct {
	OrgId  int64
	User   *SignedInUser
	Result []*DataSource
}

type GetAllDataSourcesQuery struct {
	Result []*DataSource
}

type GetDataSourceByIdQuery struct {
	Id     int64
	OrgId  int64
	Result *DataSource
}

type GetDataSourceByNameQuery struct {
	Name   string
	OrgId  int64
	Result *DataSource
}

// ---------------------
//  Permissions
// ---------------------

type DsPermissionType int

const (
	DsPermissionNoAccess DsPermissionType = iota
	DsPermissionQuery
)

func (p DsPermissionType) String() string {
	names := map[int]string{
		int(DsPermissionQuery):    "Query",
		int(DsPermissionNoAccess): "No Access",
	}
	return names[int(p)]
}

type DatasourcesPermissionFilterQuery struct {
	User        *SignedInUser
	Datasources []*DataSource
	Result      []*DataSource
}
<commit_msg>start tracking usage stats for tempo (#28948)<commit_after>package models

import (
	"errors"
	"time"

	"github.com/grafana/grafana/pkg/components/securejsondata"
	"github.com/grafana/grafana/pkg/components/simplejson"
)

const (
	DS_GRAPHITE      = "graphite"
	DS_INFLUXDB      = "influxdb"
	DS_INFLUXDB_08   = "influxdb_08"
	DS_ES            = "elasticsearch"
	DS_OPENTSDB      = "opentsdb"
	DS_CLOUDWATCH    = "cloudwatch"
	DS_KAIROSDB      = "kairosdb"
	DS_PROMETHEUS    = "prometheus"
	DS_POSTGRES      = "postgres"
	DS_MYSQL         = "mysql"
	DS_MSSQL         = "mssql"
	DS_ACCESS_DIRECT = "direct"
	DS_ACCESS_PROXY  = "proxy"
	// Stackdriver was renamed Google Cloud monitoring 2020-05 but we keep
	// "stackdriver" to avoid breaking changes in reporting.
	DS_CLOUD_MONITORING = "stackdriver"
	DS_AZURE_MONITOR    = "grafana-azure-monitor-datasource"
	DS_LOKI             = "loki"
)

var (
	ErrDataSourceNotFound                = errors.New("data source not found")
	ErrDataSourceNameExists              = errors.New("data source with the same name already exists")
	ErrDataSourceUidExists               = errors.New("data source with the same uid already exists")
	ErrDataSourceUpdatingOldVersion      = errors.New("trying to update old version of datasource")
	ErrDatasourceIsReadOnly              = errors.New("data source is readonly, can only be updated from configuration")
	ErrDataSourceAccessDenied            = errors.New("data source access denied")
	ErrDataSourceFailedGenerateUniqueUid = errors.New("failed to generate unique datasource ID")
)

type DsAccess string

type DataSource struct {
	Id      int64 `json:"id"`
	OrgId   int64 `json:"orgId"`
	Version int   `json:"version"`

	Name              string                        `json:"name"`
	Type              string                        `json:"type"`
	Access            DsAccess                      `json:"access"`
	Url               string                        `json:"url"`
	Password          string                        `json:"password"`
	User              string                        `json:"user"`
	Database          string                        `json:"database"`
	BasicAuth         bool                          `json:"basicAuth"`
	BasicAuthUser     string                        `json:"basicAuthUser"`
	BasicAuthPassword string                        `json:"basicAuthPassword"`
	WithCredentials   bool                          `json:"withCredentials"`
	IsDefault         bool                          `json:"isDefault"`
	JsonData          *simplejson.Json              `json:"jsonData"`
	SecureJsonData    securejsondata.SecureJsonData `json:"secureJsonData"`
	ReadOnly          bool                          `json:"readOnly"`
	Uid               string                        `json:"uid"`

	Created time.Time `json:"created"`
	Updated time.Time `json:"updated"`
}

// DecryptedBasicAuthPassword returns data source basic auth password in plain text. It uses either deprecated
// basic_auth_password field or encrypted secure_json_data[basicAuthPassword] variable.
func (ds *DataSource) DecryptedBasicAuthPassword() string {
	return ds.decryptedValue("basicAuthPassword", ds.BasicAuthPassword)
}

// DecryptedPassword returns data source password in plain text. It uses either deprecated password field
// or encrypted secure_json_data[password] variable.
func (ds *DataSource) DecryptedPassword() string {
	return ds.decryptedValue("password", ds.Password)
}

// decryptedValue returns decrypted value from secureJsonData
func (ds *DataSource) decryptedValue(field string, fallback string) string {
	if value, ok := ds.DecryptedValue(field); ok {
		return value
	}
	return fallback
}

var knownDatasourcePlugins = map[string]bool{
	DS_ES:                                    true,
	DS_GRAPHITE:                              true,
	DS_INFLUXDB:                              true,
	DS_INFLUXDB_08:                           true,
	DS_KAIROSDB:                              true,
	DS_CLOUDWATCH:                            true,
	DS_PROMETHEUS:                            true,
	DS_OPENTSDB:                              true,
	DS_POSTGRES:                              true,
	DS_MYSQL:                                 true,
	DS_MSSQL:                                 true,
	DS_CLOUD_MONITORING:                      true,
	DS_AZURE_MONITOR:                         true,
	DS_LOKI:                                  true,
	"opennms":                                true,
	"abhisant-druid-datasource":              true,
	"dalmatinerdb-datasource":                true,
	"gnocci":                                 true,
	"zabbix":                                 true,
	"newrelic-app":                           true,
	"grafana-datadog-datasource":             true,
	"grafana-simple-json":                    true,
	"grafana-splunk-datasource":              true,
	"udoprog-heroic-datasource":              true,
	"grafana-openfalcon-datasource":          true,
	"opennms-datasource":                     true,
	"rackerlabs-blueflood-datasource":        true,
	"crate-datasource":                       true,
	"ayoungprogrammer-finance-datasource":    true,
	"monasca-datasource":                     true,
	"vertamedia-clickhouse-datasource":       true,
	"alexanderzobnin-zabbix-datasource":      true,
	"grafana-influxdb-flux-datasource":       true,
	"doitintl-bigquery-datasource":           true,
	"grafana-azure-data-explorer-datasource": true,
	"tempo":                                  true,
}

func IsKnownDataSourcePlugin(dsType string) bool {
	_, exists := knownDatasourcePlugins[dsType]
	return exists
}

// ----------------------
// COMMANDS

// Also acts as api DTO
type AddDataSourceCommand struct {
	Name              string            `json:"name" binding:"Required"`
	Type              string            `json:"type" binding:"Required"`
	Access            DsAccess          `json:"access" binding:"Required"`
	Url               string            `json:"url"`
	Password          string            `json:"password"`
	Database          string            `json:"database"`
	User              string            `json:"user"`
	BasicAuth         bool              `json:"basicAuth"`
	BasicAuthUser     string            `json:"basicAuthUser"`
	BasicAuthPassword string            `json:"basicAuthPassword"`
	WithCredentials   bool              `json:"withCredentials"`
	IsDefault         bool              `json:"isDefault"`
	JsonData          *simplejson.Json  `json:"jsonData"`
	SecureJsonData    map[string]string `json:"secureJsonData"`
	Uid               string            `json:"uid"`

	OrgId    int64 `json:"-"`
	ReadOnly bool  `json:"-"`

	Result *DataSource
}

// Also acts as api DTO
type UpdateDataSourceCommand struct {
	Name              string            `json:"name" binding:"Required"`
	Type              string            `json:"type" binding:"Required"`
	Access            DsAccess          `json:"access" binding:"Required"`
	Url               string            `json:"url"`
	Password          string            `json:"password"`
	User              string            `json:"user"`
	Database          string            `json:"database"`
	BasicAuth         bool              `json:"basicAuth"`
	BasicAuthUser     string            `json:"basicAuthUser"`
	BasicAuthPassword string            `json:"basicAuthPassword"`
	WithCredentials   bool              `json:"withCredentials"`
	IsDefault         bool              `json:"isDefault"`
	JsonData          *simplejson.Json  `json:"jsonData"`
	SecureJsonData    map[string]string `json:"secureJsonData"`
	Version           int               `json:"version"`
	Uid               string            `json:"uid"`

	OrgId    int64 `json:"-"`
	Id       int64 `json:"-"`
	ReadOnly bool  `json:"-"`

	Result *DataSource
}

type DeleteDataSourceByIdCommand struct {
	Id    int64
	OrgId int64

	DeletedDatasourcesCount int64
}

type DeleteDataSourceByNameCommand struct {
	Name  string
	OrgId int64

	DeletedDatasourcesCount int64
}

// ---------------------
// QUERIES

type GetDataSourcesQuery struct {
	OrgId  int64
	User   *SignedInUser
	Result []*DataSource
}

type GetAllDataSourcesQuery struct {
	Result []*DataSource
}

type GetDataSourceByIdQuery struct {
	Id     int64
	OrgId  int64
	Result *DataSource
}

type GetDataSourceByNameQuery struct {
	Name   string
	OrgId  int64
	Result *DataSource
}

// ---------------------
//  Permissions
// ---------------------

type DsPermissionType int

const (
	DsPermissionNoAccess DsPermissionType = iota
	DsPermissionQuery
)

func (p DsPermissionType) String() string {
	names := map[int]string{
		int(DsPermissionQuery):    "Query",
		int(DsPermissionNoAccess): "No Access",
	}
	return names[int(p)]
}

type DatasourcesPermissionFilterQuery struct {
	User        *SignedInUser
	Datasources []*DataSource
	Result      []*DataSource
}
<|endoftext|>


<commit_before>package systemd

import (
	"errors"
	"fmt"
	"net"
	"strconv"

	"github.com/coreos/go-systemd/activation"
)

// ListenFD returns the specified socket activated files as a slice of
// net.Listeners or all of the activated files if "*" is given.
func ListenFD(addr string) ([]net.Listener, error) {
	files := activation.Files(false)
	if files == nil || len(files) == 0 {
		return nil, errors.New("No sockets found")
	}

	// default to all fds just like unix:// and tcp://
	if addr == "" {
		addr = "*"
	}

	fdNum, _ := strconv.Atoi(addr)
	fdOffset := fdNum - 3
	if (addr != "*") && (len(files) < int(fdOffset)+1) {
		return nil, errors.New("Too few socket activated files passed in")
	}

	// socket activation
	listeners := make([]net.Listener, len(files))
	for i, f := range files {
		var err error
		listeners[i], err = net.FileListener(f)
		if err != nil {
			return nil, fmt.Errorf("Error setting up FileListener for fd %d: %s", f.Fd(), err.Error())
		}
	}

	if addr == "*" {
		return listeners, nil
	}

	return []net.Listener{listeners[fdOffset]}, nil
}
<commit_msg>chore(systemd): use activation.Listeners instead of Files<commit_after>package systemd

import (
	"errors"
	"net"
	"strconv"

	"github.com/coreos/go-systemd/activation"
)

// ListenFD returns the specified socket activated files as a slice of
// net.Listeners or all of the activated files if "*" is given.
func ListenFD(addr string) ([]net.Listener, error) {
	// socket activation
	listeners, err := activation.Listeners(false)
	if err != nil {
		return nil, err
	}

	if listeners == nil || len(listeners) == 0 {
		return nil, errors.New("No sockets found")
	}

	// default to all fds just like unix:// and tcp://
	if addr == "" {
		addr = "*"
	}

	fdNum, _ := strconv.Atoi(addr)
	fdOffset := fdNum - 3
	if (addr != "*") && (len(listeners) < int(fdOffset)+1) {
		return nil, errors.New("Too few socket activated files passed in")
	}

	if addr == "*" {
		return listeners, nil
	}

	return []net.Listener{listeners[fdOffset]}, nil
}
<|endoftext|>


<commit_before>// Copyright 2011, 2012, 2013 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package environs

import (
	"io"
	"os"

	"github.com/juju/juju/constraints"
	"github.com/juju/juju/environs/cloudinit"
	"github.com/juju/juju/environs/config"
	"github.com/juju/juju/environs/storage"
	"github.com/juju/juju/instance"
	"github.com/juju/juju/network"
	"github.com/juju/juju/state"
	"github.com/juju/juju/tools"
)

// A EnvironProvider represents a computing and storage provider.
type EnvironProvider interface {
	// Prepare prepares an environment for use. Any additional
	// configuration attributes in the returned environment should
	// be saved to be used later. If the environment is already
	// prepared, this call is equivalent to Open.
	Prepare(ctx BootstrapContext, cfg *config.Config) (Environ, error)

	// Open opens the environment and returns it.
	// The configuration must have come from a previously
	// prepared environment.
	Open(cfg *config.Config) (Environ, error)

	// Validate ensures that config is a valid configuration for this
	// provider, applying changes to it if necessary, and returns the
	// validated configuration.
	// If old is not nil, it holds the previous environment configuration
	// for consideration when validating changes.
	Validate(cfg, old *config.Config) (valid *config.Config, err error)

	// Boilerplate returns a default configuration for the environment in yaml format.
	// The text should be a key followed by some number of attributes:
	//    `environName:
	//        type: environTypeName
	//        attr1: val1
	//    `
	// The text is used as a template (see the template package) with one extra template
	// function available, rand, which expands to a random hexadecimal string when invoked.
	BoilerplateConfig() string

	// SecretAttrs filters the supplied configuration returning only values
	// which are considered sensitive. All of the values of these secret
	// attributes need to be strings.
	SecretAttrs(cfg *config.Config) (map[string]string, error)
}

// EnvironStorage implements storage access for an environment.
type EnvironStorage interface {
	// Storage returns storage specific to the environment.
	Storage() storage.Storage
}

// ConfigGetter implements access to an environment's configuration.
type ConfigGetter interface {
	// Config returns the configuration data with which the Environ was created.
	// Note that this is not necessarily current; the canonical location
	// for the configuration data is stored in the state.
	Config() *config.Config
}

// BootstrapParams holds the parameters for bootstrapping an environment.
type BootstrapParams struct {
	// Constraints are used to choose the initial instance specification,
	// and will be stored in the new environment's state.
	Constraints constraints.Value

	// Placement, if non-empty, holds an environment-specific placement
	// directive used to choose the initial instance.
	Placement string

	// AvailableTools is a collection of tools which the Bootstrap method
	// may use to decide which architecture/series to instantiate.
	AvailableTools tools.List

	// ContainerBridgeName, if non-empty, overrides the default
	// network bridge device to use for LXC and KVM containers. See
	// environs.DefaultBridgeName.
	ContainerBridgeName string
}

// BootstrapFinalizer is a function returned from Environ.Bootstrap.
// The caller must pass a MachineConfig with the Tools field set.
type BootstrapFinalizer func(BootstrapContext, *cloudinit.MachineConfig) error

// An Environ represents a juju environment as specified
// in the environments.yaml file.
//
// Due to the limitations of some providers (for example ec2), the
// results of the Environ methods may not be fully sequentially
// consistent. In particular, while a provider may retry when it
// gets an error for an operation, it will not retry when
// an operation succeeds, even if that success is not
// consistent with a previous operation.
//
// Even though Juju takes care not to share an Environ between concurrent
// workers, it does allow concurrent method calls into the provider
// implementation.  The typical provider implementation needs locking to
// avoid undefined behaviour when the configuration changes.
type Environ interface {
	// Bootstrap creates a new instance with the series and architecture
	// of its choice, constrained to those of the available tools, and
	// returns the instance's architecture, series, and a function that
	// must be called to finalize the bootstrap process by transferring
	// the tools and installing the initial Juju state server.
	//
	// It is possible to direct Bootstrap to use a specific architecture
	// (or fail if it cannot start an instance of that architecture) by
	// using an architecture constraint; this will have the effect of
	// limiting the available tools to just those matching the specified
	// architecture.
	Bootstrap(ctx BootstrapContext, params BootstrapParams) (arch, series string, _ BootstrapFinalizer, _ error)

	// InstanceBroker defines methods for starting and stopping
	// instances.
	InstanceBroker

	// AllocateAddress requests a specific address to be allocated for the
	// given instance on the given network.
	AllocateAddress(instId instance.Id, netId network.Id, addr network.Address) error

	// ReleaseAddress releases a specific address previously allocated with
	// AllocateAddress.
	ReleaseAddress(instId instance.Id, netId network.Id, addr network.Address) error

	// Subnets returns basic information about subnets known
	// by the provider for the environment.
	Subnets(inst instance.Id, networks []network.Id) ([]network.SubnetInfo, error)

	// NetworkInterfaces requests information about the network
	// interfaces on the given instance.
	NetworkInterfaces(instId instance.Id) ([]network.InterfaceInfo, error)

	// ConfigGetter allows the retrieval of the configuration data.
	ConfigGetter

	// EnvironCapability allows access to this environment's capabilities.
	state.EnvironCapability

	// ConstraintsValidator returns a Validator instance which
	// is used to validate and merge constraints.
	ConstraintsValidator() (constraints.Validator, error)

	// SetConfig updates the Environ's configuration.
	//
	// Calls to SetConfig do not affect the configuration of
	// values previously obtained from Storage.
	SetConfig(cfg *config.Config) error

	// Instances returns a slice of instances corresponding to the
	// given instance ids.  If no instances were found, but there
	// was no other error, it will return ErrNoInstances.  If
	// some but not all the instances were found, the returned slice
	// will have some nil slots, and an ErrPartialInstances error
	// will be returned.
	Instances(ids []instance.Id) ([]instance.Instance, error)

	// StateServerInstances returns the IDs of instances corresponding
	// to Juju state servers. If there are no state server instances,
	// ErrNotBootstrapped is returned.
	StateServerInstances() ([]instance.Id, error)

	// Destroy shuts down all known machines and destroys the
	// rest of the environment. Note that on some providers,
	// very recently started instances may not be destroyed
	// because they are not yet visible.
	//
	// When Destroy has been called, any Environ referring to the
	// same remote environment may become invalid
	Destroy() error

	// OpenPorts opens the given port ranges for the whole environment.
	// Must only be used if the environment was setup with the
	// FwGlobal firewall mode.
	OpenPorts(ports []network.PortRange) error

	// ClosePorts closes the given port ranges for the whole environment.
	// Must only be used if the environment was setup with the
	// FwGlobal firewall mode.
	ClosePorts(ports []network.PortRange) error

	// Ports returns the port ranges opened for the whole environment.
	// Must only be used if the environment was setup with the
	// FwGlobal firewall mode.
	Ports() ([]network.PortRange, error)

	// Provider returns the EnvironProvider that created this Environ.
	Provider() EnvironProvider

	state.Prechecker
}

// BootstrapContext is an interface that is passed to
// Environ.Bootstrap, providing a means of obtaining
// information about and manipulating the context in which
// it is being invoked.
type BootstrapContext interface {
	GetStdin() io.Reader
	GetStdout() io.Writer
	GetStderr() io.Writer
	Infof(format string, params ...interface{})
	Verbosef(format string, params ...interface{})

	// InterruptNotify starts watching for interrupt signals
	// on behalf of the caller, sending them to the supplied
	// channel.
	InterruptNotify(sig chan<- os.Signal)

	// StopInterruptNotify undoes the effects of a previous
	// call to InterruptNotify with the same channel. After
	// StopInterruptNotify returns, no more signals will be
	// delivered to the channel.
	StopInterruptNotify(chan<- os.Signal)

	// ShouldVerifyCredentials indicates whether the caller's cloud
	// credentials should be verified.
	ShouldVerifyCredentials() bool
}
<commit_msg>Tweak<commit_after>// Copyright 2011, 2012, 2013 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package environs

import (
	"io"
	"os"

	"github.com/juju/juju/constraints"
	"github.com/juju/juju/environs/cloudinit"
	"github.com/juju/juju/environs/config"
	"github.com/juju/juju/environs/storage"
	"github.com/juju/juju/instance"
	"github.com/juju/juju/network"
	"github.com/juju/juju/state"
	"github.com/juju/juju/tools"
)

// A EnvironProvider represents a computing and storage provider.
type EnvironProvider interface {
	// Prepare prepares an environment for use. Any additional
	// configuration attributes in the returned environment should
	// be saved to be used later. If the environment is already
	// prepared, this call is equivalent to Open.
	Prepare(ctx BootstrapContext, cfg *config.Config) (Environ, error)

	// Open opens the environment and returns it.
	// The configuration must have come from a previously
	// prepared environment.
	Open(cfg *config.Config) (Environ, error)

	// Validate ensures that config is a valid configuration for this
	// provider, applying changes to it if necessary, and returns the
	// validated configuration.
	// If old is not nil, it holds the previous environment configuration
	// for consideration when validating changes.
	Validate(cfg, old *config.Config) (valid *config.Config, err error)

	// Boilerplate returns a default configuration for the environment in yaml format.
	// The text should be a key followed by some number of attributes:
	//    `environName:
	//        type: environTypeName
	//        attr1: val1
	//    `
	// The text is used as a template (see the template package) with one extra template
	// function available, rand, which expands to a random hexadecimal string when invoked.
	BoilerplateConfig() string

	// SecretAttrs filters the supplied configuration returning only values
	// which are considered sensitive. All of the values of these secret
	// attributes need to be strings.
	SecretAttrs(cfg *config.Config) (map[string]string, error)
}

// EnvironStorage implements storage access for an environment.
type EnvironStorage interface {
	// Storage returns storage specific to the environment.
	Storage() storage.Storage
}

// ConfigGetter implements access to an environment's configuration.
type ConfigGetter interface {
	// Config returns the configuration data with which the Environ was created.
	// Note that this is not necessarily current; the canonical location
	// for the configuration data is stored in the state.
	Config() *config.Config
}

// BootstrapParams holds the parameters for bootstrapping an environment.
type BootstrapParams struct {
	// Constraints are used to choose the initial instance specification,
	// and will be stored in the new environment's state.
	Constraints constraints.Value

	// Placement, if non-empty, holds an environment-specific placement
	// directive used to choose the initial instance.
	Placement string

	// AvailableTools is a collection of tools which the Bootstrap method
	// may use to decide which architecture/series to instantiate.
	AvailableTools tools.List

	// ContainerBridgeName, if non-empty, overrides the default
	// network bridge device to use for LXC and KVM containers. See
	// environs.DefaultBridgeName.
	ContainerBridgeName string
}

// BootstrapFinalizer is a function returned from Environ.Bootstrap.
// The caller must pass a MachineConfig with the Tools field set.
type BootstrapFinalizer func(BootstrapContext, *cloudinit.MachineConfig) error

// An Environ represents a juju environment as specified
// in the environments.yaml file.
//
// Due to the limitations of some providers (for example ec2), the
// results of the Environ methods may not be fully sequentially
// consistent. In particular, while a provider may retry when it
// gets an error for an operation, it will not retry when
// an operation succeeds, even if that success is not
// consistent with a previous operation.
//
// Even though Juju takes care not to share an Environ between concurrent
// workers, it does allow concurrent method calls into the provider
// implementation.  The typical provider implementation needs locking to
// avoid undefined behaviour when the configuration changes.
type Environ interface {
	// Bootstrap creates a new instance with the series and architecture
	// of its choice, constrained to those of the available tools, and
	// returns the instance's architecture, series, and a function that
	// must be called to finalize the bootstrap process by transferring
	// the tools and installing the initial Juju state server.
	//
	// It is possible to direct Bootstrap to use a specific architecture
	// (or fail if it cannot start an instance of that architecture) by
	// using an architecture constraint; this will have the effect of
	// limiting the available tools to just those matching the specified
	// architecture.
	Bootstrap(ctx BootstrapContext, params BootstrapParams) (arch, series string, _ BootstrapFinalizer, _ error)

	// InstanceBroker defines methods for starting and stopping
	// instances.
	InstanceBroker

	// AllocateAddress requests a specific address to be allocated for the
	// given instance on the given network.
	AllocateAddress(instId instance.Id, netId network.Id, addr network.Address) error

	// ReleaseAddress releases a specific address previously allocated with
	// AllocateAddress.
	ReleaseAddress(instId instance.Id, netId network.Id, addr network.Address) error

	// Subnets returns basic information about subnets known
	// by the provider for the environment.
	Subnets(inst instance.Id, subnetIds []network.Id) ([]network.SubnetInfo, error)

	// NetworkInterfaces requests information about the network
	// interfaces on the given instance.
	NetworkInterfaces(instId instance.Id) ([]network.InterfaceInfo, error)

	// ConfigGetter allows the retrieval of the configuration data.
	ConfigGetter

	// EnvironCapability allows access to this environment's capabilities.
	state.EnvironCapability

	// ConstraintsValidator returns a Validator instance which
	// is used to validate and merge constraints.
	ConstraintsValidator() (constraints.Validator, error)

	// SetConfig updates the Environ's configuration.
	//
	// Calls to SetConfig do not affect the configuration of
	// values previously obtained from Storage.
	SetConfig(cfg *config.Config) error

	// Instances returns a slice of instances corresponding to the
	// given instance ids.  If no instances were found, but there
	// was no other error, it will return ErrNoInstances.  If
	// some but not all the instances were found, the returned slice
	// will have some nil slots, and an ErrPartialInstances error
	// will be returned.
	Instances(ids []instance.Id) ([]instance.Instance, error)

	// StateServerInstances returns the IDs of instances corresponding
	// to Juju state servers. If there are no state server instances,
	// ErrNotBootstrapped is returned.
	StateServerInstances() ([]instance.Id, error)

	// Destroy shuts down all known machines and destroys the
	// rest of the environment. Note that on some providers,
	// very recently started instances may not be destroyed
	// because they are not yet visible.
	//
	// When Destroy has been called, any Environ referring to the
	// same remote environment may become invalid
	Destroy() error

	// OpenPorts opens the given port ranges for the whole environment.
	// Must only be used if the environment was setup with the
	// FwGlobal firewall mode.
	OpenPorts(ports []network.PortRange) error

	// ClosePorts closes the given port ranges for the whole environment.
	// Must only be used if the environment was setup with the
	// FwGlobal firewall mode.
	ClosePorts(ports []network.PortRange) error

	// Ports returns the port ranges opened for the whole environment.
	// Must only be used if the environment was setup with the
	// FwGlobal firewall mode.
	Ports() ([]network.PortRange, error)

	// Provider returns the EnvironProvider that created this Environ.
	Provider() EnvironProvider

	state.Prechecker
}

// BootstrapContext is an interface that is passed to
// Environ.Bootstrap, providing a means of obtaining
// information about and manipulating the context in which
// it is being invoked.
type BootstrapContext interface {
	GetStdin() io.Reader
	GetStdout() io.Writer
	GetStderr() io.Writer
	Infof(format string, params ...interface{})
	Verbosef(format string, params ...interface{})

	// InterruptNotify starts watching for interrupt signals
	// on behalf of the caller, sending them to the supplied
	// channel.
	InterruptNotify(sig chan<- os.Signal)

	// StopInterruptNotify undoes the effects of a previous
	// call to InterruptNotify with the same channel. After
	// StopInterruptNotify returns, no more signals will be
	// delivered to the channel.
	StopInterruptNotify(chan<- os.Signal)

	// ShouldVerifyCredentials indicates whether the caller's cloud
	// credentials should be verified.
	ShouldVerifyCredentials() bool
}
<|endoftext|>


<commit_before>package streams

import (
	"io"
	"os"
	"path"
	"path/filepath"

	"github.com/GitbookIO/go-gitbook-api/utils"
)

type StreamFunc func(path string) (io.ReadCloser, error)

func PickStream(p string) (io.ReadCloser, error) {
	basepath := filepath.Base(p)

	if !exists(p) {
		return nil, fmt.Errorf("PickStream: Path '%s' does not exist", p)
	}

	// Tar.gz
	if strings.HasSuffix(basepath, ".tar.gz") || strings.HasSuffix(basepath, ".tgz") {
		return File(p)
	}

	// Git repo
	if isGitDir(p) {
		return GitHead(p)
	} else if dir := path.Join(p, ".git"); isGitDir(dir) {
		return GitHead(dir)
	}

	// Standard folder
	return Folder(p)
}

func GitHead(p string) (io.ReadCloser, error) {
	return Git(p, "HEAD")
}

// Git returns an io.ReadCloser of a repo as a tar.gz
func Git(p, ref string) (io.ReadCloser, error) {
	return utils.GitTarGz(p, ref)
}

func Folder(p string) (io.ReadCloser, error) {
	return utils.TarGzExclude(
		p,

		// Excluded files & folders
		".git",
		"node_modules",
		"bower",
		"_book",
		"book.pdf",
		"book.mobi",
		"book.epub",
	)
}

func File(p string) (io.ReadCloser, error) {
	return os.Open(p)
}

func isGitDir(dirpath string) bool {
	return (exists(path.Join(dirpath, "HEAD")) &&
		exists(path.Join(dirpath, "objects")) &&
		exists(path.Join(dirpath, "refs")))
}

// Does a file exist on disk ?
func exists(path string) bool {
	_, err := os.Stat(path)
	if err == nil {
		return true
	}
	if os.IsNotExist(err) {
		return false
	}
	return false
}
<commit_msg>Add missing imports to streams/streams.go<commit_after>package streams

import (
	"fmt"
	"io"
	"os"
	"path"
	"path/filepath"
	"strings"

	"github.com/GitbookIO/go-gitbook-api/utils"
)

type StreamFunc func(path string) (io.ReadCloser, error)

func PickStream(p string) (io.ReadCloser, error) {
	basepath := filepath.Base(p)

	if !exists(p) {
		return nil, fmt.Errorf("PickStream: Path '%s' does not exist", p)
	}

	// Tar.gz
	if strings.HasSuffix(basepath, ".tar.gz") || strings.HasSuffix(basepath, ".tgz") {
		return File(p)
	}

	// Git repo
	if isGitDir(p) {
		return GitHead(p)
	} else if dir := path.Join(p, ".git"); isGitDir(dir) {
		return GitHead(dir)
	}

	// Standard folder
	return Folder(p)
}

func GitHead(p string) (io.ReadCloser, error) {
	return Git(p, "HEAD")
}

// Git returns an io.ReadCloser of a repo as a tar.gz
func Git(p, ref string) (io.ReadCloser, error) {
	return utils.GitTarGz(p, ref)
}

func Folder(p string) (io.ReadCloser, error) {
	return utils.TarGzExclude(
		p,

		// Excluded files & folders
		".git",
		"node_modules",
		"bower",
		"_book",
		"book.pdf",
		"book.mobi",
		"book.epub",
	)
}

func File(p string) (io.ReadCloser, error) {
	return os.Open(p)
}

func isGitDir(dirpath string) bool {
	return (exists(path.Join(dirpath, "HEAD")) &&
		exists(path.Join(dirpath, "objects")) &&
		exists(path.Join(dirpath, "refs")))
}

// Does a file exist on disk ?
func exists(path string) bool {
	_, err := os.Stat(path)
	if err == nil {
		return true
	}
	if os.IsNotExist(err) {
		return false
	}
	return false
}
<|endoftext|>


<commit_before>// Package api provides rest-like server
package api

import (
	"context"
	"encoding/xml"
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	"github.com/didip/tollbooth/v6"
	"github.com/didip/tollbooth_chi"
	"github.com/go-chi/chi/v5"
	"github.com/go-chi/chi/v5/middleware"
	"github.com/go-chi/render"
	"github.com/go-pkgz/lcw"
	log "github.com/go-pkgz/lgr"
	"github.com/go-pkgz/rest"
	"github.com/go-pkgz/rest/logger"

	"github.com/umputun/feed-master/app/config"
	"github.com/umputun/feed-master/app/feed"
	"github.com/umputun/feed-master/app/proc"
	"github.com/umputun/feed-master/app/youtube"
)

// Server provides HTTP API
type Server struct {
	Version    string
	Conf       config.Conf
	Store      *proc.BoltDB
	YoutubeSvc YoutubeSvc
	httpServer *http.Server
	cache      lcw.LoadingCache
}

// YoutubeSvc provides access to youtube's audio rss
type YoutubeSvc interface {
	RSSFeed(cinfo youtube.FeedInfo) (string, error)
}

// Run starts http server for API with all routes
func (s *Server) Run(ctx context.Context, port int) {
	var err error
	if s.cache, err = lcw.NewExpirableCache(lcw.TTL(time.Minute*5), lcw.MaxCacheSize(10*1024*1024)); err != nil {
		log.Printf("[PANIC] failed to make loading cache, %v", err)
		return
	}

	go func() {
		<-ctx.Done()
		if s.httpServer != nil {
			if clsErr := s.httpServer.Close(); clsErr != nil {
				log.Printf("[ERROR] failed to close proxy http server, %v", clsErr)
			}
		}
	}()

	s.httpServer = &http.Server{
		Addr:              fmt.Sprintf(":%d", port),
		Handler:           s.router(),
		ReadHeaderTimeout: 5 * time.Second,
		WriteTimeout:      30 * time.Second,
		IdleTimeout:       30 * time.Second,
	}
	err = s.httpServer.ListenAndServe()
	log.Printf("[WARN] http server terminated, %s", err)
}

func (s *Server) router() *chi.Mux {
	router := chi.NewRouter()
	router.Use(middleware.RealIP, rest.Recoverer(log.Default()))
	router.Use(middleware.Throttle(1000), middleware.Timeout(60*time.Second))
	router.Use(rest.AppInfo("feed-master", "umputun", s.Version), rest.Ping)
	router.Use(tollbooth_chi.LimitHandler(tollbooth.NewLimiter(5, nil)))

	router.Group(func(rimg chi.Router) {
		l := logger.New(logger.Log(log.Default()), logger.Prefix("[DEBUG]"))
		rimg.Use(l.Handler)
		rimg.Get("/images/{name}", s.getImageCtrl)
		rimg.Get("/image/{name}", s.getImageCtrl)
		rimg.Head("/image/{name}", s.getImageHeadCtrl)
		rimg.Head("/images/{name}", s.getImageHeadCtrl)
	})

	router.Group(func(rrss chi.Router) {
		l := logger.New(logger.Log(log.Default()), logger.Prefix("[INFO]"))
		rrss.Use(l.Handler)
		rrss.Get("/rss/{name}", s.getFeedCtrl)
		rrss.Get("/list", s.getListCtrl)
		rrss.Get("/feed/{name}", s.getFeedPageCtrl)
		rrss.Get("/feed/{name}/sources", s.getSourcesPageCtrl)
		rrss.Get("/feeds", s.getFeedsPageCtrl)
	})

	router.Route("/yt", func(r chi.Router) {
		l := logger.New(logger.Log(log.Default()), logger.Prefix("[INFO]"))
		r.Use(l.Handler)
		r.Get("/rss/{channel}", s.getYoutubeFeedCtrl)
	})

	if s.Conf.YouTube.BaseURL != "" {
		baseYtURL, parseErr := url.Parse(s.Conf.YouTube.BaseURL)
		if parseErr != nil {
			log.Printf("[ERROR] failed to parse base url %s, %v", s.Conf.YouTube.BaseURL, parseErr)
		}
		ytfs, fsErr := rest.NewFileServer(baseYtURL.Path, s.Conf.YouTube.FilesLocation)
		if fsErr == nil {
			router.Mount(baseYtURL.Path, ytfs)
		} else {
			log.Printf("[WARN] can't start static file server for yt, %v", fsErr)
		}
	}

	fs, err := rest.NewFileServer("/static", filepath.Join("webapp", "static"))
	if err == nil {
		router.Mount("/static", fs)
	} else {
		log.Printf("[WARN] can't start static file server, %v", err)
	}
	return router
}

// GET /rss/{name} - returns rss for given feeds set
func (s *Server) getFeedCtrl(w http.ResponseWriter, r *http.Request) {
	feedName := chi.URLParam(r, "name")
	items, err := s.Store.Load(feedName, s.Conf.System.MaxTotal, true)
	if err != nil {
		rest.SendErrorJSON(w, r, log.Default(), http.StatusBadRequest, err, "failed to get feed")
		return
	}

	for i, itm := range items {
		// add ts suffix to titles
		switch s.Conf.Feeds[feedName].ExtendDateTitle {
		case "yyyyddmm":
			items[i].Title = fmt.Sprintf("%s (%s)", itm.Title, itm.DT.Format("2006-02-01"))
		case "yyyymmdd":
			items[i].Title = fmt.Sprintf("%s (%s)", itm.Title, itm.DT.Format("2006-01-02"))
		}
	}

	rss := feed.Rss2{
		Version:       "2.0",
		NsItunes:      "http://www.itunes.com/dtds/podcast-1.0.dtd",
		NsMedia:       "http://search.yahoo.com/mrss/",
		ItemList:      items,
		Title:         s.Conf.Feeds[feedName].Title,
		Description:   s.Conf.Feeds[feedName].Description,
		Language:      s.Conf.Feeds[feedName].Language,
		Link:          s.Conf.Feeds[feedName].Link,
		PubDate:       items[0].PubDate,
		LastBuildDate: time.Now().Format(time.RFC822Z),
		NsItunes:      "http://www.itunes.com/dtds/podcast-1.0.dtd",
	}

	// replace link to UI page
	if s.Conf.System.BaseURL != "" {
		baseURL := s.Conf.System.BaseURL
		rss.Link = baseURL + "/feed/" + feedName
		rss.ItunesImage = feed.ItunesImg{
			URL: baseURL + "/image/" + feedName,
		}
		rss.MediaThumbnail = feed.MediaThumbnail{
			URL: baseURL + "/image/" + feedName,
		}
	}

	b, err := xml.MarshalIndent(&rss, "", "  ")
	if err != nil {
		rest.SendErrorJSON(w, r, log.Default(), http.StatusInternalServerError, err, "failed to marshal rss")
		return
	}

	w.Header().Set("Content-Type", "application/xml; charset=UTF-8")
	res := `<?xml version="1.0" encoding="UTF-8"?>` + "\n" + string(b)
	// this hack to avoid having different items for marshal and unmarshal due to "itunes" namespace
	res = strings.Replace(res, "<duration>", "<itunes:duration>", -1)
	res = strings.Replace(res, "</duration>", "</itunes:duration>", -1)

	_, _ = fmt.Fprintf(w, "%s", res)
}

// GET /image/{name}
func (s *Server) getImageCtrl(w http.ResponseWriter, r *http.Request) {
	fm := chi.URLParam(r, "name")
	fm = strings.TrimSuffix(fm, ".png")
	feedConf, found := s.Conf.Feeds[fm]
	if !found {
		rest.SendErrorJSON(w, r, log.Default(), http.StatusBadRequest,
			fmt.Errorf("image %s not found", fm), "failed to load image")
		return
	}

	b, err := os.ReadFile(feedConf.Image)
	if err != nil {
		rest.SendErrorJSON(w, r, log.Default(), http.StatusBadRequest,
			errors.New("can't read  "+chi.URLParam(r, "name")), "failed to read image")
		return
	}
	w.Header().Set("Content-Type", "image/png")
	if _, err := w.Write(b); err != nil {
		log.Printf("[WARN] failed to send image, %s", err)
	}
}

// HEAD /image/{name}
func (s *Server) getImageHeadCtrl(w http.ResponseWriter, r *http.Request) {
	fm := chi.URLParam(r, "name")
	fm = strings.TrimSuffix(fm, ".png")
	feedConf, found := s.Conf.Feeds[fm]
	if !found {
		w.WriteHeader(http.StatusNotFound)
		return
	}

	info, err := os.Stat(feedConf.Image)
	if err != nil {
		w.WriteHeader(http.StatusNotFound)
		return
	}
	w.Header().Set("Content-Type", "image/png")
	w.Header().Set("Content-Length", strconv.Itoa(int(info.Size())))
	w.WriteHeader(http.StatusOK)
}

// GET /list - returns feed's image
func (s *Server) getListCtrl(w http.ResponseWriter, r *http.Request) {
	feeds := s.feeds()
	render.JSON(w, r, feeds)
}

// GET /yt/rss/{channel} - returns rss for given youtube channel
func (s *Server) getYoutubeFeedCtrl(w http.ResponseWriter, r *http.Request) {
	channel := chi.URLParam(r, "channel")

	fi := youtube.FeedInfo{ID: channel}
	for _, f := range s.Conf.YouTube.Channels {
		if f.ID == channel {
			fi = f
			break
		}
	}

	res, err := s.YoutubeSvc.RSSFeed(fi)
	if err != nil {
		rest.SendErrorJSON(w, r, log.Default(), http.StatusInternalServerError, err, "failed to read yt list")
		return
	}

	w.Header().Set("Content-Type", "application/xml; charset=UTF-8")
	res = `<?xml version="1.0" encoding="UTF-8"?>` + "\n" + res
	_, _ = fmt.Fprintf(w, "%s", res)
}

func (s *Server) feeds() []string {
	feeds := make([]string, 0, len(s.Conf.Feeds))
	for k := range s.Conf.Feeds {
		feeds = append(feeds, k)
	}
	return feeds
}
<commit_msg>handle case when no image<commit_after>// Package api provides rest-like server
package api

import (
	"context"
	"encoding/xml"
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	"github.com/didip/tollbooth/v6"
	"github.com/didip/tollbooth_chi"
	"github.com/go-chi/chi/v5"
	"github.com/go-chi/chi/v5/middleware"
	"github.com/go-chi/render"
	"github.com/go-pkgz/lcw"
	log "github.com/go-pkgz/lgr"
	"github.com/go-pkgz/rest"
	"github.com/go-pkgz/rest/logger"

	"github.com/umputun/feed-master/app/config"
	"github.com/umputun/feed-master/app/feed"
	"github.com/umputun/feed-master/app/proc"
	"github.com/umputun/feed-master/app/youtube"
)

// Server provides HTTP API
type Server struct {
	Version    string
	Conf       config.Conf
	Store      *proc.BoltDB
	YoutubeSvc YoutubeSvc
	httpServer *http.Server
	cache      lcw.LoadingCache
}

// YoutubeSvc provides access to youtube's audio rss
type YoutubeSvc interface {
	RSSFeed(cinfo youtube.FeedInfo) (string, error)
}

// Run starts http server for API with all routes
func (s *Server) Run(ctx context.Context, port int) {
	var err error
	if s.cache, err = lcw.NewExpirableCache(lcw.TTL(time.Minute*5), lcw.MaxCacheSize(10*1024*1024)); err != nil {
		log.Printf("[PANIC] failed to make loading cache, %v", err)
		return
	}

	go func() {
		<-ctx.Done()
		if s.httpServer != nil {
			if clsErr := s.httpServer.Close(); clsErr != nil {
				log.Printf("[ERROR] failed to close proxy http server, %v", clsErr)
			}
		}
	}()

	s.httpServer = &http.Server{
		Addr:              fmt.Sprintf(":%d", port),
		Handler:           s.router(),
		ReadHeaderTimeout: 5 * time.Second,
		WriteTimeout:      30 * time.Second,
		IdleTimeout:       30 * time.Second,
	}
	err = s.httpServer.ListenAndServe()
	log.Printf("[WARN] http server terminated, %s", err)
}

func (s *Server) router() *chi.Mux {
	router := chi.NewRouter()
	router.Use(middleware.RealIP, rest.Recoverer(log.Default()))
	router.Use(middleware.Throttle(1000), middleware.Timeout(60*time.Second))
	router.Use(rest.AppInfo("feed-master", "umputun", s.Version), rest.Ping)
	router.Use(tollbooth_chi.LimitHandler(tollbooth.NewLimiter(5, nil)))

	router.Group(func(rimg chi.Router) {
		l := logger.New(logger.Log(log.Default()), logger.Prefix("[DEBUG]"))
		rimg.Use(l.Handler)
		rimg.Get("/images/{name}", s.getImageCtrl)
		rimg.Get("/image/{name}", s.getImageCtrl)
		rimg.Head("/image/{name}", s.getImageHeadCtrl)
		rimg.Head("/images/{name}", s.getImageHeadCtrl)
	})

	router.Group(func(rrss chi.Router) {
		l := logger.New(logger.Log(log.Default()), logger.Prefix("[INFO]"))
		rrss.Use(l.Handler)
		rrss.Get("/rss/{name}", s.getFeedCtrl)
		rrss.Get("/list", s.getListCtrl)
		rrss.Get("/feed/{name}", s.getFeedPageCtrl)
		rrss.Get("/feed/{name}/sources", s.getSourcesPageCtrl)
		rrss.Get("/feeds", s.getFeedsPageCtrl)
	})

	router.Route("/yt", func(r chi.Router) {
		l := logger.New(logger.Log(log.Default()), logger.Prefix("[INFO]"))
		r.Use(l.Handler)
		r.Get("/rss/{channel}", s.getYoutubeFeedCtrl)
	})

	if s.Conf.YouTube.BaseURL != "" {
		baseYtURL, parseErr := url.Parse(s.Conf.YouTube.BaseURL)
		if parseErr != nil {
			log.Printf("[ERROR] failed to parse base url %s, %v", s.Conf.YouTube.BaseURL, parseErr)
		}
		ytfs, fsErr := rest.NewFileServer(baseYtURL.Path, s.Conf.YouTube.FilesLocation)
		if fsErr == nil {
			router.Mount(baseYtURL.Path, ytfs)
		} else {
			log.Printf("[WARN] can't start static file server for yt, %v", fsErr)
		}
	}

	fs, err := rest.NewFileServer("/static", filepath.Join("webapp", "static"))
	if err == nil {
		router.Mount("/static", fs)
	} else {
		log.Printf("[WARN] can't start static file server, %v", err)
	}
	return router
}

// GET /rss/{name} - returns rss for given feeds set
func (s *Server) getFeedCtrl(w http.ResponseWriter, r *http.Request) {
	feedName := chi.URLParam(r, "name")
	items, err := s.Store.Load(feedName, s.Conf.System.MaxTotal, true)
	if err != nil {
		rest.SendErrorJSON(w, r, log.Default(), http.StatusBadRequest, err, "failed to get feed")
		return
	}

	for i, itm := range items {
		// add ts suffix to titles
		switch s.Conf.Feeds[feedName].ExtendDateTitle {
		case "yyyyddmm":
			items[i].Title = fmt.Sprintf("%s (%s)", itm.Title, itm.DT.Format("2006-02-01"))
		case "yyyymmdd":
			items[i].Title = fmt.Sprintf("%s (%s)", itm.Title, itm.DT.Format("2006-01-02"))
		}
	}

	rss := feed.Rss2{
		Version:       "2.0",
		NsItunes:      "http://www.itunes.com/dtds/podcast-1.0.dtd",
		NsMedia:       "http://search.yahoo.com/mrss/",
		ItemList:      items,
		Title:         s.Conf.Feeds[feedName].Title,
		Description:   s.Conf.Feeds[feedName].Description,
		Language:      s.Conf.Feeds[feedName].Language,
		Link:          s.Conf.Feeds[feedName].Link,
		PubDate:       items[0].PubDate,
		LastBuildDate: time.Now().Format(time.RFC822Z),
		NsItunes:      "http://www.itunes.com/dtds/podcast-1.0.dtd",
	}

	// replace link to UI page
	if s.Conf.System.BaseURL != "" {
		baseURL := s.Conf.System.BaseURL
		rss.Link = baseURL + "/feed/" + feedName
		if s.Conf.Feeds[feedName].Image != "" {
			rss.ItunesImage = feed.ItunesImg{
				URL: baseURL + "/image/" + feedName,
			}
			rss.MediaThumbnail = feed.MediaThumbnail{
				URL: baseURL + "/image/" + feedName,
			}
		}
	}

	b, err := xml.MarshalIndent(&rss, "", "  ")
	if err != nil {
		rest.SendErrorJSON(w, r, log.Default(), http.StatusInternalServerError, err, "failed to marshal rss")
		return
	}

	w.Header().Set("Content-Type", "application/xml; charset=UTF-8")
	res := `<?xml version="1.0" encoding="UTF-8"?>` + "\n" + string(b)
	// this hack to avoid having different items for marshal and unmarshal due to "itunes" namespace
	res = strings.Replace(res, "<duration>", "<itunes:duration>", -1)
	res = strings.Replace(res, "</duration>", "</itunes:duration>", -1)

	_, _ = fmt.Fprintf(w, "%s", res)
}

// GET /image/{name}
func (s *Server) getImageCtrl(w http.ResponseWriter, r *http.Request) {
	fm := chi.URLParam(r, "name")
	fm = strings.TrimSuffix(fm, ".png")
	feedConf, found := s.Conf.Feeds[fm]
	if !found {
		rest.SendErrorJSON(w, r, log.Default(), http.StatusBadRequest,
			fmt.Errorf("image %s not found", fm), "failed to load image")
		return
	}

	b, err := os.ReadFile(feedConf.Image)
	if err != nil {
		rest.SendErrorJSON(w, r, log.Default(), http.StatusBadRequest,
			errors.New("can't read  "+chi.URLParam(r, "name")), "failed to read image")
		return
	}
	w.Header().Set("Content-Type", "image/png")
	if _, err := w.Write(b); err != nil {
		log.Printf("[WARN] failed to send image, %s", err)
	}
}

// HEAD /image/{name}
func (s *Server) getImageHeadCtrl(w http.ResponseWriter, r *http.Request) {
	fm := chi.URLParam(r, "name")
	fm = strings.TrimSuffix(fm, ".png")
	feedConf, found := s.Conf.Feeds[fm]
	if !found {
		w.WriteHeader(http.StatusNotFound)
		return
	}

	info, err := os.Stat(feedConf.Image)
	if err != nil {
		w.WriteHeader(http.StatusNotFound)
		return
	}
	w.Header().Set("Content-Type", "image/png")
	w.Header().Set("Content-Length", strconv.Itoa(int(info.Size())))
	w.WriteHeader(http.StatusOK)
}

// GET /list - returns feed's image
func (s *Server) getListCtrl(w http.ResponseWriter, r *http.Request) {
	feeds := s.feeds()
	render.JSON(w, r, feeds)
}

// GET /yt/rss/{channel} - returns rss for given youtube channel
func (s *Server) getYoutubeFeedCtrl(w http.ResponseWriter, r *http.Request) {
	channel := chi.URLParam(r, "channel")

	fi := youtube.FeedInfo{ID: channel}
	for _, f := range s.Conf.YouTube.Channels {
		if f.ID == channel {
			fi = f
			break
		}
	}

	res, err := s.YoutubeSvc.RSSFeed(fi)
	if err != nil {
		rest.SendErrorJSON(w, r, log.Default(), http.StatusInternalServerError, err, "failed to read yt list")
		return
	}

	w.Header().Set("Content-Type", "application/xml; charset=UTF-8")
	res = `<?xml version="1.0" encoding="UTF-8"?>` + "\n" + res
	_, _ = fmt.Fprintf(w, "%s", res)
}

func (s *Server) feeds() []string {
	feeds := make([]string, 0, len(s.Conf.Feeds))
	for k := range s.Conf.Feeds {
		feeds = append(feeds, k)
	}
	return feeds
}
<|endoftext|>