<commit_before>package drivers

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

	"github.com/pkg/errors"

	"github.com/lxc/lxd/lxd/migration"
	"github.com/lxc/lxd/lxd/operations"
	"github.com/lxc/lxd/shared"
	"github.com/lxc/lxd/shared/api"
	"github.com/lxc/lxd/shared/validate"
)

var cephfsVersion string
var cephfsLoaded bool

type cephfs struct {
	common
}

// load is used to run one-time action per-driver rather than per-pool.
func (d *cephfs) load() error {
	// Register the patches.
	d.patches = map[string]func() error{
		"storage_create_vm":                        nil,
		"storage_zfs_mount":                        nil,
		"storage_create_vm_again":                  nil,
		"storage_zfs_volmode":                      nil,
		"storage_rename_custom_volume_add_project": nil,
		"storage_lvm_skipactivation":               nil,
	}

	// Done if previously loaded.
	if cephfsLoaded {
		return nil
	}

	// Validate the required binaries.
	for _, tool := range []string{"ceph", "rbd"} {
		_, err := exec.LookPath(tool)
		if err != nil {
			return fmt.Errorf("Required tool '%s' is missing", tool)
		}
	}

	// Detect and record the version.
	if cephfsVersion == "" {
		out, err := shared.RunCommand("rbd", "--version")
		if err != nil {
			return err
		}

		cephfsVersion = strings.TrimSpace(out)
	}

	cephfsLoaded = true
	return nil
}

// isRemote returns true indicating this driver uses remote storage.
func (d *cephfs) isRemote() bool {
	return true
}

// Info returns the pool driver information.
func (d *cephfs) Info() Info {
	return Info{
		Name:               "cephfs",
		Version:            cephfsVersion,
		OptimizedImages:    false,
		PreservesInodes:    false,
		Remote:             d.isRemote(),
		VolumeTypes:        []VolumeType{VolumeTypeCustom},
		VolumeMultiNode:    true,
		BlockBacking:       false,
		RunningQuotaResize: true,
		RunningCopyFreeze:  false,
		DirectIO:           true,
		MountedRoot:        true,
	}
}

// Create is called during pool creation and is effectively using an empty driver struct.
// WARNING: The Create() function cannot rely on any of the struct attributes being set.
func (d *cephfs) Create() error {
	// Config validation.
	if d.config["source"] == "" {
		return fmt.Errorf("Missing required source name/path")
	}

	if d.config["cephfs.path"] != "" && d.config["cephfs.path"] != d.config["source"] {
		return fmt.Errorf("cephfs.path must match the source")
	}

	// Set default properties if missing.
	if d.config["cephfs.cluster_name"] == "" {
		d.config["cephfs.cluster_name"] = "ceph"
	}

	if d.config["cephfs.user.name"] == "" {
		d.config["cephfs.user.name"] = "admin"
	}

	d.config["cephfs.path"] = d.config["source"]

	// Parse the namespace / path.
	fields := strings.SplitN(d.config["cephfs.path"], "/", 2)
	fsName := fields[0]
	fsPath := "/"
	if len(fields) > 1 {
		fsPath = fields[1]
	}

	// Check that the filesystem exists.
	if !d.fsExists(d.config["cephfs.cluster_name"], d.config["cephfs.user.name"], fsName) {
		return fmt.Errorf("The requested '%v' CEPHFS doesn't exist", fsName)
	}

	// Create a temporary mountpoint.
	mountPath, err := ioutil.TempDir("", "lxd_cephfs_")
	if err != nil {
		return errors.Wrap(err, "Failed to create temporary directory under")
	}
	defer os.RemoveAll(mountPath)

	err = os.Chmod(mountPath, 0700)
	if err != nil {
		return errors.Wrapf(err, "Failed to chmod '%s'", mountPath)
	}

	mountPoint := filepath.Join(mountPath, "mount")

	err = os.Mkdir(mountPoint, 0700)
	if err != nil {
		return errors.Wrapf(err, "Failed to create directory '%s'", mountPoint)
	}

	// Get the credentials and host.
	monAddresses, userSecret, err := d.getConfig(d.config["cephfs.cluster_name"], d.config["cephfs.user.name"])
	if err != nil {
		return err
	}

	// Mount the pool.
	uri := strings.Join(monAddresses, ",")
	if !strings.Contains(uri, ":6789") {
		uri = fmt.Sprintf("%s:6789", uri)
	}
	uri = fmt.Sprintf("%s:/", uri)

	err = TryMount(uri, mountPoint, "ceph", 0, fmt.Sprintf("name=%v,secret=%v,mds_namespace=%v", d.config["cephfs.user.name"], userSecret, fsName))
	if err != nil {
		return err
	}
	defer forceUnmount(mountPoint)

	// Create the path if missing.
	err = os.MkdirAll(filepath.Join(mountPoint, fsPath), 0755)
	if err != nil {
		return errors.Wrapf(err, "Failed to create directory '%s'", filepath.Join(mountPoint, fsPath))
	}

	// Check that the existing path is empty.
	ok, _ := shared.PathIsEmpty(filepath.Join(mountPoint, fsPath))
	if !ok {
		return fmt.Errorf("Only empty CEPHFS paths can be used as a LXD storage pool")
	}

	return nil
}

// Delete clears any local and remote data related to this driver instance.
func (d *cephfs) Delete(op *operations.Operation) error {
	// Parse the namespace / path.
	fields := strings.SplitN(d.config["cephfs.path"], "/", 2)
	fsName := fields[0]
	fsPath := "/"
	if len(fields) > 1 {
		fsPath = fields[1]
	}

	// Create a temporary mountpoint.
	mountPath, err := ioutil.TempDir("", "lxd_cephfs_")
	if err != nil {
		return errors.Wrap(err, "Failed to create temporary directory under")
	}
	defer os.RemoveAll(mountPath)

	err = os.Chmod(mountPath, 0700)
	if err != nil {
		return errors.Wrapf(err, "Failed to chmod '%s'", mountPath)
	}

	mountPoint := filepath.Join(mountPath, "mount")
	err = os.Mkdir(mountPoint, 0700)
	if err != nil {
		return errors.Wrapf(err, "Failed to create directory '%s'", mountPoint)
	}

	// Get the credentials and host.
	monAddresses, userSecret, err := d.getConfig(d.config["cephfs.cluster_name"], d.config["cephfs.user.name"])
	if err != nil {
		return err
	}

	// Mount the pool.
	uri := strings.Join(monAddresses, ",")
	if !strings.Contains(uri, ":6789") {
		uri = fmt.Sprintf("%s:6789", uri)
	}
	uri = fmt.Sprintf("%s:/", uri)

	err = TryMount(uri, mountPoint, "ceph", 0, fmt.Sprintf("name=%v,secret=%v,mds_namespace=%v", d.config["cephfs.user.name"], userSecret, fsName))
	if err != nil {
		return err
	}
	defer forceUnmount(mountPoint)

	// On delete, wipe everything in the directory.
	err = wipeDirectory(GetPoolMountPath(d.name))
	if err != nil {
		return err
	}

	// Delete the pool from the parent.
	if shared.PathExists(filepath.Join(mountPoint, fsPath)) {
		// Delete the path itself.
		if fsPath != "" && fsPath != "/" {
			err = os.Remove(filepath.Join(mountPoint, fsPath))
			if err != nil && !os.IsNotExist(err) {
				return errors.Wrapf(err, "Failed to remove directory '%s'", filepath.Join(mountPoint, fsPath))
			}
		}
	}

	// Make sure the existing pool is unmounted.
	_, err = d.Unmount()
	if err != nil {
		return err
	}

	return nil
}

// Validate checks that all provide keys are supported and that no conflicting or missing configuration is present.
func (d *cephfs) Validate(config map[string]string) error {
	rules := map[string]func(value string) error{
		"cephfs.cluster_name":    validate.IsAny,
		"cephfs.path":            validate.IsAny,
		"cephfs.user.name":       validate.IsAny,
		"volatile.pool.pristine": validate.IsAny,
	}

	return d.validatePool(config, rules)
}

// Update applies any driver changes required from a configuration change.
func (d *cephfs) Update(changedConfig map[string]string) error {
	return nil
}

// Mount brings up the driver and sets it up to be used.
func (d *cephfs) Mount() (bool, error) {
	// Check if already mounted.
	if shared.IsMountPoint(GetPoolMountPath(d.name)) {
		return false, nil
	}

	// Parse the namespace / path.
	fields := strings.SplitN(d.config["cephfs.path"], "/", 2)
	fsName := fields[0]
	fsPath := ""
	if len(fields) > 1 {
		fsPath = fields[1]
	}

	// Get the credentials and host.
	monAddresses, userSecret, err := d.getConfig(d.config["cephfs.cluster_name"], d.config["cephfs.user.name"])
	if err != nil {
		return false, err
	}

	// Mount the pool.
	uri := strings.Join(monAddresses, ",")
	if !strings.Contains(uri, ":6789") {
		uri = fmt.Sprintf("%s:6789", uri)
	}
	uri = fmt.Sprintf("%s:/%s", uri, fsPath)

	err = TryMount(uri, GetPoolMountPath(d.name), "ceph", 0, fmt.Sprintf("name=%v,secret=%v,mds_namespace=%v", d.config["cephfs.user.name"], userSecret, fsName))
	if err != nil {
		return false, err
	}

	return true, nil
}

// Unmount clears any of the runtime state of the driver.
func (d *cephfs) Unmount() (bool, error) {
	return forceUnmount(GetPoolMountPath(d.name))
}

// GetResources returns the pool resource usage information.
func (d *cephfs) GetResources() (*api.ResourcesStoragePool, error) {
	return genericVFSGetResources(d)
}

// MigrationTypes returns the supported migration types and options supported by the driver.
func (d *cephfs) MigrationTypes(contentType ContentType, refresh bool) []migration.Type {
	var rsyncFeatures []string

	// Do not pass compression argument to rsync if the associated
	// config key, that is rsync.compression, is set to false.
	if d.Config()["rsync.compression"] != "" && !shared.IsTrue(d.Config()["rsync.compression"]) {
		rsyncFeatures = []string{"delete", "bidirectional"}
	} else {
		rsyncFeatures = []string{"delete", "compress", "bidirectional"}
	}

	if contentType != ContentTypeFS {
		return nil
	}

	// Do not support xattr transfer on cephfs
	return []migration.Type{
		{
			FSType:   migration.MigrationFSType_RSYNC,
			Features: rsyncFeatures,
		},
	}
}
<commit_msg>lxd/storage/drivers/driver/cephfs: Removes RunningQuotaResize<commit_after>package drivers

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

	"github.com/pkg/errors"

	"github.com/lxc/lxd/lxd/migration"
	"github.com/lxc/lxd/lxd/operations"
	"github.com/lxc/lxd/shared"
	"github.com/lxc/lxd/shared/api"
	"github.com/lxc/lxd/shared/validate"
)

var cephfsVersion string
var cephfsLoaded bool

type cephfs struct {
	common
}

// load is used to run one-time action per-driver rather than per-pool.
func (d *cephfs) load() error {
	// Register the patches.
	d.patches = map[string]func() error{
		"storage_create_vm":                        nil,
		"storage_zfs_mount":                        nil,
		"storage_create_vm_again":                  nil,
		"storage_zfs_volmode":                      nil,
		"storage_rename_custom_volume_add_project": nil,
		"storage_lvm_skipactivation":               nil,
	}

	// Done if previously loaded.
	if cephfsLoaded {
		return nil
	}

	// Validate the required binaries.
	for _, tool := range []string{"ceph", "rbd"} {
		_, err := exec.LookPath(tool)
		if err != nil {
			return fmt.Errorf("Required tool '%s' is missing", tool)
		}
	}

	// Detect and record the version.
	if cephfsVersion == "" {
		out, err := shared.RunCommand("rbd", "--version")
		if err != nil {
			return err
		}

		cephfsVersion = strings.TrimSpace(out)
	}

	cephfsLoaded = true
	return nil
}

// isRemote returns true indicating this driver uses remote storage.
func (d *cephfs) isRemote() bool {
	return true
}

// Info returns the pool driver information.
func (d *cephfs) Info() Info {
	return Info{
		Name:              "cephfs",
		Version:           cephfsVersion,
		OptimizedImages:   false,
		PreservesInodes:   false,
		Remote:            d.isRemote(),
		VolumeTypes:       []VolumeType{VolumeTypeCustom},
		VolumeMultiNode:   true,
		BlockBacking:      false,
		RunningCopyFreeze: false,
		DirectIO:          true,
		MountedRoot:       true,
	}
}

// Create is called during pool creation and is effectively using an empty driver struct.
// WARNING: The Create() function cannot rely on any of the struct attributes being set.
func (d *cephfs) Create() error {
	// Config validation.
	if d.config["source"] == "" {
		return fmt.Errorf("Missing required source name/path")
	}

	if d.config["cephfs.path"] != "" && d.config["cephfs.path"] != d.config["source"] {
		return fmt.Errorf("cephfs.path must match the source")
	}

	// Set default properties if missing.
	if d.config["cephfs.cluster_name"] == "" {
		d.config["cephfs.cluster_name"] = "ceph"
	}

	if d.config["cephfs.user.name"] == "" {
		d.config["cephfs.user.name"] = "admin"
	}

	d.config["cephfs.path"] = d.config["source"]

	// Parse the namespace / path.
	fields := strings.SplitN(d.config["cephfs.path"], "/", 2)
	fsName := fields[0]
	fsPath := "/"
	if len(fields) > 1 {
		fsPath = fields[1]
	}

	// Check that the filesystem exists.
	if !d.fsExists(d.config["cephfs.cluster_name"], d.config["cephfs.user.name"], fsName) {
		return fmt.Errorf("The requested '%v' CEPHFS doesn't exist", fsName)
	}

	// Create a temporary mountpoint.
	mountPath, err := ioutil.TempDir("", "lxd_cephfs_")
	if err != nil {
		return errors.Wrap(err, "Failed to create temporary directory under")
	}
	defer os.RemoveAll(mountPath)

	err = os.Chmod(mountPath, 0700)
	if err != nil {
		return errors.Wrapf(err, "Failed to chmod '%s'", mountPath)
	}

	mountPoint := filepath.Join(mountPath, "mount")

	err = os.Mkdir(mountPoint, 0700)
	if err != nil {
		return errors.Wrapf(err, "Failed to create directory '%s'", mountPoint)
	}

	// Get the credentials and host.
	monAddresses, userSecret, err := d.getConfig(d.config["cephfs.cluster_name"], d.config["cephfs.user.name"])
	if err != nil {
		return err
	}

	// Mount the pool.
	uri := strings.Join(monAddresses, ",")
	if !strings.Contains(uri, ":6789") {
		uri = fmt.Sprintf("%s:6789", uri)
	}
	uri = fmt.Sprintf("%s:/", uri)

	err = TryMount(uri, mountPoint, "ceph", 0, fmt.Sprintf("name=%v,secret=%v,mds_namespace=%v", d.config["cephfs.user.name"], userSecret, fsName))
	if err != nil {
		return err
	}
	defer forceUnmount(mountPoint)

	// Create the path if missing.
	err = os.MkdirAll(filepath.Join(mountPoint, fsPath), 0755)
	if err != nil {
		return errors.Wrapf(err, "Failed to create directory '%s'", filepath.Join(mountPoint, fsPath))
	}

	// Check that the existing path is empty.
	ok, _ := shared.PathIsEmpty(filepath.Join(mountPoint, fsPath))
	if !ok {
		return fmt.Errorf("Only empty CEPHFS paths can be used as a LXD storage pool")
	}

	return nil
}

// Delete clears any local and remote data related to this driver instance.
func (d *cephfs) Delete(op *operations.Operation) error {
	// Parse the namespace / path.
	fields := strings.SplitN(d.config["cephfs.path"], "/", 2)
	fsName := fields[0]
	fsPath := "/"
	if len(fields) > 1 {
		fsPath = fields[1]
	}

	// Create a temporary mountpoint.
	mountPath, err := ioutil.TempDir("", "lxd_cephfs_")
	if err != nil {
		return errors.Wrap(err, "Failed to create temporary directory under")
	}
	defer os.RemoveAll(mountPath)

	err = os.Chmod(mountPath, 0700)
	if err != nil {
		return errors.Wrapf(err, "Failed to chmod '%s'", mountPath)
	}

	mountPoint := filepath.Join(mountPath, "mount")
	err = os.Mkdir(mountPoint, 0700)
	if err != nil {
		return errors.Wrapf(err, "Failed to create directory '%s'", mountPoint)
	}

	// Get the credentials and host.
	monAddresses, userSecret, err := d.getConfig(d.config["cephfs.cluster_name"], d.config["cephfs.user.name"])
	if err != nil {
		return err
	}

	// Mount the pool.
	uri := strings.Join(monAddresses, ",")
	if !strings.Contains(uri, ":6789") {
		uri = fmt.Sprintf("%s:6789", uri)
	}
	uri = fmt.Sprintf("%s:/", uri)

	err = TryMount(uri, mountPoint, "ceph", 0, fmt.Sprintf("name=%v,secret=%v,mds_namespace=%v", d.config["cephfs.user.name"], userSecret, fsName))
	if err != nil {
		return err
	}
	defer forceUnmount(mountPoint)

	// On delete, wipe everything in the directory.
	err = wipeDirectory(GetPoolMountPath(d.name))
	if err != nil {
		return err
	}

	// Delete the pool from the parent.
	if shared.PathExists(filepath.Join(mountPoint, fsPath)) {
		// Delete the path itself.
		if fsPath != "" && fsPath != "/" {
			err = os.Remove(filepath.Join(mountPoint, fsPath))
			if err != nil && !os.IsNotExist(err) {
				return errors.Wrapf(err, "Failed to remove directory '%s'", filepath.Join(mountPoint, fsPath))
			}
		}
	}

	// Make sure the existing pool is unmounted.
	_, err = d.Unmount()
	if err != nil {
		return err
	}

	return nil
}

// Validate checks that all provide keys are supported and that no conflicting or missing configuration is present.
func (d *cephfs) Validate(config map[string]string) error {
	rules := map[string]func(value string) error{
		"cephfs.cluster_name":    validate.IsAny,
		"cephfs.path":            validate.IsAny,
		"cephfs.user.name":       validate.IsAny,
		"volatile.pool.pristine": validate.IsAny,
	}

	return d.validatePool(config, rules)
}

// Update applies any driver changes required from a configuration change.
func (d *cephfs) Update(changedConfig map[string]string) error {
	return nil
}

// Mount brings up the driver and sets it up to be used.
func (d *cephfs) Mount() (bool, error) {
	// Check if already mounted.
	if shared.IsMountPoint(GetPoolMountPath(d.name)) {
		return false, nil
	}

	// Parse the namespace / path.
	fields := strings.SplitN(d.config["cephfs.path"], "/", 2)
	fsName := fields[0]
	fsPath := ""
	if len(fields) > 1 {
		fsPath = fields[1]
	}

	// Get the credentials and host.
	monAddresses, userSecret, err := d.getConfig(d.config["cephfs.cluster_name"], d.config["cephfs.user.name"])
	if err != nil {
		return false, err
	}

	// Mount the pool.
	uri := strings.Join(monAddresses, ",")
	if !strings.Contains(uri, ":6789") {
		uri = fmt.Sprintf("%s:6789", uri)
	}
	uri = fmt.Sprintf("%s:/%s", uri, fsPath)

	err = TryMount(uri, GetPoolMountPath(d.name), "ceph", 0, fmt.Sprintf("name=%v,secret=%v,mds_namespace=%v", d.config["cephfs.user.name"], userSecret, fsName))
	if err != nil {
		return false, err
	}

	return true, nil
}

// Unmount clears any of the runtime state of the driver.
func (d *cephfs) Unmount() (bool, error) {
	return forceUnmount(GetPoolMountPath(d.name))
}

// GetResources returns the pool resource usage information.
func (d *cephfs) GetResources() (*api.ResourcesStoragePool, error) {
	return genericVFSGetResources(d)
}

// MigrationTypes returns the supported migration types and options supported by the driver.
func (d *cephfs) MigrationTypes(contentType ContentType, refresh bool) []migration.Type {
	var rsyncFeatures []string

	// Do not pass compression argument to rsync if the associated
	// config key, that is rsync.compression, is set to false.
	if d.Config()["rsync.compression"] != "" && !shared.IsTrue(d.Config()["rsync.compression"]) {
		rsyncFeatures = []string{"delete", "bidirectional"}
	} else {
		rsyncFeatures = []string{"delete", "compress", "bidirectional"}
	}

	if contentType != ContentTypeFS {
		return nil
	}

	// Do not support xattr transfer on cephfs
	return []migration.Type{
		{
			FSType:   migration.MigrationFSType_RSYNC,
			Features: rsyncFeatures,
		},
	}
}
<|endoftext|>


<commit_before>package looli

import (
	"github.com/cssivision/router"
	"html/template"
	"net/http"
	"path"
	"strings"
)

var (
	defaultStatusCode = http.StatusOK
	default404Body    = "404 page not found"
	default405Body    = "405 method not allowed"
)

// RouterPrefix is used internally to configure router, a RouterPrefix is associated with a basePath
// and an array of handlers (middleware)
type RouterPrefix struct {
	basePath    string
	router      *router.Router
	Handlers    []HandlerFunc
	template    *template.Template
	engine      *Engine
	allNoRoute  []HandlerFunc
	allNoMethod []HandlerFunc
}

// Use adds middleware to the router.
func (p *RouterPrefix) Use(middleware ...HandlerFunc) {
	if len(middleware) == 0 {
		panic("there must be at least one middleware")
	}
	p.Handlers = append(p.Handlers, middleware...)
	p.rebuild404Handlers()
	p.rebuild405Handlers()
}

// Use adds handlers as middleware to the router.
func (p *RouterPrefix) UseHandler(handlers ...Handler) {
	var middlwares []HandlerFunc
	for _, handler := range handlers {
		middlwares = append(middlwares, handler.Handle)
	}
}

// Get is a shortcut for router.Handle("GET", path, handle)
func (p *RouterPrefix) Get(pattern string, handlers ...HandlerFunc) {
	p.Handle(http.MethodGet, pattern, handlers...)
}

// Post is a shortcut for router.Handle("Post", path, handle)
func (p *RouterPrefix) Post(pattern string, handlers ...HandlerFunc) {
	p.Handle(http.MethodPost, pattern, handlers...)
}

// Put is a shortcut for router.Handle("Put", path, handle)
func (p *RouterPrefix) Put(pattern string, handlers ...HandlerFunc) {
	p.Handle(http.MethodPut, pattern, handlers...)
}

// Delete is a shortcut for router.Handle("DELETE", path, handle)
func (p *RouterPrefix) Delete(pattern string, handlers ...HandlerFunc) {
	p.Handle(http.MethodDelete, pattern, handlers...)
}

// Head is a shortcut for router.Handle("HEAD", path, handle)
func (p *RouterPrefix) Head(pattern string, handlers ...HandlerFunc) {
	p.Handle(http.MethodHead, pattern, handlers...)
}

// Options is a shortcut for router.Handle("OPTIONS", path, handle)
func (p *RouterPrefix) Options(pattern string, handlers ...HandlerFunc) {
	p.Handle(http.MethodOptions, pattern, handlers...)
}

// Patch is a shortcut for router.Handle("PATCH", path, handle)
func (p *RouterPrefix) Patch(pattern string, handlers ...HandlerFunc) {
	p.Handle(http.MethodPatch, pattern, handlers...)
}

// Any registers a route that matches all the HTTP methods.
// GET, POST, PUT, PATCH, HEAD, OPTIONS, DELETE, CONNECT, TRACE
func (p *RouterPrefix) Any(pattern string, handlers ...HandlerFunc) {
	p.Handle(http.MethodGet, pattern, handlers...)
	p.Handle(http.MethodPost, pattern, handlers...)
	p.Handle(http.MethodPut, pattern, handlers...)
	p.Handle(http.MethodDelete, pattern, handlers...)
	p.Handle(http.MethodHead, pattern, handlers...)
	p.Handle(http.MethodOptions, pattern, handlers...)
	p.Handle(http.MethodPatch, pattern, handlers...)
	p.Handle(http.MethodTrace, pattern, handlers...)
	p.Handle(http.MethodConnect, pattern, handlers...)
}

// Handle registers a new request handle and middleware with the given path and method.
func (p *RouterPrefix) Handle(method, pattern string, handlers ...HandlerFunc) {
	if len(handlers) == 0 {
		panic("there must be at least one handler")
	}

	if p.basePath != "" {
		pattern = p.basePath + pattern
	}

	handlers = p.combineHandlers(handlers)
	muxHandler := p.composeMiddleware(handlers)
	p.router.Handle(method, pattern, muxHandler)
}

func (p *RouterPrefix) LoadHTMLGlob(pattern string) {
	templ := template.Must(template.ParseGlob(pattern))
	p.SetHTMLTemplate(templ)
}

func (p *RouterPrefix) LoadHTMLFiles(files ...string) {
	templ := template.Must(template.ParseFiles(files...))
	p.SetHTMLTemplate(templ)
}

func (p *RouterPrefix) SetHTMLTemplate(templ *template.Template) {
	p.template = templ
}

// StaticFile register router pattern and response file in path
func (p *RouterPrefix) StaticFile(pattern, filepath string) {
	if strings.Contains(pattern, ":") || strings.Contains(pattern, "*") {
		panic("URL parameters can not be used when serving a static folder")
	}
	handler := func(c *Context) {
		c.ServeFile(filepath)
	}

	p.Head(pattern, handler)
	p.Get(pattern, handler)
}

// Static register router pattern and response file in the request url
func (p *RouterPrefix) Static(pattern, dir string) {
	if strings.Contains(pattern, ":") || strings.Contains(pattern, "*") {
		panic("URL parameters can not be used when serving a static folder")
	}

	fileServer := http.StripPrefix(pattern, http.FileServer(http.Dir(dir)))
	handler := func(c *Context) {
		fileServer.ServeHTTP(c.ResponseWriter, c.Request)
	}

	urlPattern := path.Join(pattern, "/*filepath")
	p.Head(urlPattern, handler)
	p.Get(urlPattern, handler)
}

// combine middleware and handlers for specific route
func (p *RouterPrefix) combineHandlers(handlers []HandlerFunc) []HandlerFunc {
	finalSize := len(p.Handlers) + len(handlers)
	if finalSize >= int(abortIndex) {
		panic("too many handlers")
	}
	mergedHandlers := make([]HandlerFunc, finalSize)
	copyHandlers(mergedHandlers, p.Handlers)
	copyHandlers(mergedHandlers[len(p.Handlers):], handlers)
	return mergedHandlers
}

// Prefix creates a new router prefix. You should add all the routes that have common
// middlwares or the same path prefix. For example, all the routes that use a common
// middlware could be grouped.
func (p *RouterPrefix) Prefix(basePath string) *RouterPrefix {
	return &RouterPrefix{
		basePath: basePath,
		router:   p.router,
		Handlers: p.Handlers,
	}
}

func copyHandlers(dst, src []HandlerFunc) {
	for index, val := range src {
		dst[index] = val
	}
}

// Construct handler for specific router
func (p *RouterPrefix) composeMiddleware(handlers []HandlerFunc) router.Handle {
	return func(rw http.ResponseWriter, req *http.Request, ps router.Params) {
		context := &Context{
			ResponseWriter: rw,
			Request:        req,
			handlers:       handlers,
			current:        -1,
			Params:         ps,
			Path:           req.URL.Path,
			template:       p.template,
			engine:         p.engine,
			statusCode:     defaultStatusCode,
		}

		context.Next()
	}
}

func (p *RouterPrefix) rebuild404Handlers() {
	p.allNoRoute = p.combineHandlers(nil)
}

func (p *RouterPrefix) rebuild405Handlers() {
	p.allNoMethod = p.combineHandlers(nil)
}

func (p *RouterPrefix) noRoute(rw http.ResponseWriter, req *http.Request) {
	context := &Context{
		ResponseWriter: rw,
		Request:        req,
		handlers:       p.allNoRoute,
		current:        -1,
		Path:           req.URL.String(),
		template:       p.template,
		engine:         p.engine,
		statusCode:     defaultStatusCode,
	}

	context.Status(http.StatusNotFound)
	context.String(default404Body)
	context.Next()
}

func (p *RouterPrefix) noMethod(rw http.ResponseWriter, req *http.Request) {
	context := &Context{
		ResponseWriter: rw,
		Request:        req,
		handlers:       p.allNoMethod,
		current:        -1,
		Path:           req.URL.Path,
		template:       p.template,
		engine:         p.engine,
		statusCode:     defaultStatusCode,
	}

	context.Status(http.StatusMethodNotAllowed)
	context.String(default405Body)
	context.Next()
}

// NoRoute which is called when no matching route is found. If it is not set, http.NotFound is used.
func (p *RouterPrefix) NoRoute(handlers ...HandlerFunc) {
	if len(handlers) == 0 {
		panic("there must be at least one handler")
	}

	handlers = p.combineHandlers(handlers)
	handler := p.composeMiddleware(handlers)
	p.router.NoRoute = http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
		handler(rw, req, router.Params{})
	})
}

// NoMethod which is called when method is not registered. If it is not set, default is used.
func (p *RouterPrefix) NoMethod(handlers ...HandlerFunc) {
	if len(handlers) == 0 {
		panic("there must be at least one handler")
	}

	handlers = p.combineHandlers(handlers)
	handler := p.composeMiddleware(handlers)
	p.router.NoMethod = http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
		handler(rw, req, router.Params{})
	})
}
<commit_msg>fix: add comment<commit_after>package looli

import (
	"github.com/cssivision/router"
	"html/template"
	"net/http"
	"path"
	"strings"
)

var (
	defaultStatusCode = http.StatusOK
	default404Body    = "404 page not found"
	default405Body    = "405 method not allowed"
)

// RouterPrefix is used internally to configure router, a RouterPrefix is associated with a basePath
// and an array of handlers (middleware)
type RouterPrefix struct {
	basePath    string
	router      *router.Router
	Handlers    []HandlerFunc
	template    *template.Template
	engine      *Engine
	allNoRoute  []HandlerFunc
	allNoMethod []HandlerFunc
}

// Use adds middleware to the router.
func (p *RouterPrefix) Use(middleware ...HandlerFunc) {
	if len(middleware) == 0 {
		panic("there must be at least one middleware")
	}
	p.Handlers = append(p.Handlers, middleware...)
	p.rebuild404Handlers()
	p.rebuild405Handlers()
}

// Use adds handlers as middleware to the router.
func (p *RouterPrefix) UseHandler(handlers ...Handler) {
	var middlwares []HandlerFunc
	for _, handler := range handlers {
		middlwares = append(middlwares, handler.Handle)
	}
}

// Get is a shortcut for router.Handle("GET", path, handle)
func (p *RouterPrefix) Get(pattern string, handlers ...HandlerFunc) {
	p.Handle(http.MethodGet, pattern, handlers...)
}

// Post is a shortcut for router.Handle("Post", path, handle)
func (p *RouterPrefix) Post(pattern string, handlers ...HandlerFunc) {
	p.Handle(http.MethodPost, pattern, handlers...)
}

// Put is a shortcut for router.Handle("Put", path, handle)
func (p *RouterPrefix) Put(pattern string, handlers ...HandlerFunc) {
	p.Handle(http.MethodPut, pattern, handlers...)
}

// Delete is a shortcut for router.Handle("DELETE", path, handle)
func (p *RouterPrefix) Delete(pattern string, handlers ...HandlerFunc) {
	p.Handle(http.MethodDelete, pattern, handlers...)
}

// Head is a shortcut for router.Handle("HEAD", path, handle)
func (p *RouterPrefix) Head(pattern string, handlers ...HandlerFunc) {
	p.Handle(http.MethodHead, pattern, handlers...)
}

// Options is a shortcut for router.Handle("OPTIONS", path, handle)
func (p *RouterPrefix) Options(pattern string, handlers ...HandlerFunc) {
	p.Handle(http.MethodOptions, pattern, handlers...)
}

// Patch is a shortcut for router.Handle("PATCH", path, handle)
func (p *RouterPrefix) Patch(pattern string, handlers ...HandlerFunc) {
	p.Handle(http.MethodPatch, pattern, handlers...)
}

// Any registers a route that matches all the HTTP methods.
// GET, POST, PUT, PATCH, HEAD, OPTIONS, DELETE, CONNECT, TRACE
func (p *RouterPrefix) Any(pattern string, handlers ...HandlerFunc) {
	p.Handle(http.MethodGet, pattern, handlers...)
	p.Handle(http.MethodPost, pattern, handlers...)
	p.Handle(http.MethodPut, pattern, handlers...)
	p.Handle(http.MethodDelete, pattern, handlers...)
	p.Handle(http.MethodHead, pattern, handlers...)
	p.Handle(http.MethodOptions, pattern, handlers...)
	p.Handle(http.MethodPatch, pattern, handlers...)
	p.Handle(http.MethodTrace, pattern, handlers...)
	p.Handle(http.MethodConnect, pattern, handlers...)
}

// Handle registers a new request handle and middleware with the given path and method.
func (p *RouterPrefix) Handle(method, pattern string, handlers ...HandlerFunc) {
	if len(handlers) == 0 {
		panic("there must be at least one handler")
	}

	if p.basePath != "" {
		pattern = p.basePath + pattern
	}

	handlers = p.combineHandlers(handlers)
	muxHandler := p.composeMiddleware(handlers)
	p.router.Handle(method, pattern, muxHandler)
}

func (p *RouterPrefix) LoadHTMLGlob(pattern string) {
	templ := template.Must(template.ParseGlob(pattern))
	p.SetHTMLTemplate(templ)
}

func (p *RouterPrefix) LoadHTMLFiles(files ...string) {
	templ := template.Must(template.ParseFiles(files...))
	p.SetHTMLTemplate(templ)
}

func (p *RouterPrefix) SetHTMLTemplate(templ *template.Template) {
	p.template = templ
}

// StaticFile register router pattern and response file in path
func (p *RouterPrefix) StaticFile(pattern, filepath string) {
	if strings.Contains(pattern, ":") || strings.Contains(pattern, "*") {
		panic("URL parameters can not be used when serving a static folder")
	}
	handler := func(c *Context) {
		c.ServeFile(filepath)
	}

	p.Head(pattern, handler)
	p.Get(pattern, handler)
}

// Static register router pattern and response file in the request url
func (p *RouterPrefix) Static(pattern, dir string) {
	if strings.Contains(pattern, ":") || strings.Contains(pattern, "*") {
		panic("URL parameters can not be used when serving a static folder")
	}

	fileServer := http.StripPrefix(pattern, http.FileServer(http.Dir(dir)))
	handler := func(c *Context) {
		fileServer.ServeHTTP(c.ResponseWriter, c.Request)
	}

	urlPattern := path.Join(pattern, "/*filepath")
	p.Head(urlPattern, handler)
	p.Get(urlPattern, handler)
}

// combine middleware and handlers for specific route
func (p *RouterPrefix) combineHandlers(handlers []HandlerFunc) []HandlerFunc {
	finalSize := len(p.Handlers) + len(handlers)
	if finalSize >= int(abortIndex) {
		panic("too many handlers")
	}
	mergedHandlers := make([]HandlerFunc, finalSize)
	copyHandlers(mergedHandlers, p.Handlers)
	copyHandlers(mergedHandlers[len(p.Handlers):], handlers)
	return mergedHandlers
}

// Prefix creates a new router prefix. You should add all the routes that have common
// middlwares or the same path prefix. For example, all the routes that use a common
// middlware could be grouped.
func (p *RouterPrefix) Prefix(basePath string) *RouterPrefix {
	return &RouterPrefix{
		basePath: basePath,
		router:   p.router,
		Handlers: p.Handlers,
	}
}

func copyHandlers(dst, src []HandlerFunc) {
	for index, val := range src {
		dst[index] = val
	}
}

// Construct handler for specific router
func (p *RouterPrefix) composeMiddleware(handlers []HandlerFunc) router.Handle {
	return func(rw http.ResponseWriter, req *http.Request, ps router.Params) {
		context := &Context{
			ResponseWriter: rw,
			Request:        req,
			handlers:       handlers,
			current:        -1,
			Params:         ps,
			Path:           req.URL.Path,
			template:       p.template,
			engine:         p.engine,
			statusCode:     defaultStatusCode,
		}

		context.Next()
	}
}

func (p *RouterPrefix) rebuild404Handlers() {
	p.allNoRoute = p.combineHandlers(nil)
}

func (p *RouterPrefix) rebuild405Handlers() {
	p.allNoMethod = p.combineHandlers(nil)
}

// noMethod use as a default handler for router not allowed
func (p *RouterPrefix) noRoute(rw http.ResponseWriter, req *http.Request) {
	context := &Context{
		ResponseWriter: rw,
		Request:        req,
		handlers:       p.allNoRoute,
		current:        -1,
		Path:           req.URL.String(),
		template:       p.template,
		engine:         p.engine,
		statusCode:     defaultStatusCode,
	}

	context.Status(http.StatusNotFound)
	context.String(default404Body)
	context.Next()
}

// noMethod use as a default handler for Method not allowed
func (p *RouterPrefix) noMethod(rw http.ResponseWriter, req *http.Request) {
	context := &Context{
		ResponseWriter: rw,
		Request:        req,
		handlers:       p.allNoMethod,
		current:        -1,
		Path:           req.URL.Path,
		template:       p.template,
		engine:         p.engine,
		statusCode:     defaultStatusCode,
	}

	context.Status(http.StatusMethodNotAllowed)
	context.String(default405Body)
	context.Next()
}

// NoRoute which is called when no matching route is found. If it is not set, noRoute is used.
func (p *RouterPrefix) NoRoute(handlers ...HandlerFunc) {
	if len(handlers) == 0 {
		panic("there must be at least one handler")
	}

	handlers = p.combineHandlers(handlers)
	handler := p.composeMiddleware(handlers)
	p.router.NoRoute = http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
		handler(rw, req, router.Params{})
	})
}

// NoMethod which is called when method is not registered. If it is not set, noMethod is used.
func (p *RouterPrefix) NoMethod(handlers ...HandlerFunc) {
	if len(handlers) == 0 {
		panic("there must be at least one handler")
	}

	handlers = p.combineHandlers(handlers)
	handler := p.composeMiddleware(handlers)
	p.router.NoMethod = http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
		handler(rw, req, router.Params{})
	})
}
<|endoftext|>


<commit_before>/*
RpcClient for Go RPC Servers
Copyright (C) 2012-2014 ITsysCOM GmbH

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>
*/

package rpcclient

import (
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"net/http"
	"net/rpc"
	"net/rpc/jsonrpc"
	"strings"
	"time"
)

var JSON_RPC = "json"
var JSON_HTTP = "http_jsonrpc"
var GOB_RPC = "gob"
var ReqUnsynchronized = errors.New("REQ_UNSYNCHRONIZED")

// successive Fibonacci numbers.
func Fib() func() time.Duration {
	a, b := 0, 1
	return func() time.Duration {
		a, b = b, a+b
		return time.Duration(a) * time.Second
	}
}

func NewRpcClient(transport, addr string, connectAttempts, reconnects int, codec string) (*RpcClient, error) {
	var err error
	rpcClient := &RpcClient{transport: transport, address: addr, reconnects: reconnects, codec: codec}
	delay := Fib()
	for i := 0; i < connectAttempts; i++ {
		err = rpcClient.connect()
		if err == nil { //Connected so no need to reiterate
			break
		}
		time.Sleep(delay())
	}
	if err != nil {
		return nil, err
	}
	return rpcClient, nil
}

type RpcClient struct {
	transport  string
	address    string
	reconnects int
	codec      string // JSON_RPC or GOB_RPC
	connection RpcClientConnection
}

func (self *RpcClient) connect() (err error) {
	switch self.codec {
	case JSON_RPC:
		self.connection, err = jsonrpc.Dial(self.transport, self.address)
	case JSON_HTTP:
		self.connection = &HttpJsonRpcClient{httpClient: new(http.Client), url: self.address}
	default:
		self.connection, err = rpc.Dial(self.transport, self.address)
	}
	return
}

func (self *RpcClient) reconnect() (err error) {
	if self.codec == JSON_HTTP { // http client has automatic reconnects in place
		return self.connect()
	}
	i := 0
	delay := Fib()
	for {
		if i != -1 && i >= self.reconnects { // Maximum reconnects reached, -1 for infinite reconnects
			break
		}
		if err = self.connect(); err == nil { // No error on connect, succcess
			return nil
		}
		time.Sleep(delay()) // Cound not reconnect, retry
	}
	return errors.New("RECONNECT_FAIL")
}

func (self *RpcClient) Call(serviceMethod string, args interface{}, reply interface{}) error {
	err := self.connection.Call(serviceMethod, args, reply)
	if err != nil && (err == rpc.ErrShutdown || err == ReqUnsynchronized) && self.reconnects != 0 {
		if errReconnect := self.reconnect(); errReconnect != nil {
			return err
		} else { // Run command after reconnect
			return self.connection.Call(serviceMethod, args, reply)
		}
	}
	return err
}

// Connection used in RpcClient, as interface so we can combine the rpc.RpcClient with http one or websocket
type RpcClientConnection interface {
	Call(string, interface{}, interface{}) error
}

// Response received for
type JsonRpcResponse struct {
	Id     uint64
	Result *json.RawMessage
	Error  interface{}
}

type HttpJsonRpcClient struct {
	httpClient *http.Client
	id         uint64
	url        string
}

func (self *HttpJsonRpcClient) Call(serviceMethod string, args interface{}, reply interface{}) error {
	self.id += 1
	id := self.id
	data, err := json.Marshal(map[string]interface{}{
		"method": serviceMethod,
		"id":     self.id,
		"params": [1]interface{}{args},
	})
	if err != nil {
		return err
	}
	resp, err := self.httpClient.Post(self.url, "application/json", ioutil.NopCloser(strings.NewReader(string(data)))) // Closer so we automatically have close after response
	if err != nil {
		return err
	}
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return err
	}
	var jsonRsp JsonRpcResponse
	err = json.Unmarshal(body, &jsonRsp)
	if err != nil {
		return err
	}
	if jsonRsp.Id != id {
		return ReqUnsynchronized
	}
	if jsonRsp.Error != nil || jsonRsp.Result == nil {
		x, ok := jsonRsp.Error.(string)
		if !ok {
			return fmt.Errorf("invalid error %v", jsonRsp.Error)
		}
		if x == "" {
			x = "unspecified error"
		}
		return errors.New(x)
	}
	return json.Unmarshal(*jsonRsp.Result, reply)
}
<commit_msg>Adding support for in-process RPC<commit_after>/*
RpcClient for Go RPC Servers
Copyright (C) 2012-2014 ITsysCOM GmbH

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>
*/

package rpcclient

import (
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"net/http"
	"net/rpc"
	"net/rpc/jsonrpc"
	"strings"
	"sync"
	"time"
)

const (
	JSON_RPC     = "json"
	JSON_HTTP    = "http_jsonrpc"
	GOB_RPC      = "gob"
	INTERNAL_RPC = "internal"
)

var (
	ErrReqUnsynchronized       = errors.New("REQ_UNSYNCHRONIZED")
	ErrUnsupporteServiceMethod = errors.New("UNSUPPORTED_SERVICE_METHOD")
	ErrWrongArgsType           = errors.New("WRONG_ARGS_TYPE")
	ErrWrongReplyType          = errors.New("WRONG_REPLY_TYPE")
)

// successive Fibonacci numbers.
func Fib() func() time.Duration {
	a, b := 0, 1
	return func() time.Duration {
		a, b = b, a+b
		return time.Duration(a) * time.Second
	}
}

func NewRpcClient(transport, addr string, connectAttempts, reconnects int, codec string, internalConn RpcClientConnection) (*RpcClient, error) {
	var err error
	rpcClient := &RpcClient{transport: transport, address: addr, reconnects: reconnects, codec: codec, connection: internalConn, connMux: new(sync.Mutex)}
	delay := Fib()
	for i := 0; i < connectAttempts; i++ {
		err = rpcClient.connect()
		if err == nil { //Connected so no need to reiterate
			break
		}
		time.Sleep(delay())
	}
	if err != nil {
		return nil, err
	}
	return rpcClient, nil
}

type RpcClient struct {
	transport  string
	address    string
	reconnects int
	codec      string // JSON_RPC or GOB_RPC
	connection RpcClientConnection
	connMux    *sync.Mutex
}

func (self *RpcClient) connect() (err error) {
	self.connMux.Lock()
	defer self.connMux.Unlock()
	switch self.codec {
	case JSON_RPC:
		self.connection, err = jsonrpc.Dial(self.transport, self.address)
	case JSON_HTTP:
		self.connection = &HttpJsonRpcClient{httpClient: new(http.Client), url: self.address}
	case INTERNAL_RPC:
		return nil // connection should be set on init
	default:
		self.connection, err = rpc.Dial(self.transport, self.address)
	}
	return
}

func (self *RpcClient) reconnect() (err error) {
	if self.codec == JSON_HTTP { // http client has automatic reconnects in place
		return self.connect()
	}
	i := 0
	delay := Fib()
	for {
		if i != -1 && i >= self.reconnects { // Maximum reconnects reached, -1 for infinite reconnects
			break
		}
		if err = self.connect(); err == nil { // No error on connect, succcess
			return nil
		}
		time.Sleep(delay()) // Cound not reconnect, retry
	}
	return errors.New("RECONNECT_FAIL")
}

func (self *RpcClient) Call(serviceMethod string, args interface{}, reply interface{}) error {
	err := self.connection.Call(serviceMethod, args, reply)
	if err != nil && (err == rpc.ErrShutdown || err == ErrReqUnsynchronized) && self.reconnects != 0 {
		if errReconnect := self.reconnect(); errReconnect != nil {
			return err
		} else { // Run command after reconnect
			return self.connection.Call(serviceMethod, args, reply)
		}
	}
	return err
}

// Connection used in RpcClient, as interface so we can combine the rpc.RpcClient with http one or websocket
type RpcClientConnection interface {
	Call(string, interface{}, interface{}) error
}

// Response received for
type JsonRpcResponse struct {
	Id     uint64
	Result *json.RawMessage
	Error  interface{}
}

type HttpJsonRpcClient struct {
	httpClient *http.Client
	id         uint64
	url        string
}

func (self *HttpJsonRpcClient) Call(serviceMethod string, args interface{}, reply interface{}) error {
	self.id += 1
	id := self.id
	data, err := json.Marshal(map[string]interface{}{
		"method": serviceMethod,
		"id":     self.id,
		"params": [1]interface{}{args},
	})
	if err != nil {
		return err
	}
	resp, err := self.httpClient.Post(self.url, "application/json", ioutil.NopCloser(strings.NewReader(string(data)))) // Closer so we automatically have close after response
	if err != nil {
		return err
	}
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return err
	}
	var jsonRsp JsonRpcResponse
	err = json.Unmarshal(body, &jsonRsp)
	if err != nil {
		return err
	}
	if jsonRsp.Id != id {
		return ErrReqUnsynchronized
	}
	if jsonRsp.Error != nil || jsonRsp.Result == nil {
		x, ok := jsonRsp.Error.(string)
		if !ok {
			return fmt.Errorf("invalid error %v", jsonRsp.Error)
		}
		if x == "" {
			x = "unspecified error"
		}
		return errors.New(x)
	}
	return json.Unmarshal(*jsonRsp.Result, reply)
}
<|endoftext|>


<commit_before>package rpg2d

import (
	"time"

	"github.com/ghthor/engine/rpg2d/entity"
	"github.com/ghthor/engine/rpg2d/quad"
	"github.com/ghthor/engine/sim"
	"github.com/ghthor/engine/sim/stime"
)

// An interface the user will implement to resolve
// an entity from an actor. This is user defined
// because the user is creating the entities and actors
// that will be added to the simulation. This allows the
// user to define how the actors state is stored,
// aka database design/interaction.
type EntityResolver interface {
	EntityForActor(sim.Actor) entity.Entity
}

// A SimulationDef used to configure a simulation
// to define the how the simulation will behave.
type SimulationDef struct {
	// The target FPS for the simulation to calculate at
	FPS int

	// Initial World State
	Now      stime.Time
	QuadTree quad.Quad

	// User defined to resolve an entity from and actor
	EntityResolver EntityResolver

	// User defined input application phase
	InputPhaseHandler quad.InputPhaseHandler

	// User defined the narrow phase
	NarrowPhaseHandler quad.NarrowPhaseHandler
}

type initialWorldState struct {
	now      stime.Time
	quadTree quad.Quad
}

type simSettings struct {
	fps int

	EntityResolver
	quad.InputPhaseHandler
	quad.NarrowPhaseHandler
}

// An implementation of engine/sim.RunningSimulation
type runningSimulation struct {
	//---- Communication
	// These channels are used by the public api
	// to add and remove actors. They are 1way
	// send only channels. The requests contains
	// a send only and recieve only channel so the
	// the public api call will be atomic action
	// and the caller can assume without a doubt
	// that the actor is now added or removed
	// from the simulation.
	addActor    chan<- addActorReq
	removeActor chan<- removeActorReq

	// This channel is used by the public api to request
	// that the simulation is halted. You send a 1way
	// send only channel into the game loop so the public
	// api can wait and be notified that the go routine
	// has returned and is no longer running.
	requestHalt chan<- chan<- struct{}
}

// Communication object used to atomicly add a new actor to the sim
type addActorReq struct {
	toBeAdded chan sim.Actor
	wasAdded  chan sim.Actor
}

// Implement engine/sim.RunningSimulation
func (s runningSimulation) ConnectActor(a sim.Actor) error {
	ch := make(chan sim.Actor)

	// Create an add request
	actor := addActorReq{ch, ch}

	// Send the add request to the game loop
	s.addActor <- actor

	// Send the actor to be added to the game loop
	actor.toBeAdded <- a

	// Wait for the add request to be successfully completed
	a = <-actor.wasAdded
	return nil
}

// Communication object used to atomicly remove an actor from the sim
type removeActorReq struct {
	toBeRemoved chan sim.Actor
	wasRemoved  chan sim.Actor
}

// Implement engine/sim.RunningSimulation
func (s runningSimulation) RemoveActor(a sim.Actor) error {
	ch := make(chan sim.Actor)

	// Create a remove request
	actor := removeActorReq{ch, ch}

	// Send the remove request to the game loop
	s.removeActor <- actor

	// Send the actor to be removed to the game loop
	actor.toBeRemoved <- a

	// Wait for the remove request to be successfully completed
	a = <-actor.wasRemoved
	return nil
}

// Implement engine/sim.RunningSimulation
func (s runningSimulation) Halt() (sim.HaltedSimulation, error) {
	wasHalted := make(chan struct{})

	// Send a request to the game loop to halt
	s.requestHalt <- wasHalted

	// Wait for the halt request to be successfully completed
	<-wasHalted

	return haltedSimulation{}, nil
}

// Implement engine/sim.UnstartedSimulation
func (s SimulationDef) Begin() (sim.RunningSimulation, error) {
	initialState := initialWorldState{
		now:      s.Now,
		quadTree: s.QuadTree,
	}

	settings := simSettings{
		s.FPS,

		s.EntityResolver,
		s.InputPhaseHandler,
		s.NarrowPhaseHandler,
	}

	rs := &runningSimulation{}

	// Starts 2 go routines and returns
	// The ticker and the engine communication kernel
	rs.startLoop(initialState, settings)

	return rs, nil
}

// Prepares a closure and executes it as a go routine
// Calling this function will create 2 infinte looping
// go routines. One for the clock ticker and one for
// calulating the next world state and adding/removing actors.
// This method has a pointer recv because it MUST set the
// addActor and removeActor communication channels used
// by the public api to request adding & removing actors
func (s *runningSimulation) startLoop(initialState initialWorldState, settings simSettings) {
	//---- Create all the communication channels

	// Make the 2way channels that will be used to make
	// add and remove actor requests to the go routine game loop
	addCh := make(chan addActorReq)
	removeCh := make(chan removeActorReq)

	// Set the 1way send chanels used by the public api
	s.addActor = addCh
	s.removeActor = removeCh

	// Set the 1way recieve channels used by the game loop
	var addReq <-chan addActorReq
	var removeReq <-chan removeActorReq

	addReq = addCh
	removeReq = removeCh

	// Make channel to be used to by the public api to
	// request that the simulation be halted
	haltCh := make(chan chan<- struct{})

	// Set the 1way send channel used by the public api
	s.requestHalt = haltCh

	// Set the 1way recieve channel used by the game loop
	var haltReq <-chan chan<- struct{}
	haltReq = haltCh

	quadTree := initialState.quadTree
	clock := stime.Clock(initialState.now)

	//---- User provided actor to entity resolver
	entityResolver := settings.EntityResolver

	//---- User provided input application phase
	inputPhase := settings.InputPhaseHandler

	//---- User provided narrow phase
	narrowPhase := settings.NarrowPhaseHandler

	runTick := func(q quad.Quad, t stime.Time) quad.Quad {
		return quad.RunPhasesOn(q, inputPhase, narrowPhase, t)
	}

	// Start the Clock
	ticker := time.NewTicker(time.Duration(1000/settings.fps) * time.Millisecond)

	// Start the simulation server
	go func() {
		var hasHalted chan<- struct{}

	gameLoop:
		for {
			// Prioritized select for ticker.C and haltReq
			select {
			case <-ticker.C:
				goto tick

			case hasHalted = <-haltReq:
				break gameLoop
			default:
			}

			select {
			case <-ticker.C:
				goto tick

			case actor := <-addReq:
				// a is the new sim.Actor{} to be inserted into the sim
				a := <-actor.toBeAdded

				e := entityResolver.EntityForActor(a)
				quadTree = quadTree.Insert(e)

				// signal that the operation was a success
				actor.wasAdded <- a

			case actor := <-removeReq:
				// a is the new sim.Actor{} to be removed from the sim
				a := <-actor.toBeRemoved

				// TODO removed the actor from the simulation

				// signal that the operation was a success
				actor.wasRemoved <- a

			case hasHalted = <-haltReq:
				break gameLoop
			}

		tick:
			clock = clock.Tick()
			quadTree = runTick(quadTree, clock.Now())
			// TODO quadTree to state
			// TODO send state
			continue
		}

		// We're done with cleanup and going to exit
		hasHalted <- struct{}{}
	}()
}

type haltedSimulation struct{}
<commit_msg>Force user to provide a quad tree to a simulation def<commit_after>package rpg2d

import (
	"errors"
	"time"

	"github.com/ghthor/engine/rpg2d/entity"
	"github.com/ghthor/engine/rpg2d/quad"
	"github.com/ghthor/engine/sim"
	"github.com/ghthor/engine/sim/stime"
)

// An interface the user will implement to resolve
// an entity from an actor. This is user defined
// because the user is creating the entities and actors
// that will be added to the simulation. This allows the
// user to define how the actors state is stored,
// aka database design/interaction.
type EntityResolver interface {
	EntityForActor(sim.Actor) entity.Entity
}

// A SimulationDef used to configure a simulation
// to define the how the simulation will behave.
type SimulationDef struct {
	// The target FPS for the simulation to calculate at
	FPS int

	// Initial World State
	Now      stime.Time
	QuadTree quad.Quad

	// User defined to resolve an entity from and actor
	EntityResolver EntityResolver

	// User defined input application phase
	InputPhaseHandler quad.InputPhaseHandler

	// User defined the narrow phase
	NarrowPhaseHandler quad.NarrowPhaseHandler
}

type initialWorldState struct {
	now      stime.Time
	quadTree quad.Quad
}

type simSettings struct {
	fps int

	EntityResolver
	quad.InputPhaseHandler
	quad.NarrowPhaseHandler
}

// An implementation of engine/sim.RunningSimulation
type runningSimulation struct {
	//---- Communication
	// These channels are used by the public api
	// to add and remove actors. They are 1way
	// send only channels. The requests contains
	// a send only and recieve only channel so the
	// the public api call will be atomic action
	// and the caller can assume without a doubt
	// that the actor is now added or removed
	// from the simulation.
	addActor    chan<- addActorReq
	removeActor chan<- removeActorReq

	// This channel is used by the public api to request
	// that the simulation is halted. You send a 1way
	// send only channel into the game loop so the public
	// api can wait and be notified that the go routine
	// has returned and is no longer running.
	requestHalt chan<- chan<- struct{}
}

// Communication object used to atomicly add a new actor to the sim
type addActorReq struct {
	toBeAdded chan sim.Actor
	wasAdded  chan sim.Actor
}

// Implement engine/sim.RunningSimulation
func (s runningSimulation) ConnectActor(a sim.Actor) error {
	ch := make(chan sim.Actor)

	// Create an add request
	actor := addActorReq{ch, ch}

	// Send the add request to the game loop
	s.addActor <- actor

	// Send the actor to be added to the game loop
	actor.toBeAdded <- a

	// Wait for the add request to be successfully completed
	a = <-actor.wasAdded
	return nil
}

// Communication object used to atomicly remove an actor from the sim
type removeActorReq struct {
	toBeRemoved chan sim.Actor
	wasRemoved  chan sim.Actor
}

// Implement engine/sim.RunningSimulation
func (s runningSimulation) RemoveActor(a sim.Actor) error {
	ch := make(chan sim.Actor)

	// Create a remove request
	actor := removeActorReq{ch, ch}

	// Send the remove request to the game loop
	s.removeActor <- actor

	// Send the actor to be removed to the game loop
	actor.toBeRemoved <- a

	// Wait for the remove request to be successfully completed
	a = <-actor.wasRemoved
	return nil
}

// Implement engine/sim.RunningSimulation
func (s runningSimulation) Halt() (sim.HaltedSimulation, error) {
	wasHalted := make(chan struct{})

	// Send a request to the game loop to halt
	s.requestHalt <- wasHalted

	// Wait for the halt request to be successfully completed
	<-wasHalted

	return haltedSimulation{}, nil
}

var ErrMustProvideAQuadtree = errors.New("user must provide a quad tree to a simulation defination")

// Implement engine/sim.UnstartedSimulation
func (s SimulationDef) Begin() (sim.RunningSimulation, error) {
	if s.QuadTree == nil {
		return nil, ErrMustProvideAQuadtree
	}

	initialState := initialWorldState{
		now:      s.Now,
		quadTree: s.QuadTree,
	}

	settings := simSettings{
		s.FPS,

		s.EntityResolver,
		s.InputPhaseHandler,
		s.NarrowPhaseHandler,
	}

	rs := &runningSimulation{}

	// Starts 2 go routines and returns
	// The ticker and the engine communication kernel
	rs.startLoop(initialState, settings)

	return rs, nil
}

// Prepares a closure and executes it as a go routine
// Calling this function will create 2 infinte looping
// go routines. One for the clock ticker and one for
// calulating the next world state and adding/removing actors.
// This method has a pointer recv because it MUST set the
// addActor and removeActor communication channels used
// by the public api to request adding & removing actors
func (s *runningSimulation) startLoop(initialState initialWorldState, settings simSettings) {
	//---- Create all the communication channels

	// Make the 2way channels that will be used to make
	// add and remove actor requests to the go routine game loop
	addCh := make(chan addActorReq)
	removeCh := make(chan removeActorReq)

	// Set the 1way send chanels used by the public api
	s.addActor = addCh
	s.removeActor = removeCh

	// Set the 1way recieve channels used by the game loop
	var addReq <-chan addActorReq
	var removeReq <-chan removeActorReq

	addReq = addCh
	removeReq = removeCh

	// Make channel to be used to by the public api to
	// request that the simulation be halted
	haltCh := make(chan chan<- struct{})

	// Set the 1way send channel used by the public api
	s.requestHalt = haltCh

	// Set the 1way recieve channel used by the game loop
	var haltReq <-chan chan<- struct{}
	haltReq = haltCh

	quadTree := initialState.quadTree
	clock := stime.Clock(initialState.now)

	//---- User provided actor to entity resolver
	entityResolver := settings.EntityResolver

	//---- User provided input application phase
	inputPhase := settings.InputPhaseHandler

	//---- User provided narrow phase
	narrowPhase := settings.NarrowPhaseHandler

	runTick := func(q quad.Quad, t stime.Time) quad.Quad {
		return quad.RunPhasesOn(q, inputPhase, narrowPhase, t)
	}

	// Start the Clock
	ticker := time.NewTicker(time.Duration(1000/settings.fps) * time.Millisecond)

	// Start the simulation server
	go func() {
		var hasHalted chan<- struct{}

	gameLoop:
		for {
			// Prioritized select for ticker.C and haltReq
			select {
			case <-ticker.C:
				goto tick

			case hasHalted = <-haltReq:
				break gameLoop
			default:
			}

			select {
			case <-ticker.C:
				goto tick

			case actor := <-addReq:
				// a is the new sim.Actor{} to be inserted into the sim
				a := <-actor.toBeAdded

				e := entityResolver.EntityForActor(a)
				quadTree = quadTree.Insert(e)

				// signal that the operation was a success
				actor.wasAdded <- a

			case actor := <-removeReq:
				// a is the new sim.Actor{} to be removed from the sim
				a := <-actor.toBeRemoved

				// TODO removed the actor from the simulation

				// signal that the operation was a success
				actor.wasRemoved <- a

			case hasHalted = <-haltReq:
				break gameLoop
			}

		tick:
			clock = clock.Tick()
			quadTree = runTick(quadTree, clock.Now())
			// TODO quadTree to state
			// TODO send state
			continue
		}

		// We're done with cleanup and going to exit
		hasHalted <- struct{}{}
	}()
}

type haltedSimulation struct{}
<|endoftext|>


<commit_before>package models

import (
	"time"

	mgo "gopkg.in/mgo.v2"
	"gopkg.in/mgo.v2/bson"

	"sunteng/commons/log"
)

const (
	Coll_JobLog       = "job_log"
	Coll_JobLatestLog = "job_latest_log"
)

// 任务执行记录
type JobLog struct {
	Id        bson.ObjectId `bson:"_id,omitempty" json:"id"`
	JobId     string        `bson:"jobId" json:"jobId"`               // 任务 Id，索引
	JobGroup  string        `bson:"jobGroup" json:"jobGroup"`         // 任务分组，配合 Id 跳转用
	Name      string        `bson:"name" json:"name"`                 // 任务名称
	Node      string        `bson:"node" json:"node"`                 // 运行此次任务的节点 ip，索引
	Command   string        `bson:"command" json:"command,omitempty"` // 执行的命令，包括参数
	Output    string        `bson:"output" json:"output,omitempty"`   // 任务输出的所有内容
	Success   bool          `bson:"success" json:"success"`           // 是否执行成功
	BeginTime time.Time     `bson:"beginTime" json:"beginTime"`       // 任务开始执行时间，精确到毫秒，索引
	EndTime   time.Time     `bson:"endTime" json:"endTime"`           // 任务执行完毕时间，精确到毫秒
}

type JobLatestLog struct {
	JobLog   `bson:",inline"`
	RefLogId string `bson:"refLogId,omitempty" json:"refLogId"`
}

func GetJobLogById(id bson.ObjectId) (l *JobLog, err error) {
	err = mgoDB.FindId(Coll_JobLog, id, &l)
	return
}

var selectForJobLogList = bson.M{"command": 0, "output": 0}

func GetJobLogList(query bson.M, page, size int, sort string) (list []*JobLog, total int, err error) {
	err = mgoDB.WithC(Coll_JobLog, func(c *mgo.Collection) error {
		total, err = c.Find(query).Count()
		if err != nil {
			return err
		}
		return c.Find(query).Select(selectForJobLogList).Sort(sort).Skip((page - 1) * size).Limit(size).All(&list)
	})
	return
}

func GetJobLatestLogListByJobIds(jobIds []string) (m map[string]*JobLatestLog, err error) {
	var list []*JobLatestLog
	err = mgoDB.AllSelect(Coll_JobLatestLog, bson.M{"jobId": bson.M{"$in": jobIds}}, selectForJobLogList, &list)
	if err != nil {
		return
	}

	m = make(map[string]*JobLatestLog, len(list))
	for i := range list {
		m[list[i].JobId] = list[i]
	}
	return
}

func CreateJobLog(j *Job, t time.Time, rs string, success bool) {
	jl := JobLog{
		Id:    bson.NewObjectId(),
		JobId: j.GetID(),

		JobGroup: j.Group,
		Name:     j.Name,

		Node: j.runOn,

		Command: j.Command,
		Output:  rs,
		Success: success,

		BeginTime: t,
		EndTime:   time.Now(),
	}
	if err := mgoDB.Insert(Coll_JobLog, jl); err != nil {
		log.Error(err.Error())
	}

	latestLog := &JobLatestLog{
		RefLogId: jl.Id.Hex(),
		JobLog:   jl,
	}
	latestLog.Id = ""
	if err := mgoDB.Upsert(Coll_JobLatestLog, bson.M{"jobId": jl.JobId, "jobGroup": jl.JobGroup}, latestLog); err != nil {
		log.Error(err.Error())
	}
}
<commit_msg>fixed: 更新最后日志添加 node 条件处理 upsert<commit_after>package models

import (
	"time"

	mgo "gopkg.in/mgo.v2"
	"gopkg.in/mgo.v2/bson"

	"sunteng/commons/log"
)

const (
	Coll_JobLog       = "job_log"
	Coll_JobLatestLog = "job_latest_log"
)

// 任务执行记录
type JobLog struct {
	Id        bson.ObjectId `bson:"_id,omitempty" json:"id"`
	JobId     string        `bson:"jobId" json:"jobId"`               // 任务 Id，索引
	JobGroup  string        `bson:"jobGroup" json:"jobGroup"`         // 任务分组，配合 Id 跳转用
	Name      string        `bson:"name" json:"name"`                 // 任务名称
	Node      string        `bson:"node" json:"node"`                 // 运行此次任务的节点 ip，索引
	Command   string        `bson:"command" json:"command,omitempty"` // 执行的命令，包括参数
	Output    string        `bson:"output" json:"output,omitempty"`   // 任务输出的所有内容
	Success   bool          `bson:"success" json:"success"`           // 是否执行成功
	BeginTime time.Time     `bson:"beginTime" json:"beginTime"`       // 任务开始执行时间，精确到毫秒，索引
	EndTime   time.Time     `bson:"endTime" json:"endTime"`           // 任务执行完毕时间，精确到毫秒
}

type JobLatestLog struct {
	JobLog   `bson:",inline"`
	RefLogId string `bson:"refLogId,omitempty" json:"refLogId"`
}

func GetJobLogById(id bson.ObjectId) (l *JobLog, err error) {
	err = mgoDB.FindId(Coll_JobLog, id, &l)
	return
}

var selectForJobLogList = bson.M{"command": 0, "output": 0}

func GetJobLogList(query bson.M, page, size int, sort string) (list []*JobLog, total int, err error) {
	err = mgoDB.WithC(Coll_JobLog, func(c *mgo.Collection) error {
		total, err = c.Find(query).Count()
		if err != nil {
			return err
		}
		return c.Find(query).Select(selectForJobLogList).Sort(sort).Skip((page - 1) * size).Limit(size).All(&list)
	})
	return
}

func GetJobLatestLogList(query bson.M, page, size int, sort string) (list []*JobLatestLog, total int, err error) {
	err = mgoDB.WithC(Coll_JobLatestLog, func(c *mgo.Collection) error {
		total, err = c.Find(query).Count()
		if err != nil {
			return err
		}
		return c.Find(query).Select(selectForJobLogList).Sort(sort).Skip((page - 1) * size).Limit(size).All(&list)
	})
	return
}

func GetJobLatestLogListByJobIds(jobIds []string) (m map[string]*JobLatestLog, err error) {
	var list []*JobLatestLog
	err = mgoDB.AllSelect(Coll_JobLatestLog, bson.M{"jobId": bson.M{"$in": jobIds}}, selectForJobLogList, &list)
	if err != nil {
		return
	}

	m = make(map[string]*JobLatestLog, len(list))
	for i := range list {
		m[list[i].JobId] = list[i]
	}
	return
}

func CreateJobLog(j *Job, t time.Time, rs string, success bool) {
	jl := JobLog{
		Id:    bson.NewObjectId(),
		JobId: j.GetID(),

		JobGroup: j.Group,
		Name:     j.Name,

		Node: j.runOn,

		Command: j.Command,
		Output:  rs,
		Success: success,

		BeginTime: t,
		EndTime:   time.Now(),
	}
	if err := mgoDB.Insert(Coll_JobLog, jl); err != nil {
		log.Error(err.Error())
	}

	latestLog := &JobLatestLog{
		RefLogId: jl.Id.Hex(),
		JobLog:   jl,
	}
	latestLog.Id = ""
	if err := mgoDB.Upsert(Coll_JobLatestLog, bson.M{"node": jl.Node, "jobId": jl.JobId, "jobGroup": jl.JobGroup}, latestLog); err != nil {
		log.Error(err.Error())
	}
}
<|endoftext|>


<commit_before>/*
   Copyright (c) 2016 VMware, Inc. All Rights Reserved.
   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 models

import (
	"time"
)

// Project holds the details of a project.
type Project struct {
	ProjectID       int64     `orm:"column(project_id)" json:"project_id"`
	OwnerID         int       `orm:"column(owner_id)" json:"owner_id"`
	Name            string    `orm:"column(name)" json:"name"`
	CreationTime    time.Time `orm:"column(creation_time)" json:"creation_time"`
	CreationTimeStr string    `json:"creation_time_str"`
	Deleted         int       `orm:"column(deleted)" json:"deleted""`
	//UserID          int `json:"UserId"`
	OwnerName string `json:"owner_name"`
	Public    int    `orm:"column(public)" json:"public"`
	//This field does not have correspondent column in DB, this is just for UI to disable button
	Togglable bool

	UpdateTime time.Time `orm:"update_time" json:"update_time"`
	Role       int       `json:"current_user_role_id"`
	RepoCount  int       `json:"repo_count"`
}
<commit_msg>fix annotation error<commit_after>/*
   Copyright (c) 2016 VMware, Inc. All Rights Reserved.
   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 models

import (
	"time"
)

// Project holds the details of a project.
type Project struct {
	ProjectID       int64     `orm:"column(project_id)" json:"project_id"`
	OwnerID         int       `orm:"column(owner_id)" json:"owner_id"`
	Name            string    `orm:"column(name)" json:"name"`
	CreationTime    time.Time `orm:"column(creation_time)" json:"creation_time"`
	CreationTimeStr string    `json:"creation_time_str"`
	Deleted         int       `orm:"column(deleted)" json:"deleted"`
	//UserID          int `json:"UserId"`
	OwnerName string `json:"owner_name"`
	Public    int    `orm:"column(public)" json:"public"`
	//This field does not have correspondent column in DB, this is just for UI to disable button
	Togglable bool

	UpdateTime time.Time `orm:"update_time" json:"update_time"`
	Role       int       `json:"current_user_role_id"`
	RepoCount  int       `json:"repo_count"`
}
<|endoftext|>


<commit_before>// Package models contains the types for schema 'trackit'.
package models

// Code generated by xo. DO NOT EDIT.

import (
	"database/sql"
	"errors"
	"time"
)

// User represents a row from 'trackit.user'.
type User struct {
	ID                     int            `json:"id"`                       // id
	Email                  string         `json:"email"`                    // email
	Auth                   string         `json:"auth"`                     // auth
	NextExternal           sql.NullString `json:"next_external"`            // next_external
	ParentUserID           sql.NullInt64  `json:"parent_user_id"`           // parent_user_id
	AwsCustomerIdentifier  string         `json:"aws_customer_identifier"`  // aws_customer_identifier
	AwsCustomerEntitlement bool           `json:"aws_customer_entitlement"` // aws_customer_entitlement
	NextUpdateEntitlement  time.Time      `json:"next_update_entitlement"`  // next_update_entitlement
	AnomaliesFilters       []byte         `json:"anomalies_filters"`        // anomalies_filters
	LastSeen               time.Time      `json:"last_seen"`                // last_seen
	LastUnusedReminder     time.Time      `json:"last_unused_reminder"`     // last_unused_reminder

	// xo fields
	_exists, _deleted bool
}

// Exists determines if the User exists in the database.
func (u *User) Exists() bool {
	return u._exists
}

// Deleted provides information if the User has been deleted from the database.
func (u *User) Deleted() bool {
	return u._deleted
}

// Insert inserts the User to the database.
func (u *User) Insert(db XODB) error {
	var err error

	// if already exist, bail
	if u._exists {
		return errors.New("insert failed: already exists")
	}

	// sql insert query, primary key provided by autoincrement
	const sqlstr = `INSERT INTO trackit.user (` +
		`email, auth, next_external, parent_user_id, aws_customer_identifier, aws_customer_entitlement, next_update_entitlement, anomalies_filters, last_seen, last_unused_reminder` +
		`) VALUES (` +
		`?, ?, ?, ?, ?, ?, ?, ?, ?, ?` +
		`)`

	// run query
	XOLog(sqlstr, u.Email, u.Auth, u.NextExternal, u.ParentUserID, u.AwsCustomerIdentifier, u.AwsCustomerEntitlement, u.NextUpdateEntitlement, u.AnomaliesFilters, u.LastSeen, u.LastUnusedReminder)
	res, err := db.Exec(sqlstr, u.Email, u.Auth, u.NextExternal, u.ParentUserID, u.AwsCustomerIdentifier, u.AwsCustomerEntitlement, u.NextUpdateEntitlement, u.AnomaliesFilters, u.LastSeen, u.LastUnusedReminder)
	if err != nil {
		return err
	}

	// retrieve id
	id, err := res.LastInsertId()
	if err != nil {
		return err
	}

	// set primary key and existence
	u.ID = int(id)
	u._exists = true

	return nil
}

// Update updates the User in the database.
func (u *User) Update(db XODB) error {
	var err error

	// if doesn't exist, bail
	if !u._exists {
		return errors.New("update failed: does not exist")
	}

	// if deleted, bail
	if u._deleted {
		return errors.New("update failed: marked for deletion")
	}

	// sql query
	const sqlstr = `UPDATE trackit.user SET ` +
		`email = ?, auth = ?, next_external = ?, parent_user_id = ?, aws_customer_identifier = ?, aws_customer_entitlement = ?, next_update_entitlement = ?, anomalies_filters = ?, last_seen = ?, last_unused_reminder = ?` +
		` WHERE id = ?`

	// run query
	XOLog(sqlstr, u.Email, u.Auth, u.NextExternal, u.ParentUserID, u.AwsCustomerIdentifier, u.AwsCustomerEntitlement, u.NextUpdateEntitlement, u.AnomaliesFilters, u.LastSeen, u.LastUnusedReminder, u.ID)
	_, err = db.Exec(sqlstr, u.Email, u.Auth, u.NextExternal, u.ParentUserID, u.AwsCustomerIdentifier, u.AwsCustomerEntitlement, u.NextUpdateEntitlement, u.AnomaliesFilters, u.LastSeen, u.LastUnusedReminder, u.ID)
	return err
}

// Save saves the User to the database.
func (u *User) Save(db XODB) error {
	if u.Exists() {
		return u.Update(db)
	}

	return u.Insert(db)
}

// Delete deletes the User from the database.
func (u *User) Delete(db XODB) error {
	var err error

	// if doesn't exist, bail
	if !u._exists {
		return nil
	}

	// if deleted, bail
	if u._deleted {
		return nil
	}

	// sql query
	const sqlstr = `DELETE FROM trackit.user WHERE id = ?`

	// run query
	XOLog(sqlstr, u.ID)
	_, err = db.Exec(sqlstr, u.ID)
	if err != nil {
		return err
	}

	// set deleted
	u._deleted = true

	return nil
}

// User returns the User associated with the User's ParentUserID (parent_user_id).
//
// Generated from foreign key 'parent_user'.
func (u *User) User(db XODB) (*User, error) {
	return UserByID(db, int(u.ParentUserID.Int64))
}

// UsersByParentUserID retrieves a row from 'trackit.user' as a User.
//
// Generated from index 'parent_user'.
func UsersByParentUserID(db XODB, parentUserID sql.NullInt64) ([]*User, error) {
	var err error

	// sql query
	const sqlstr = `SELECT ` +
		`id, email, auth, next_external, parent_user_id, aws_customer_identifier, aws_customer_entitlement, next_update_entitlement, anomalies_filters, last_seen, last_unused_reminder ` +
		`FROM trackit.user ` +
		`WHERE parent_user_id = ?`

	// run query
	XOLog(sqlstr, parentUserID)
	q, err := db.Query(sqlstr, parentUserID)
	if err != nil {
		return nil, err
	}
	defer q.Close()

	// load results
	res := []*User{}
	for q.Next() {
		u := User{
			_exists: true,
		}

		// scan
		err = q.Scan(&u.ID, &u.Email, &u.Auth, &u.NextExternal, &u.ParentUserID, &u.AwsCustomerIdentifier, &u.AwsCustomerEntitlement, &u.NextUpdateEntitlement, &u.AnomaliesFilters, &u.LastSeen, &u.LastUnusedReminder)
		if err != nil {
			return nil, err
		}

		res = append(res, &u)
	}

	return res, nil
}

// UserByEmail retrieves a row from 'trackit.user' as a User.
//
// Generated from index 'unique_email'.
func UserByEmail(db XODB, email string) (*User, error) {
	var err error

	// sql query
	const sqlstr = `SELECT ` +
		`id, email, auth, next_external, parent_user_id, aws_customer_identifier, aws_customer_entitlement, next_update_entitlement, anomalies_filters, last_seen, last_unused_reminder ` +
		`FROM trackit.user ` +
		`WHERE email = ?`

	// run query
	XOLog(sqlstr, email)
	u := User{
		_exists: true,
	}

	err = db.QueryRow(sqlstr, email).Scan(&u.ID, &u.Email, &u.Auth, &u.NextExternal, &u.ParentUserID, &u.AwsCustomerIdentifier, &u.AwsCustomerEntitlement, &u.NextUpdateEntitlement, &u.AnomaliesFilters, &u.LastSeen, &u.LastUnusedReminder)
	if err != nil {
		return nil, err
	}

	return &u, nil
}

// UserByID retrieves a row from 'trackit.user' as a User.
//
// Generated from index 'user_id_pkey'.
func UserByID(db XODB, id int) (*User, error) {
	var err error

	// sql query
	const sqlstr = `SELECT ` +
		`id, email, auth, next_external, parent_user_id, aws_customer_identifier, aws_customer_entitlement, next_update_entitlement, anomalies_filters, last_seen, last_unused_reminder ` +
		`FROM trackit.user ` +
		`WHERE id = ?`

	// run query
	XOLog(sqlstr, id)
	u := User{
		_exists: true,
	}

	err = db.QueryRow(sqlstr, id).Scan(&u.ID, &u.Email, &u.Auth, &u.NextExternal, &u.ParentUserID, &u.AwsCustomerIdentifier, &u.AwsCustomerEntitlement, &u.NextUpdateEntitlement, &u.AnomaliesFilters, &u.LastSeen, &u.LastUnusedReminder)
	if err != nil {
		return nil, err
	}

	return &u, nil
}
<commit_msg>Added new fields from user xo model<commit_after>// Package models contains the types for schema 'trackit'.
package models

// Code generated by xo. DO NOT EDIT.

import (
	"database/sql"
	"errors"
	"time"
)

// User represents a row from 'trackit.user'.
type User struct {
	ID                     int            `json:"id"`                       // id
	Created                time.Time      `json:"created"`                  // created
	Modified               time.Time      `json:"modified"`                 // modified
	Email                  string         `json:"email"`                    // email
	Auth                   string         `json:"auth"`                     // auth
	NextExternal           sql.NullString `json:"next_external"`            // next_external
	ParentUserID           sql.NullInt64  `json:"parent_user_id"`           // parent_user_id
	AwsCustomerIdentifier  string         `json:"aws_customer_identifier"`  // aws_customer_identifier
	AwsCustomerEntitlement bool           `json:"aws_customer_entitlement"` // aws_customer_entitlement
	NextUpdateEntitlement  time.Time      `json:"next_update_entitlement"`  // next_update_entitlement
	AnomaliesFilters       []byte         `json:"anomalies_filters"`        // anomalies_filters
	NextUpdateTags         time.Time      `json:"next_update_tags"`         // next_update_tags
	LastSeen               time.Time      `json:"last_seen"`                // last_seen
	LastUnusedReminder     time.Time      `json:"last_unused_reminder"`     // last_unused_reminder
	LastUnusedSlack        time.Time      `json:"last_unused_slack"`        // last_unused_slack

	// xo fields
	_exists, _deleted bool
}

// Exists determines if the User exists in the database.
func (u *User) Exists() bool {
	return u._exists
}

// Deleted provides information if the User has been deleted from the database.
func (u *User) Deleted() bool {
	return u._deleted
}

// Insert inserts the User to the database.
func (u *User) Insert(db XODB) error {
	var err error

	// if already exist, bail
	if u._exists {
		return errors.New("insert failed: already exists")
	}

	// sql insert query, primary key provided by autoincrement
	const sqlstr = `INSERT INTO trackit.user (` +
		`created, modified, email, auth, next_external, parent_user_id, aws_customer_identifier, aws_customer_entitlement, next_update_entitlement, anomalies_filters, next_update_tags, last_seen, last_unused_reminder, last_unused_slack` +
		`) VALUES (` +
		`?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?` +
		`)`

	// run query
	XOLog(sqlstr, u.Created, u.Modified, u.Email, u.Auth, u.NextExternal, u.ParentUserID, u.AwsCustomerIdentifier, u.AwsCustomerEntitlement, u.NextUpdateEntitlement, u.AnomaliesFilters, u.NextUpdateTags, u.LastSeen, u.LastUnusedReminder, u.LastUnusedSlack)
	res, err := db.Exec(sqlstr, u.Created, u.Modified, u.Email, u.Auth, u.NextExternal, u.ParentUserID, u.AwsCustomerIdentifier, u.AwsCustomerEntitlement, u.NextUpdateEntitlement, u.AnomaliesFilters, u.NextUpdateTags, u.LastSeen, u.LastUnusedReminder, u.LastUnusedSlack)
	if err != nil {
		return err
	}

	// retrieve id
	id, err := res.LastInsertId()
	if err != nil {
		return err
	}

	// set primary key and existence
	u.ID = int(id)
	u._exists = true

	return nil
}

// Update updates the User in the database.
func (u *User) Update(db XODB) error {
	var err error

	// if doesn't exist, bail
	if !u._exists {
		return errors.New("update failed: does not exist")
	}

	// if deleted, bail
	if u._deleted {
		return errors.New("update failed: marked for deletion")
	}

	// sql query
	const sqlstr = `UPDATE trackit.user SET ` +
		`created = ?, modified = ?, email = ?, auth = ?, next_external = ?, parent_user_id = ?, aws_customer_identifier = ?, aws_customer_entitlement = ?, next_update_entitlement = ?, anomalies_filters = ?, next_update_tags = ?, last_seen = ?, last_unused_reminder = ?, last_unused_slack = ?` +
		` WHERE id = ?`

	// run query
	XOLog(sqlstr, u.Created, u.Modified, u.Email, u.Auth, u.NextExternal, u.ParentUserID, u.AwsCustomerIdentifier, u.AwsCustomerEntitlement, u.NextUpdateEntitlement, u.AnomaliesFilters, u.NextUpdateTags, u.LastSeen, u.LastUnusedReminder, u.LastUnusedSlack, u.ID)
	_, err = db.Exec(sqlstr, u.Created, u.Modified, u.Email, u.Auth, u.NextExternal, u.ParentUserID, u.AwsCustomerIdentifier, u.AwsCustomerEntitlement, u.NextUpdateEntitlement, u.AnomaliesFilters, u.NextUpdateTags, u.LastSeen, u.LastUnusedReminder, u.LastUnusedSlack, u.ID)
	return err
}

// Save saves the User to the database.
func (u *User) Save(db XODB) error {
	if u.Exists() {
		return u.Update(db)
	}

	return u.Insert(db)
}

// Delete deletes the User from the database.
func (u *User) Delete(db XODB) error {
	var err error

	// if doesn't exist, bail
	if !u._exists {
		return nil
	}

	// if deleted, bail
	if u._deleted {
		return nil
	}

	// sql query
	const sqlstr = `DELETE FROM trackit.user WHERE id = ?`

	// run query
	XOLog(sqlstr, u.ID)
	_, err = db.Exec(sqlstr, u.ID)
	if err != nil {
		return err
	}

	// set deleted
	u._deleted = true

	return nil
}

// User returns the User associated with the User's ParentUserID (parent_user_id).
//
// Generated from foreign key 'parent_user'.
func (u *User) User(db XODB) (*User, error) {
	return UserByID(db, int(u.ParentUserID.Int64))
}

// UsersByParentUserID retrieves a row from 'trackit.user' as a User.
//
// Generated from index 'parent_user'.
func UsersByParentUserID(db XODB, parentUserID sql.NullInt64) ([]*User, error) {
	var err error

	// sql query
	const sqlstr = `SELECT ` +
		`id, created, modified, email, auth, next_external, parent_user_id, aws_customer_identifier, aws_customer_entitlement, next_update_entitlement, anomalies_filters, next_update_tags, last_seen, last_unused_reminder, last_unused_slack ` +
		`FROM trackit.user ` +
		`WHERE parent_user_id = ?`

	// run query
	XOLog(sqlstr, parentUserID)
	q, err := db.Query(sqlstr, parentUserID)
	if err != nil {
		return nil, err
	}
	defer q.Close()

	// load results
	res := []*User{}
	for q.Next() {
		u := User{
			_exists: true,
		}

		// scan
		err = q.Scan(&u.ID, &u.Created, &u.Modified, &u.Email, &u.Auth, &u.NextExternal, &u.ParentUserID, &u.AwsCustomerIdentifier, &u.AwsCustomerEntitlement, &u.NextUpdateEntitlement, &u.AnomaliesFilters, &u.NextUpdateTags, &u.LastSeen, &u.LastUnusedReminder, &u.LastUnusedSlack)
		if err != nil {
			return nil, err
		}

		res = append(res, &u)
	}

	return res, nil
}

// UserByEmail retrieves a row from 'trackit.user' as a User.
//
// Generated from index 'unique_email'.
func UserByEmail(db XODB, email string) (*User, error) {
	var err error

	// sql query
	const sqlstr = `SELECT ` +
		`id, created, modified, email, auth, next_external, parent_user_id, aws_customer_identifier, aws_customer_entitlement, next_update_entitlement, anomalies_filters, next_update_tags, last_seen, last_unused_reminder, last_unused_slack ` +
		`FROM trackit.user ` +
		`WHERE email = ?`

	// run query
	XOLog(sqlstr, email)
	u := User{
		_exists: true,
	}

	err = db.QueryRow(sqlstr, email).Scan(&u.ID, &u.Created, &u.Modified, &u.Email, &u.Auth, &u.NextExternal, &u.ParentUserID, &u.AwsCustomerIdentifier, &u.AwsCustomerEntitlement, &u.NextUpdateEntitlement, &u.AnomaliesFilters, &u.NextUpdateTags, &u.LastSeen, &u.LastUnusedReminder, &u.LastUnusedSlack)
	if err != nil {
		return nil, err
	}

	return &u, nil
}

// UserByID retrieves a row from 'trackit.user' as a User.
//
// Generated from index 'user_id_pkey'.
func UserByID(db XODB, id int) (*User, error) {
	var err error

	// sql query
	const sqlstr = `SELECT ` +
		`id, created, modified, email, auth, next_external, parent_user_id, aws_customer_identifier, aws_customer_entitlement, next_update_entitlement, anomalies_filters, next_update_tags, last_seen, last_unused_reminder, last_unused_slack ` +
		`FROM trackit.user ` +
		`WHERE id = ?`

	// run query
	XOLog(sqlstr, id)
	u := User{
		_exists: true,
	}

	err = db.QueryRow(sqlstr, id).Scan(&u.ID, &u.Created, &u.Modified, &u.Email, &u.Auth, &u.NextExternal, &u.ParentUserID, &u.AwsCustomerIdentifier, &u.AwsCustomerEntitlement, &u.NextUpdateEntitlement, &u.AnomaliesFilters, &u.NextUpdateTags, &u.LastSeen, &u.LastUnusedReminder, &u.LastUnusedSlack)
	if err != nil {
		return nil, err
	}

	return &u, nil
}
<|endoftext|>


<commit_before>package handlers

import (
	"container/heap"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"strings"
	"time"

	"github.com/boltdb/bolt"
	"github.com/gojp/goreportcard/download"
)

const (
	// DBPath is the relative (or absolute) path to the bolt database file
	DBPath string = "goreportcard.db"

	// RepoBucket is the bucket in which repos will be cached in the bolt DB
	RepoBucket string = "repos"

	// MetaBucket is the bucket containing the names of the projects with the
	// top 100 high scores, and other meta information
	MetaBucket string = "meta"
)

// CheckHandler handles the request for checking a repo
func CheckHandler(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")

	repo, err := download.Clean(r.FormValue("repo"))
	if err != nil {
		log.Println("ERROR: from download.Clean:", err)
		w.WriteHeader(http.StatusBadRequest)
		w.Write([]byte(`Could not download the repository: ` + err.Error()))
		return
	}

	log.Printf("Checking repo %q...", repo)

	forceRefresh := r.Method != "GET" // if this is a GET request, try to fetch from cached version in boltdb first
	resp, err := newChecksResp(repo, forceRefresh)
	if err != nil {
		log.Println("ERROR: from newChecksResp:", err)
		w.WriteHeader(http.StatusBadRequest)
		w.Write([]byte(`Could not download the repository.`))
		return
	}

	respBytes, err := json.Marshal(resp)
	if err != nil {
		log.Println("ERROR: could not marshal json:", err)
		http.Error(w, err.Error(), 500)
		return
	}

	// write to boltdb
	db, err := bolt.Open(DBPath, 0755, &bolt.Options{Timeout: 1 * time.Second})
	if err != nil {
		log.Println("Failed to open bolt database: ", err)
		http.Error(w, err.Error(), 500)
		return
	}
	defer db.Close()

	// is this a new repo? if so, increase the count in the high scores bucket later
	isNewRepo := false
	var oldRepoBytes []byte
	err = db.View(func(tx *bolt.Tx) error {
		b := tx.Bucket([]byte(RepoBucket))
		if b == nil {
			return fmt.Errorf("repo bucket not found")
		}
		oldRepoBytes = b.Get([]byte(repo))
		return nil
	})
	if err != nil {
		log.Println(err)
	}

	// if this is a new repo, or the user force-refreshed, update the cache
	if isNewRepo || forceRefresh {
		err = db.Update(func(tx *bolt.Tx) error {
			log.Printf("Saving repo %q to cache...", repo)

			b := tx.Bucket([]byte(RepoBucket))
			if b == nil {
				return fmt.Errorf("repo bucket not found")
			}

			// save repo to cache
			err = b.Put([]byte(repo), respBytes)
			if err != nil {
				return err
			}
			return updateMetadata(tx, resp, repo, isNewRepo)
		})

		if err != nil {
			log.Println("Bolt writing error:", err)
		}

	}

	db.Update(func(tx *bolt.Tx) error {
		// fetch meta-bucket
		mb := tx.Bucket([]byte(MetaBucket))
		return updateRecentlyViewed(mb, repo)
	})

	b, err := json.Marshal(map[string]string{"redirect": "/report/" + repo})
	if err != nil {
		log.Println("JSON marshal error:", err)
	}
	w.WriteHeader(http.StatusOK)
	w.Write(b)
	return
}

func updateHighScores(mb *bolt.Bucket, resp checksResp, repo string) error {
	// check if we need to update the high score list
	if resp.Files < 100 {
		// only repos with >= 100 files are considered for the high score list
		return nil
	}

	// start updating high score list
	scoreBytes := mb.Get([]byte("scores"))
	if scoreBytes == nil {
		scoreBytes, _ = json.Marshal([]ScoreHeap{})
	}
	scores := &ScoreHeap{}
	json.Unmarshal(scoreBytes, scores)

	heap.Init(scores)
	if len(*scores) > 0 && (*scores)[0].Score > resp.Average*100.0 && len(*scores) == 50 {
		// lowest score on list is higher than this repo's score, so no need to add, unless
		// we do not have 50 high scores yet
		return nil
	}
	// if this repo is already in the list, remove the original entry:
	for i := range *scores {
		if strings.ToLower((*scores)[i].Repo) == strings.ToLower(repo) {
			heap.Remove(scores, i)
			break
		}
	}
	// now we can safely push it onto the heap
	heap.Push(scores, scoreItem{
		Repo:  repo,
		Score: resp.Average * 100.0,
		Files: resp.Files,
	})
	if len(*scores) > 50 {
		// trim heap if it's grown to over 50
		*scores = (*scores)[1:51]
	}
	scoreBytes, err := json.Marshal(&scores)
	if err != nil {
		return err
	}
	err = mb.Put([]byte("scores"), scoreBytes)
	if err != nil {
		return err
	}

	return nil
}

func updateReposCount(mb *bolt.Bucket, repo string) (err error) {
	log.Printf("New repo %q, adding to repo count...", repo)
	totalInt := 0
	total := mb.Get([]byte("total_repos"))
	if total != nil {
		err = json.Unmarshal(total, &totalInt)
		if err != nil {
			return fmt.Errorf("could not unmarshal total repos count: %v", err)
		}
	}
	totalInt++ // increase repo count
	total, err = json.Marshal(totalInt)
	if err != nil {
		return fmt.Errorf("could not marshal total repos count: %v", err)
	}
	mb.Put([]byte("total_repos"), total)
	log.Println("Repo count is now", totalInt)
	return nil
}

type recentItem struct {
	Repo string
}

func updateRecentlyViewed(mb *bolt.Bucket, repo string) error {
	if mb == nil {
		return fmt.Errorf("meta bucket not found")
	}
	b := mb.Get([]byte("recent"))
	if b == nil {
		b, _ = json.Marshal([]recentItem{})
	}
	recent := []recentItem{}
	json.Unmarshal(b, &recent)

	// add it to the slice, if it is not in there already
	for i := range recent {
		if recent[i].Repo == repo {
			return nil
		}
	}

	recent = append(recent, recentItem{Repo: repo})
	if len(recent) > 5 {
		// trim recent if it's grown to over 5
		recent = (recent)[1:6]
	}
	b, err := json.Marshal(&recent)
	if err != nil {
		return err
	}
	err = mb.Put([]byte("recent"), b)
	if err != nil {
		return err
	}

	return nil
}

func updateMetadata(tx *bolt.Tx, resp checksResp, repo string, isNewRepo bool) error {
	// fetch meta-bucket
	mb := tx.Bucket([]byte(MetaBucket))
	if mb == nil {
		return fmt.Errorf("high score bucket not found")
	}
	// update total repos count
	if isNewRepo {
		err := updateReposCount(mb, repo)
		if err != nil {
			return err
		}
	}

	return updateHighScores(mb, resp, repo)
}
<commit_msg>add stats back<commit_after>package handlers

import (
	"container/heap"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"strings"
	"time"

	"github.com/boltdb/bolt"
	"github.com/gojp/goreportcard/download"
)

const (
	// DBPath is the relative (or absolute) path to the bolt database file
	DBPath string = "goreportcard.db"

	// RepoBucket is the bucket in which repos will be cached in the bolt DB
	RepoBucket string = "repos"

	// MetaBucket is the bucket containing the names of the projects with the
	// top 100 high scores, and other meta information
	MetaBucket string = "meta"
)

// CheckHandler handles the request for checking a repo
func CheckHandler(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")

	repo, err := download.Clean(r.FormValue("repo"))
	if err != nil {
		log.Println("ERROR: from download.Clean:", err)
		w.WriteHeader(http.StatusBadRequest)
		w.Write([]byte(`Could not download the repository: ` + err.Error()))
		return
	}

	log.Printf("Checking repo %q...", repo)

	forceRefresh := r.Method != "GET" // if this is a GET request, try to fetch from cached version in boltdb first
	resp, err := newChecksResp(repo, forceRefresh)
	if err != nil {
		log.Println("ERROR: from newChecksResp:", err)
		w.WriteHeader(http.StatusBadRequest)
		w.Write([]byte(`Could not download the repository.`))
		return
	}

	respBytes, err := json.Marshal(resp)
	if err != nil {
		log.Println("ERROR: could not marshal json:", err)
		http.Error(w, err.Error(), 500)
		return
	}

	// write to boltdb
	db, err := bolt.Open(DBPath, 0755, &bolt.Options{Timeout: 1 * time.Second})
	if err != nil {
		log.Println("Failed to open bolt database: ", err)
		http.Error(w, err.Error(), 500)
		return
	}
	defer db.Close()

	// is this a new repo? if so, increase the count in the high scores bucket later
	isNewRepo := false
	var oldRepoBytes []byte
	err = db.View(func(tx *bolt.Tx) error {
		b := tx.Bucket([]byte(RepoBucket))
		if b == nil {
			return fmt.Errorf("repo bucket not found")
		}
		oldRepoBytes = b.Get([]byte(repo))
		return nil
	})
	if err != nil {
		log.Println("ERROR getting repo from repo bucket:", err)
	}

	// get the old score and store it for stats updating
	var oldScore *float64
	if isNewRepo = oldRepoBytes == nil; !isNewRepo {
		oldRepo := checksResp{}
		err = json.Unmarshal(oldRepoBytes, &oldRepo)
		if err != nil {
			log.Println("ERROR: could not unmarshal json:", err)
			http.Error(w, err.Error(), 500)
			return
		}
		oldScore = &oldRepo.Average
	}

	// if this is a new repo, or the user force-refreshed, update the cache
	if isNewRepo || forceRefresh {
		err = db.Update(func(tx *bolt.Tx) error {
			log.Printf("Saving repo %q to cache...", repo)

			b := tx.Bucket([]byte(RepoBucket))
			if b == nil {
				return fmt.Errorf("repo bucket not found")
			}

			// save repo to cache
			err = b.Put([]byte(repo), respBytes)
			if err != nil {
				return err
			}
			return updateMetadata(tx, resp, repo, isNewRepo, oldScore)
		})

		if err != nil {
			log.Println("Bolt writing error:", err)
		}

	}

	db.Update(func(tx *bolt.Tx) error {
		// fetch meta-bucket
		mb := tx.Bucket([]byte(MetaBucket))
		return updateRecentlyViewed(mb, repo)
	})

	b, err := json.Marshal(map[string]string{"redirect": "/report/" + repo})
	if err != nil {
		log.Println("JSON marshal error:", err)
	}
	w.WriteHeader(http.StatusOK)
	w.Write(b)
	return
}

func updateHighScores(mb *bolt.Bucket, resp checksResp, repo string) error {
	// check if we need to update the high score list
	if resp.Files < 100 {
		// only repos with >= 100 files are considered for the high score list
		return nil
	}

	// start updating high score list
	scoreBytes := mb.Get([]byte("scores"))
	if scoreBytes == nil {
		scoreBytes, _ = json.Marshal([]ScoreHeap{})
	}
	scores := &ScoreHeap{}
	json.Unmarshal(scoreBytes, scores)

	heap.Init(scores)
	if len(*scores) > 0 && (*scores)[0].Score > resp.Average*100.0 && len(*scores) == 50 {
		// lowest score on list is higher than this repo's score, so no need to add, unless
		// we do not have 50 high scores yet
		return nil
	}
	// if this repo is already in the list, remove the original entry:
	for i := range *scores {
		if strings.ToLower((*scores)[i].Repo) == strings.ToLower(repo) {
			heap.Remove(scores, i)
			break
		}
	}
	// now we can safely push it onto the heap
	heap.Push(scores, scoreItem{
		Repo:  repo,
		Score: resp.Average * 100.0,
		Files: resp.Files,
	})
	if len(*scores) > 50 {
		// trim heap if it's grown to over 50
		*scores = (*scores)[1:51]
	}
	scoreBytes, err := json.Marshal(&scores)
	if err != nil {
		return err
	}
	err = mb.Put([]byte("scores"), scoreBytes)
	if err != nil {
		return err
	}

	return nil
}

func updateReposCount(mb *bolt.Bucket, repo string) (err error) {
	log.Printf("New repo %q, adding to repo count...", repo)
	totalInt := 0
	total := mb.Get([]byte("total_repos"))
	if total != nil {
		err = json.Unmarshal(total, &totalInt)
		if err != nil {
			return fmt.Errorf("could not unmarshal total repos count: %v", err)
		}
	}
	totalInt++ // increase repo count
	total, err = json.Marshal(totalInt)
	if err != nil {
		return fmt.Errorf("could not marshal total repos count: %v", err)
	}
	mb.Put([]byte("total_repos"), total)
	log.Println("Repo count is now", totalInt)
	return nil
}

type recentItem struct {
	Repo string
}

func updateRecentlyViewed(mb *bolt.Bucket, repo string) error {
	if mb == nil {
		return fmt.Errorf("meta bucket not found")
	}
	b := mb.Get([]byte("recent"))
	if b == nil {
		b, _ = json.Marshal([]recentItem{})
	}
	recent := []recentItem{}
	json.Unmarshal(b, &recent)

	// add it to the slice, if it is not in there already
	for i := range recent {
		if recent[i].Repo == repo {
			return nil
		}
	}

	recent = append(recent, recentItem{Repo: repo})
	if len(recent) > 5 {
		// trim recent if it's grown to over 5
		recent = (recent)[1:6]
	}
	b, err := json.Marshal(&recent)
	if err != nil {
		return err
	}
	err = mb.Put([]byte("recent"), b)
	if err != nil {
		return err
	}

	return nil
}

func updateStats(mb *bolt.Bucket, resp checksResp, oldScore *float64) error {
	scores := make([]int, 101, 101)
	statsBytes := mb.Get([]byte("stats"))
	if statsBytes == nil {
		statsBytes, _ = json.Marshal(scores)
	}
	err := json.Unmarshal(statsBytes, &scores)
	if err != nil {
		return err
	}
	scores[int(resp.Average*100)]++
	if oldScore != nil {
		scores[int(*oldScore*100)]--
	}
	newStats, err := json.Marshal(scores)
	if err != nil {
		return err
	}
	err = mb.Put([]byte("stats"), newStats)
	if err != nil {
		return err
	}
	return nil
}

func updateMetadata(tx *bolt.Tx, resp checksResp, repo string, isNewRepo bool, oldScore *float64) error {
	// fetch meta-bucket
	mb := tx.Bucket([]byte(MetaBucket))
	if mb == nil {
		return fmt.Errorf("high score bucket not found")
	}
	// update total repos count
	if isNewRepo {
		err := updateReposCount(mb, repo)
		if err != nil {
			return err
		}
	}

	err := updateHighScores(mb, resp, repo)
	if err != nil {
		return err
	}

	return updateStats(mb, resp, oldScore)
}
<|endoftext|>