<commit_before>/*
 * This file is part of the KubeVirt project
 *
 * 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.
 *
 * Copyright 2018 Red Hat, Inc.
 *
 */

package tests_test

import (
	"context"
	"fmt"
	"time"

	"kubevirt.io/kubevirt/pkg/hooks"
	"kubevirt.io/kubevirt/tests/util"

	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
	k8sv1 "k8s.io/api/core/v1"

	v1 "kubevirt.io/api/core/v1"
	"kubevirt.io/client-go/kubecli"
	hooksv1alpha1 "kubevirt.io/kubevirt/pkg/hooks/v1alpha1"
	hooksv1alpha2 "kubevirt.io/kubevirt/pkg/hooks/v1alpha2"
	virtconfig "kubevirt.io/kubevirt/pkg/virt-config"
	"kubevirt.io/kubevirt/tests"
	cd "kubevirt.io/kubevirt/tests/containerdisk"
	"kubevirt.io/kubevirt/tests/flags"
)

const (
	hookSidecarImage     = "example-hook-sidecar"
	sidecarContainerName = "hook-sidecar-0"
)

var _ = Describe("[sig-compute]HookSidecars", func() {

	var err error
	var virtClient kubecli.KubevirtClient

	var vmi *v1.VirtualMachineInstance

	BeforeEach(func() {
		virtClient, err = kubecli.GetKubevirtClient()
		util.PanicOnError(err)

		tests.BeforeTestCleanup()
		vmi = tests.NewRandomVMIWithEphemeralDisk(cd.ContainerDiskFor(cd.ContainerDiskAlpine))
		vmi.ObjectMeta.Annotations = RenderSidecar(hooksv1alpha1.Version)
	})

	Describe("[rfe_id:2667][crit:medium][vendor:cnv-qe@redhat.com][level:component] VMI definition", func() {
		getVMIPod := func(vmi *v1.VirtualMachineInstance) (*k8sv1.Pod, error) {
			podSelector := tests.UnfinishedVMIPodSelector(vmi)
			vmiPods, err := virtClient.CoreV1().Pods(vmi.GetNamespace()).List(context.Background(), podSelector)

			if err != nil || len(vmiPods.Items) != 1 {
				return nil, fmt.Errorf("could not retrieve the VMI pod: %v", err)
			}
			return &vmiPods.Items[0], nil
		}

		Context("with SM BIOS hook sidecar", func() {
			It("[test_id:3155]should successfully start with hook sidecar annotation", func() {
				By("Starting a VMI")
				vmi, err = virtClient.VirtualMachineInstance(util.NamespaceTestDefault).Create(vmi)
				Expect(err).ToNot(HaveOccurred())
				tests.WaitForSuccessfulVMIStart(vmi)
			}, 300)

			It("[test_id:3156]should successfully start with hook sidecar annotation for v1alpha2", func() {
				By("Starting a VMI")
				vmi, err = virtClient.VirtualMachineInstance(util.NamespaceTestDefault).Create(vmi)
				vmi.ObjectMeta.Annotations = RenderSidecar(hooksv1alpha2.Version)
				Expect(err).ToNot(HaveOccurred())
				tests.WaitForSuccessfulVMIStart(vmi)
			}, 300)

			It("[test_id:3157]should call Collect and OnDefineDomain on the hook sidecar", func() {
				By("Getting hook-sidecar logs")
				vmi, err = virtClient.VirtualMachineInstance(util.NamespaceTestDefault).Create(vmi)
				Expect(err).ToNot(HaveOccurred())
				logs := func() string { return getHookSidecarLogs(virtClient, vmi) }
				tests.WaitForSuccessfulVMIStart(vmi)
				Eventually(logs,
					11*time.Second,
					500*time.Millisecond).
					Should(ContainSubstring("Hook's Info method has been called"))
				Eventually(logs,
					11*time.Second,
					500*time.Millisecond).
					Should(ContainSubstring("Hook's OnDefineDomain callback method has been called"))
			}, 300)

			It("[test_id:3158]should update domain XML with SM BIOS properties", func() {
				By("Reading domain XML using virsh")
				tests.SkipIfNoCmd("kubectl")
				vmi, err = virtClient.VirtualMachineInstance(util.NamespaceTestDefault).Create(vmi)
				tests.WaitForSuccessfulVMIStart(vmi)
				domainXml, err := tests.GetRunningVirtualMachineInstanceDomainXML(virtClient, vmi)
				Expect(err).NotTo(HaveOccurred())
				Expect(domainXml).Should(ContainSubstring("<sysinfo type='smbios'>"))
				Expect(domainXml).Should(ContainSubstring("<smbios mode='sysinfo'/>"))
				Expect(domainXml).Should(ContainSubstring("<entry name='manufacturer'>Radical Edward</entry>"))
			}, 300)

			It("should not start with hook sidecar annotation when the version is not provided", func() {
				By("Starting a VMI")
				vmi.ObjectMeta.Annotations = RenderInvalidSMBiosSidecar()
				vmi, err = virtClient.VirtualMachineInstance(util.NamespaceTestDefault).Create(vmi)
				Expect(err).NotTo(HaveOccurred(), "the request to create the VMI should be accepted")

				Eventually(func() bool {
					vmiPod, err := getVMIPod(vmi)
					Expect(err).NotTo(HaveOccurred(), "must be able to retrieve the VMI virt-launcher pod")

					for _, container := range vmiPod.Status.ContainerStatuses {
						if container.Name == sidecarContainerName && container.State.Terminated != nil {
							terminated := container.State.Terminated
							return terminated.ExitCode != 0 && terminated.Reason == "Error"
						}
					}
					return false
				}, 15*time.Second, time.Second).Should(
					BeTrue(),
					fmt.Sprintf("the %s container must fail if it was not provided the hook version to advertise itself", sidecarContainerName))
			}, 30)
		})

		Context("[Serial]with sidecar feature gate disabled", func() {
			BeforeEach(func() {
				tests.DisableFeatureGate(virtconfig.SidecarGate)
			})

			It("[test_id:2666]should not start with hook sidecar annotation", func() {
				By("Starting a VMI")
				vmi, err = virtClient.VirtualMachineInstance(util.NamespaceTestDefault).Create(vmi)
				Expect(err).To(HaveOccurred(), "should not create a VMI without sidecar feature gate")
				Expect(err.Error()).Should(ContainSubstring(fmt.Sprintf("invalid entry metadata.annotations.%s", hooks.HookSidecarListAnnotationName)))
			}, 30)
		})
	})
})

func getHookSidecarLogs(virtCli kubecli.KubevirtClient, vmi *v1.VirtualMachineInstance) string {
	namespace := vmi.GetObjectMeta().GetNamespace()
	podName := tests.GetVmPodName(virtCli, vmi)

	var tailLines int64 = 100
	logsRaw, err := virtCli.CoreV1().
		Pods(namespace).
		GetLogs(podName, &k8sv1.PodLogOptions{
			TailLines: &tailLines,
			Container: sidecarContainerName,
		}).
		DoRaw(context.Background())
	Expect(err).To(BeNil())

	return string(logsRaw)
}

func RenderSidecar(version string) map[string]string {
	return map[string]string{
		"hooks.kubevirt.io/hookSidecars":              fmt.Sprintf(`[{"args": ["--version", "%s"],"image": "%s/%s:%s", "imagePullPolicy": "IfNotPresent"}]`, version, flags.KubeVirtUtilityRepoPrefix, hookSidecarImage, flags.KubeVirtUtilityVersionTag),
		"smbios.vm.kubevirt.io/baseBoardManufacturer": "Radical Edward",
	}
}

func RenderInvalidSMBiosSidecar() map[string]string {
	return map[string]string{
		"hooks.kubevirt.io/hookSidecars":              fmt.Sprintf(`[{"image": "%s/%s:%s", "imagePullPolicy": "IfNotPresent"}]`, flags.KubeVirtUtilityRepoPrefix, hookSidecarImage, flags.KubeVirtUtilityVersionTag),
		"smbios.vm.kubevirt.io/baseBoardManufacturer": "Radical Edward",
	}
}
<commit_msg>Fix a flake in the hook sidecar tests<commit_after>/*
 * This file is part of the KubeVirt project
 *
 * 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.
 *
 * Copyright 2018 Red Hat, Inc.
 *
 */

package tests_test

import (
	"context"
	"fmt"
	"time"

	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"

	k8sv1 "k8s.io/api/core/v1"

	v1 "kubevirt.io/api/core/v1"
	"kubevirt.io/client-go/kubecli"
	"kubevirt.io/kubevirt/pkg/hooks"
	hooksv1alpha1 "kubevirt.io/kubevirt/pkg/hooks/v1alpha1"
	hooksv1alpha2 "kubevirt.io/kubevirt/pkg/hooks/v1alpha2"
	virtconfig "kubevirt.io/kubevirt/pkg/virt-config"
	"kubevirt.io/kubevirt/tests"
	cd "kubevirt.io/kubevirt/tests/containerdisk"
	"kubevirt.io/kubevirt/tests/flags"
	"kubevirt.io/kubevirt/tests/util"
)

const (
	hookSidecarImage     = "example-hook-sidecar"
	sidecarContainerName = "hook-sidecar-0"
)

var _ = Describe("[sig-compute]HookSidecars", func() {

	var err error
	var virtClient kubecli.KubevirtClient

	var vmi *v1.VirtualMachineInstance

	BeforeEach(func() {
		virtClient, err = kubecli.GetKubevirtClient()
		util.PanicOnError(err)

		tests.BeforeTestCleanup()
		vmi = tests.NewRandomVMIWithEphemeralDisk(cd.ContainerDiskFor(cd.ContainerDiskAlpine))
		vmi.ObjectMeta.Annotations = RenderSidecar(hooksv1alpha1.Version)
	})

	Describe("[rfe_id:2667][crit:medium][vendor:cnv-qe@redhat.com][level:component] VMI definition", func() {
		getVMIPod := func(vmi *v1.VirtualMachineInstance) (*k8sv1.Pod, bool, error) {
			podSelector := tests.UnfinishedVMIPodSelector(vmi)
			vmiPods, err := virtClient.CoreV1().Pods(vmi.GetNamespace()).List(context.Background(), podSelector)

			if err != nil {
				return nil, false, fmt.Errorf("could not retrieve the VMI pod: %v", err)
			} else if len(vmiPods.Items) == 0 {
				return nil, false, nil
			}
			return &vmiPods.Items[0], true, nil
		}

		Context("with SM BIOS hook sidecar", func() {
			It("[test_id:3155]should successfully start with hook sidecar annotation", func() {
				By("Starting a VMI")
				vmi, err = virtClient.VirtualMachineInstance(util.NamespaceTestDefault).Create(vmi)
				Expect(err).ToNot(HaveOccurred())
				tests.WaitForSuccessfulVMIStart(vmi)
			}, 300)

			It("[test_id:3156]should successfully start with hook sidecar annotation for v1alpha2", func() {
				By("Starting a VMI")
				vmi, err = virtClient.VirtualMachineInstance(util.NamespaceTestDefault).Create(vmi)
				vmi.ObjectMeta.Annotations = RenderSidecar(hooksv1alpha2.Version)
				Expect(err).ToNot(HaveOccurred())
				tests.WaitForSuccessfulVMIStart(vmi)
			}, 300)

			It("[test_id:3157]should call Collect and OnDefineDomain on the hook sidecar", func() {
				By("Getting hook-sidecar logs")
				vmi, err = virtClient.VirtualMachineInstance(util.NamespaceTestDefault).Create(vmi)
				Expect(err).ToNot(HaveOccurred())
				logs := func() string { return getHookSidecarLogs(virtClient, vmi) }
				tests.WaitForSuccessfulVMIStart(vmi)
				Eventually(logs,
					11*time.Second,
					500*time.Millisecond).
					Should(ContainSubstring("Hook's Info method has been called"))
				Eventually(logs,
					11*time.Second,
					500*time.Millisecond).
					Should(ContainSubstring("Hook's OnDefineDomain callback method has been called"))
			}, 300)

			It("[test_id:3158]should update domain XML with SM BIOS properties", func() {
				By("Reading domain XML using virsh")
				tests.SkipIfNoCmd("kubectl")
				vmi, err = virtClient.VirtualMachineInstance(util.NamespaceTestDefault).Create(vmi)
				tests.WaitForSuccessfulVMIStart(vmi)
				domainXml, err := tests.GetRunningVirtualMachineInstanceDomainXML(virtClient, vmi)
				Expect(err).NotTo(HaveOccurred())
				Expect(domainXml).Should(ContainSubstring("<sysinfo type='smbios'>"))
				Expect(domainXml).Should(ContainSubstring("<smbios mode='sysinfo'/>"))
				Expect(domainXml).Should(ContainSubstring("<entry name='manufacturer'>Radical Edward</entry>"))
			}, 300)

			It("should not start with hook sidecar annotation when the version is not provided", func() {
				By("Starting a VMI")
				vmi.ObjectMeta.Annotations = RenderInvalidSMBiosSidecar()
				vmi, err = virtClient.VirtualMachineInstance(util.NamespaceTestDefault).Create(vmi)
				Expect(err).NotTo(HaveOccurred(), "the request to create the VMI should be accepted")

				Eventually(func() bool {
					vmiPod, exists, err := getVMIPod(vmi)
					if err != nil {
						Expect(err).NotTo(HaveOccurred(), "must be able to retrieve the VMI virt-launcher pod")
					} else if !exists {
						return false
					}

					for _, container := range vmiPod.Status.ContainerStatuses {
						if container.Name == sidecarContainerName && container.State.Terminated != nil {
							terminated := container.State.Terminated
							return terminated.ExitCode != 0 && terminated.Reason == "Error"
						}
					}
					return false
				}, 15*time.Second, time.Second).Should(
					BeTrue(),
					fmt.Sprintf("the %s container must fail if it was not provided the hook version to advertise itself", sidecarContainerName))
			}, 30)
		})

		Context("[Serial]with sidecar feature gate disabled", func() {
			BeforeEach(func() {
				tests.DisableFeatureGate(virtconfig.SidecarGate)
			})

			It("[test_id:2666]should not start with hook sidecar annotation", func() {
				By("Starting a VMI")
				vmi, err = virtClient.VirtualMachineInstance(util.NamespaceTestDefault).Create(vmi)
				Expect(err).To(HaveOccurred(), "should not create a VMI without sidecar feature gate")
				Expect(err.Error()).Should(ContainSubstring(fmt.Sprintf("invalid entry metadata.annotations.%s", hooks.HookSidecarListAnnotationName)))
			}, 30)
		})
	})
})

func getHookSidecarLogs(virtCli kubecli.KubevirtClient, vmi *v1.VirtualMachineInstance) string {
	namespace := vmi.GetObjectMeta().GetNamespace()
	podName := tests.GetVmPodName(virtCli, vmi)

	var tailLines int64 = 100
	logsRaw, err := virtCli.CoreV1().
		Pods(namespace).
		GetLogs(podName, &k8sv1.PodLogOptions{
			TailLines: &tailLines,
			Container: sidecarContainerName,
		}).
		DoRaw(context.Background())
	Expect(err).To(BeNil())

	return string(logsRaw)
}

func RenderSidecar(version string) map[string]string {
	return map[string]string{
		"hooks.kubevirt.io/hookSidecars":              fmt.Sprintf(`[{"args": ["--version", "%s"],"image": "%s/%s:%s", "imagePullPolicy": "IfNotPresent"}]`, version, flags.KubeVirtUtilityRepoPrefix, hookSidecarImage, flags.KubeVirtUtilityVersionTag),
		"smbios.vm.kubevirt.io/baseBoardManufacturer": "Radical Edward",
	}
}

func RenderInvalidSMBiosSidecar() map[string]string {
	return map[string]string{
		"hooks.kubevirt.io/hookSidecars":              fmt.Sprintf(`[{"image": "%s/%s:%s", "imagePullPolicy": "IfNotPresent"}]`, flags.KubeVirtUtilityRepoPrefix, hookSidecarImage, flags.KubeVirtUtilityVersionTag),
		"smbios.vm.kubevirt.io/baseBoardManufacturer": "Radical Edward",
	}
}
<|endoftext|>


<commit_before>package hetzner

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
	"strconv"
	"strings"
	"time"
)

const (
	baseURL = "https://dns.hetzner.com/api/v1"
)

type hetznerProvider struct {
	apiKey             string
	zones              map[string]zone
	requestRateLimiter requestRateLimiter
}

func checkIsLockedSystemRecord(record record) error {
	if record.Type == "SOA" {
		// The upload of a BIND zone file can change the SOA record.
		// Implementing this edge case this is too complex for now.
		return fmt.Errorf("SOA records are locked in HETZNER zones. They are hence not available for updating")
	}
	return nil
}

func getHomogenousDelay(headers http.Header, quotaName string) (time.Duration, error) {
	quota, err := parseHeaderAsInt(headers, "X-Ratelimit-Limit-"+strings.Title(quotaName))
	if err != nil {
		return 0, err
	}

	var unit time.Duration
	switch quotaName {
	case "hour":
		unit = time.Hour
	case "minute":
		unit = time.Minute
	case "second":
		unit = time.Second
	}

	delay := time.Duration(int64(unit) / quota)
	return delay, nil
}

func getRetryAfterDelay(header http.Header) (time.Duration, error) {
	retryAfter, err := parseHeaderAsInt(header, "Retry-After")
	if err != nil {
		return 0, err
	}
	delay := time.Duration(retryAfter * int64(time.Second))
	return delay, nil
}

func parseHeaderAsInt(headers http.Header, headerName string) (int64, error) {
	value, ok := headers[headerName]
	if !ok {
		return 0, fmt.Errorf("header %q is missing", headerName)
	}
	return strconv.ParseInt(value[0], 10, 0)
}

func (api *hetznerProvider) bulkCreateRecords(records []record) error {
	for _, record := range records {
		if err := checkIsLockedSystemRecord(record); err != nil {
			return err
		}
	}

	request := bulkCreateRecordsRequest{
		Records: records,
	}
	return api.request("/records/bulk", "POST", request, nil)
}

func (api *hetznerProvider) bulkUpdateRecords(records []record) error {
	for _, record := range records {
		if err := checkIsLockedSystemRecord(record); err != nil {
			return err
		}
	}

	request := bulkUpdateRecordsRequest{
		Records: records,
	}
	return api.request("/records/bulk", "PUT", request, nil)
}

func (api *hetznerProvider) createRecord(record record) error {
	if err := checkIsLockedSystemRecord(record); err != nil {
		return err
	}

	request := createRecordRequest{
		Name:   record.Name,
		TTL:    *record.TTL,
		Type:   record.Type,
		Value:  record.Value,
		ZoneID: record.ZoneID,
	}
	return api.request("/records", "POST", request, nil)
}

func (api *hetznerProvider) createZone(name string) error {
	request := createZoneRequest{
		Name: name,
	}
	return api.request("/zones", "POST", request, nil)
}

func (api *hetznerProvider) deleteRecord(record record) error {
	if err := checkIsLockedSystemRecord(record); err != nil {
		return err
	}

	url := fmt.Sprintf("/records/%s", record.ID)
	return api.request(url, "DELETE", nil, nil)
}

func (api *hetznerProvider) getAllRecords(domain string) ([]record, error) {
	zone, err := api.getZone(domain)
	if err != nil {
		return nil, err
	}
	page := 1
	records := make([]record, 0)
	for {
		response := &getAllRecordsResponse{}
		url := fmt.Sprintf("/records?zone_id=%s&per_page=100&page=%d", zone.ID, page)
		if err := api.request(url, "GET", nil, response); err != nil {
			return nil, fmt.Errorf("failed fetching zone records for %q: %w", domain, err)
		}
		for _, record := range response.Records {
			if record.TTL == nil {
				record.TTL = &zone.TTL
			}

			if checkIsLockedSystemRecord(record) != nil {
				// Some records are not available for updating, hide them.
				continue
			}

			records = append(records, record)
		}
		// meta.pagination may not be present. In that case LastPage is 0 and below the current page number.
		if page >= response.Meta.Pagination.LastPage {
			break
		}
		page++
	}
	return records, nil
}

func (api *hetznerProvider) getAllZones() error {
	if api.zones != nil {
		return nil
	}
	zones := map[string]zone{}
	page := 1
	for {
		response := &getAllZonesResponse{}
		url := fmt.Sprintf("/zones?per_page=100&page=%d", page)
		if err := api.request(url, "GET", nil, response); err != nil {
			return fmt.Errorf("failed fetching zones: %w", err)
		}
		for _, zone := range response.Zones {
			zones[zone.Name] = zone
		}
		// meta.pagination may not be present. In that case LastPage is 0 and below the current page number.
		if page >= response.Meta.Pagination.LastPage {
			break
		}
		page++
	}
	api.zones = zones
	return nil
}

func (api *hetznerProvider) getZone(name string) (*zone, error) {
	if err := api.getAllZones(); err != nil {
		return nil, err
	}
	zone, ok := api.zones[name]
	if !ok {
		return nil, fmt.Errorf("%q is not a zone in this HETZNER account", name)
	}
	return &zone, nil
}

func (api *hetznerProvider) request(endpoint string, method string, request interface{}, target interface{}) error {
	for {
		var requestBody io.Reader
		if request != nil {
			requestBodySerialised, err := json.Marshal(request)
			if err != nil {
				return err
			}
			requestBody = bytes.NewBuffer(requestBodySerialised)
		}
		req, err := http.NewRequest(method, baseURL+endpoint, requestBody)
		if err != nil {
			return err
		}
		req.Header.Add("Auth-API-Token", api.apiKey)

		api.requestRateLimiter.beforeRequest()
		resp, err := http.DefaultClient.Do(req)
		api.requestRateLimiter.afterRequest()
		if err != nil {
			return err
		}
		cleanupResponseBody := func() {
			err := resp.Body.Close()
			if err != nil {
				fmt.Printf("failed closing response body: %q\n", err)
			}
		}

		api.requestRateLimiter.handleResponse(*resp)
		// retry the request when rate-limited
		if resp.StatusCode == 429 {
			api.requestRateLimiter.handleRateLimitedRequest()
			cleanupResponseBody()
			continue
		}

		defer cleanupResponseBody()
		if resp.StatusCode != 200 {
			data, _ := ioutil.ReadAll(resp.Body)
			fmt.Println(string(data))
			return fmt.Errorf("bad status code from HETZNER: %d not 200", resp.StatusCode)
		}
		if target == nil {
			return nil
		}
		decoder := json.NewDecoder(resp.Body)
		return decoder.Decode(target)
	}
}

func (api *hetznerProvider) startRateLimited() {
	// _Now_ is the best reference we can get for the last request.
	// Head-On-Head invocations of DNSControl benefit from fewer initial
	//  rate-limited requests.
	api.requestRateLimiter.lastRequest = time.Now()
	// use the default delay until we have had a chance to parse limits.
	api.requestRateLimiter.setDefaultDelay()
}

func (api *hetznerProvider) updateRecord(record record) error {
	if err := checkIsLockedSystemRecord(record); err != nil {
		return err
	}

	url := fmt.Sprintf("/records/%s", record.ID)
	return api.request(url, "PUT", record, nil)
}

type requestRateLimiter struct {
	delay                     time.Duration
	lastRequest               time.Time
	optimizeForRateLimitQuota string
}

func (requestRateLimiter *requestRateLimiter) afterRequest() {
	requestRateLimiter.lastRequest = time.Now()
}

func (requestRateLimiter *requestRateLimiter) beforeRequest() {
	if requestRateLimiter.delay == 0 {
		return
	}
	time.Sleep(time.Until(requestRateLimiter.lastRequest.Add(requestRateLimiter.delay)))
}

func (requestRateLimiter *requestRateLimiter) setDefaultDelay() {
	// default to a rate-limit of 1 req/s -- the next response should update it.
	requestRateLimiter.delay = time.Second
}

func (requestRateLimiter *requestRateLimiter) setOptimizeForRateLimitQuota(quota string) error {
	quotaNormalized := strings.ToLower(quota)
	switch quotaNormalized {
	case "hour", "minute", "second":
		requestRateLimiter.optimizeForRateLimitQuota = quotaNormalized
	case "":
		requestRateLimiter.optimizeForRateLimitQuota = "second"
	default:
		return fmt.Errorf("%q is not a valid quota, expected 'Hour', 'Minute', 'Second' or unset", quota)
	}
	return nil
}

func (requestRateLimiter *requestRateLimiter) handleRateLimitedRequest() {
	message := "Rate-Limited, consider bumping the setting 'optimize_for_rate_limit_quota': %q -> %q"
	switch requestRateLimiter.optimizeForRateLimitQuota {
	case "hour":
		message = "Rate-Limited, you are already using the slowest request rate. Consider contacting the Hetzner Support for raising your quota."
	case "minute":
		message = fmt.Sprintf(message, "Minute", "Hour")
	case "second":
		message = fmt.Sprintf(message, "Second", "Minute")
	}
	fmt.Println(message)
}

func (requestRateLimiter *requestRateLimiter) handleResponse(resp http.Response) {
	homogenousDelay, err := getHomogenousDelay(resp.Header, requestRateLimiter.optimizeForRateLimitQuota)
	if err != nil {
		requestRateLimiter.setDefaultDelay()
		return
	}

	delay := homogenousDelay
	if resp.StatusCode == 429 {
		retryAfterDelay, err := getRetryAfterDelay(resp.Header)
		if err == nil {
			delay = retryAfterDelay
		}
	}
	requestRateLimiter.delay = delay
}
<commit_msg>HETZNER: handle a 404 from listing zones gracefully (#1371)<commit_after>package hetzner

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
	"strconv"
	"strings"
	"time"
)

const (
	baseURL = "https://dns.hetzner.com/api/v1"
)

type hetznerProvider struct {
	apiKey             string
	zones              map[string]zone
	requestRateLimiter requestRateLimiter
}

func checkIsLockedSystemRecord(record record) error {
	if record.Type == "SOA" {
		// The upload of a BIND zone file can change the SOA record.
		// Implementing this edge case this is too complex for now.
		return fmt.Errorf("SOA records are locked in HETZNER zones. They are hence not available for updating")
	}
	return nil
}

func getHomogenousDelay(headers http.Header, quotaName string) (time.Duration, error) {
	quota, err := parseHeaderAsInt(headers, "X-Ratelimit-Limit-"+strings.Title(quotaName))
	if err != nil {
		return 0, err
	}

	var unit time.Duration
	switch quotaName {
	case "hour":
		unit = time.Hour
	case "minute":
		unit = time.Minute
	case "second":
		unit = time.Second
	}

	delay := time.Duration(int64(unit) / quota)
	return delay, nil
}

func getRetryAfterDelay(header http.Header) (time.Duration, error) {
	retryAfter, err := parseHeaderAsInt(header, "Retry-After")
	if err != nil {
		return 0, err
	}
	delay := time.Duration(retryAfter * int64(time.Second))
	return delay, nil
}

func parseHeaderAsInt(headers http.Header, headerName string) (int64, error) {
	value, ok := headers[headerName]
	if !ok {
		return 0, fmt.Errorf("header %q is missing", headerName)
	}
	return strconv.ParseInt(value[0], 10, 0)
}

func (api *hetznerProvider) bulkCreateRecords(records []record) error {
	for _, record := range records {
		if err := checkIsLockedSystemRecord(record); err != nil {
			return err
		}
	}

	request := bulkCreateRecordsRequest{
		Records: records,
	}
	return api.request("/records/bulk", "POST", request, nil, nil)
}

func (api *hetznerProvider) bulkUpdateRecords(records []record) error {
	for _, record := range records {
		if err := checkIsLockedSystemRecord(record); err != nil {
			return err
		}
	}

	request := bulkUpdateRecordsRequest{
		Records: records,
	}
	return api.request("/records/bulk", "PUT", request, nil, nil)
}

func (api *hetznerProvider) createRecord(record record) error {
	if err := checkIsLockedSystemRecord(record); err != nil {
		return err
	}

	request := createRecordRequest{
		Name:   record.Name,
		TTL:    *record.TTL,
		Type:   record.Type,
		Value:  record.Value,
		ZoneID: record.ZoneID,
	}
	return api.request("/records", "POST", request, nil, nil)
}

func (api *hetznerProvider) createZone(name string) error {
	request := createZoneRequest{
		Name: name,
	}
	return api.request("/zones", "POST", request, nil, nil)
}

func (api *hetznerProvider) deleteRecord(record record) error {
	if err := checkIsLockedSystemRecord(record); err != nil {
		return err
	}

	url := fmt.Sprintf("/records/%s", record.ID)
	return api.request(url, "DELETE", nil, nil, nil)
}

func (api *hetznerProvider) getAllRecords(domain string) ([]record, error) {
	zone, err := api.getZone(domain)
	if err != nil {
		return nil, err
	}
	page := 1
	records := make([]record, 0)
	for {
		response := &getAllRecordsResponse{}
		url := fmt.Sprintf("/records?zone_id=%s&per_page=100&page=%d", zone.ID, page)
		if err := api.request(url, "GET", nil, response, nil); err != nil {
			return nil, fmt.Errorf("failed fetching zone records for %q: %w", domain, err)
		}
		for _, record := range response.Records {
			if record.TTL == nil {
				record.TTL = &zone.TTL
			}

			if checkIsLockedSystemRecord(record) != nil {
				// Some records are not available for updating, hide them.
				continue
			}

			records = append(records, record)
		}
		// meta.pagination may not be present. In that case LastPage is 0 and below the current page number.
		if page >= response.Meta.Pagination.LastPage {
			break
		}
		page++
	}
	return records, nil
}

func (api *hetznerProvider) getAllZones() error {
	if api.zones != nil {
		return nil
	}
	zones := map[string]zone{}
	page := 1
	statusOK := func(code int) bool {
		switch code {
		case http.StatusOK:
			return true
		case http.StatusNotFound:
			// Accept a 404 when requesting the first page
			return page == 1
		default:
			return false
		}
	}
	for {
		response := &getAllZonesResponse{}
		url := fmt.Sprintf("/zones?per_page=100&page=%d", page)
		if err := api.request(url, "GET", nil, response, statusOK); err != nil {
			return fmt.Errorf("failed fetching zones: %w", err)
		}
		for _, zone := range response.Zones {
			zones[zone.Name] = zone
		}
		// meta.pagination may not be present. In that case LastPage is 0 and below the current page number.
		if page >= response.Meta.Pagination.LastPage {
			break
		}
		page++
	}
	api.zones = zones
	return nil
}

func (api *hetznerProvider) getZone(name string) (*zone, error) {
	if err := api.getAllZones(); err != nil {
		return nil, err
	}
	zone, ok := api.zones[name]
	if !ok {
		return nil, fmt.Errorf("%q is not a zone in this HETZNER account", name)
	}
	return &zone, nil
}

func (api *hetznerProvider) request(endpoint string, method string, request interface{}, target interface{}, statusOK func(code int) bool) error {
	if statusOK == nil {
		statusOK = func(code int) bool {
			return code == http.StatusOK
		}
	}
	for {
		var requestBody io.Reader
		if request != nil {
			requestBodySerialised, err := json.Marshal(request)
			if err != nil {
				return err
			}
			requestBody = bytes.NewBuffer(requestBodySerialised)
		}
		req, err := http.NewRequest(method, baseURL+endpoint, requestBody)
		if err != nil {
			return err
		}
		req.Header.Add("Auth-API-Token", api.apiKey)

		api.requestRateLimiter.beforeRequest()
		resp, err := http.DefaultClient.Do(req)
		api.requestRateLimiter.afterRequest()
		if err != nil {
			return err
		}
		cleanupResponseBody := func() {
			err := resp.Body.Close()
			if err != nil {
				fmt.Printf("failed closing response body: %q\n", err)
			}
		}

		api.requestRateLimiter.handleResponse(*resp)
		// retry the request when rate-limited
		if resp.StatusCode == 429 {
			api.requestRateLimiter.handleRateLimitedRequest()
			cleanupResponseBody()
			continue
		}

		defer cleanupResponseBody()
		if !statusOK(resp.StatusCode) {
			data, _ := ioutil.ReadAll(resp.Body)
			fmt.Println(string(data))
			return fmt.Errorf("bad status code from HETZNER: %d not 200", resp.StatusCode)
		}
		if target == nil {
			return nil
		}
		decoder := json.NewDecoder(resp.Body)
		return decoder.Decode(target)
	}
}

func (api *hetznerProvider) startRateLimited() {
	// _Now_ is the best reference we can get for the last request.
	// Head-On-Head invocations of DNSControl benefit from fewer initial
	//  rate-limited requests.
	api.requestRateLimiter.lastRequest = time.Now()
	// use the default delay until we have had a chance to parse limits.
	api.requestRateLimiter.setDefaultDelay()
}

func (api *hetznerProvider) updateRecord(record record) error {
	if err := checkIsLockedSystemRecord(record); err != nil {
		return err
	}

	url := fmt.Sprintf("/records/%s", record.ID)
	return api.request(url, "PUT", record, nil, nil)
}

type requestRateLimiter struct {
	delay                     time.Duration
	lastRequest               time.Time
	optimizeForRateLimitQuota string
}

func (requestRateLimiter *requestRateLimiter) afterRequest() {
	requestRateLimiter.lastRequest = time.Now()
}

func (requestRateLimiter *requestRateLimiter) beforeRequest() {
	if requestRateLimiter.delay == 0 {
		return
	}
	time.Sleep(time.Until(requestRateLimiter.lastRequest.Add(requestRateLimiter.delay)))
}

func (requestRateLimiter *requestRateLimiter) setDefaultDelay() {
	// default to a rate-limit of 1 req/s -- the next response should update it.
	requestRateLimiter.delay = time.Second
}

func (requestRateLimiter *requestRateLimiter) setOptimizeForRateLimitQuota(quota string) error {
	quotaNormalized := strings.ToLower(quota)
	switch quotaNormalized {
	case "hour", "minute", "second":
		requestRateLimiter.optimizeForRateLimitQuota = quotaNormalized
	case "":
		requestRateLimiter.optimizeForRateLimitQuota = "second"
	default:
		return fmt.Errorf("%q is not a valid quota, expected 'Hour', 'Minute', 'Second' or unset", quota)
	}
	return nil
}

func (requestRateLimiter *requestRateLimiter) handleRateLimitedRequest() {
	message := "Rate-Limited, consider bumping the setting 'optimize_for_rate_limit_quota': %q -> %q"
	switch requestRateLimiter.optimizeForRateLimitQuota {
	case "hour":
		message = "Rate-Limited, you are already using the slowest request rate. Consider contacting the Hetzner Support for raising your quota."
	case "minute":
		message = fmt.Sprintf(message, "Minute", "Hour")
	case "second":
		message = fmt.Sprintf(message, "Second", "Minute")
	}
	fmt.Println(message)
}

func (requestRateLimiter *requestRateLimiter) handleResponse(resp http.Response) {
	homogenousDelay, err := getHomogenousDelay(resp.Header, requestRateLimiter.optimizeForRateLimitQuota)
	if err != nil {
		requestRateLimiter.setDefaultDelay()
		return
	}

	delay := homogenousDelay
	if resp.StatusCode == 429 {
		retryAfterDelay, err := getRetryAfterDelay(resp.Header)
		if err == nil {
			delay = retryAfterDelay
		}
	}
	requestRateLimiter.delay = delay
}
<|endoftext|>


<commit_before>package gorm

import (
	"reflect"
	"fmt"

	"github.com/jinzhu/gorm"

	db "github.com/theduke/go-dukedb"
)

type Backend struct {
	db.BaseBackend
	Db *gorm.DB

	MigrationHandler *db.MigrationHandler
}

func New(gorm *gorm.DB) *Backend {
	b := Backend{
		Db: gorm,
	}

	b.ModelInfo = make(map[string]*db.ModelInfo)
	b.MigrationHandler = db.NewMigrationHandler(&b)

	b.RegisterModel(&MigrationAttempt{})

	return &b
}

func (b Backend) GetName() string {
	return "gorm"
}

func (b *Backend) SetDebug(d bool) {
	b.Debug = d
	if d {
		b.Db = b.Db.Debug()
	}
}

func (b Backend) Copy() db.Backend {
	copied := Backend{
		Db: b.Db,
	}
	copied.ModelInfo = b.ModelInfo
	copied.SetDebug(b.GetDebug())
	return &copied
}

func (b Backend) CreateCollection(name string) db.DbError {
	info := b.GetModelInfo(name)
	if info == nil {
		return db.Error{
			Code: "unknown_model",
			Message: fmt.Sprintf("Model %v not registered with GORM backend", name),
		}
	}

	if err := b.Db.CreateTable(info.Item).Error; err != nil {
		return db.Error {
			Code: "gorm_create_table_failed",
			Message: err.Error(),
		}
	}

	return nil
}

func (b Backend) DropCollection(name string) db.DbError {
	info := b.GetModelInfo(name)
	if info == nil {
		return db.Error{
			Code: "unknown_model",
			Message: fmt.Sprintf("Model %v not registered with GORM backend", name),
		}
	}

	if err := b.Db.DropTableIfExists(info.Item).Error; err != nil {
		return db.Error {
			Code: "gorm_create_table_failed",
			Message: err.Error(),
		}
	}

	return nil
}

func (b Backend) DropAllCollections() db.DbError {
	for name := range b.ModelInfo {
		if err := b.DropCollection(name); err != nil {
			return err
		}
	}

	return nil
}

func (b *Backend) Q(model string) *db.Query {
	q := db.Q(model)
	q.Backend = b
	return q
}

func filterManyToSql(filters []db.Filter, connector string) (string, []interface{}) {
	sql := "("
	args := make([]interface{}, 0)

	count := len(filters)
	for i := 0; i < count; i++ {
		subSql, subArgs := filterToSql(filters[i])

		sql += subSql
		args = append(args, subArgs...)

		if i < count -1 {
			sql += " " + connector + " "
		}
	}

	sql += ")"

	return sql, args
}

func filterToSql(filter db.Filter) (string, []interface{}) {
	filterType := reflect.TypeOf(filter).Elem().Name()
	filterName := filter.Type()

	sql := ""
	args := make([]interface{}, 0)

	if filterType == "FieldCondition" {
		// fieldCOnditions can easily be handled generically.
		cond := filter.(*db.FieldCondition)

		operator := db.FilterToCondition(filterName)

		sql = cond.Field + " " + operator
		if filterName == "in" {
			sql += " (?)"
		} else {
			sql += " ?"
		}

		args = append(args, cond.Value)

		return sql, args
	}

	if filterName == "and" {
		and := filter.(*db.AndCondition)
		sql, args = filterManyToSql(and.Filters, "AND")
	} else if filterName == "or" {
		or := filter.(*db.OrCondition)
		sql, args = filterManyToSql(or.Filters, "OR")
	} else if filterName == "not" {
		not := filter.(*db.NotCondition)
		sql, args = filterToSql(not.Filter)
		sql = "NOT (" + sql + ")"
	} else {
		panic(fmt.Sprintf("GORM: Unhandled filter type '%v'", filterType))
	}

	return sql, args
}

func (b Backend) buildQuery(q *db.Query) (*gorm.DB, db.DbError) {
	info := b.GetModelInfo(q.Model)
	if info == nil {
		return nil, db.Error{
			Code: "unknown_model",
			Message: fmt.Sprintf("Model '%v' not registered with backend gorm", q.Model),
		}
	}

	gormQ := b.Db

	// Handle filters.
	for _, filter := range q.Filters {
		sql, args := filterToSql(filter)
		gormQ = gormQ.Where(sql, args...)
	}

	// Handle joins.
	/*
	if len(q.Joins) > 0 {
		for _, joinQ := range q.Joins {
			args := make([]interface{}, 0)

			// Join has filters!
			// Construct sql and arguments for gorm.Preload("name", "where", "args"...)
			if len(joinQ.Filters) > 0 {
				sql, queryArgs := filterManyToSql(joinQ.Filters, "AND")
				args = append(args, interface{}(sql))
				args = append(args, queryArgs...)
			}

			if joinQ.JoinField != "" || joinQ.JoinedField != "" {
				panic("gorm_backend_does_not_support_join_keys")
			}

			gormQ = gormQ.Preload(joinQ.JoinTargetField, args...)
		}
	}
	*/

	// Handle field specificiaton.
	if len(q.FieldSpec) > 0 {
		gormQ = gormQ.Select(q.FieldSpec)
	}

	// Ordering.
	if q.Orders != nil {
		sql := ""

		count := len(q.Orders)
		for i := 0; i < count; i++ {
			sql += q.Orders[i].String()
			if i < count - 1 {
				sql += ", "
			}
		}

		gormQ = gormQ.Order(sql)
	}

	// Limit & Offset.

	if q.LimitNum != 0 {
		gormQ = gormQ.Limit(q.LimitNum)
	}
	if q.OffsetNum != 0 {
		gormQ = gormQ.Offset(q.OffsetNum)
	}

	return gormQ, nil
}

func (b *Backend) BuildRelationQuery(q *db.RelationQuery) (*db.Query, db.DbError) {
	return db.BuildRelationQuery(b, nil, q)
}

// Perform a query.
func (b Backend) Query(q *db.Query) ([]db.Model, db.DbError) {
	slice, err := b.NewModelSlice(q.Model)
	if err != nil {
		return nil, err
	}

	gormQ, err := b.buildQuery(q)
	if err != nil {
		return nil, err
	}

	if err := gormQ.Find(slice).Error; err != nil {
		return nil, db.Error{Code: "db_error", Message: err.Error()}
	}

	models := db.InterfaceToModelSlice(slice)

	// Do joins.
	if len(q.Joins) > 0 {
		if err := db.BackendDoJoins(&b, q.Model, models, q.Joins); err != nil {
			return nil, db.Error{
				Code: "join_error",
				Message: err.Error(),
			}
		}
	}

	return models, nil
}

func (b Backend) QueryOne(q *db.Query) (db.Model, db.DbError) {
	res, err := b.Query(q)
	if err != nil {
		return nil, err
	}

	if len(res) == 0 {
		return nil, nil
	}

	m := res[0].(db.Model)
	return m, nil
}

func (b Backend) Count(q *db.Query) (uint64, db.DbError) {
	var count int

	info := b.GetModelInfo(q.Model)
	if info == nil {
		return 0, db.Error{
			Code: "unknown_model",
			Message: fmt.Sprintf("Model %v was not registered with backend gorm", q.Model),
		}
	}

	b.Db.Model(info.Item).Count(&count)
	return uint64(count), nil
}

func (b Backend) Last(q *db.Query) (db.Model, db.DbError) {
	orders := len(q.Orders)
	if orders > 0 {
		for i := 0; i < orders; i++ {
			q.Orders[i].Ascending = !q.Orders[i].Ascending
		}
	} else {
		info := b.GetModelInfo(q.Model)
		q = q.Order(info.GetPkName(), false)
	}

	return b.QueryOne(q.Limit(1))
}

// Find first model with primary key ID.
func (b Backend) FindOne(modelType string, id string) (db.Model, db.DbError) {
	return db.BackendFindOne(&b, modelType, id)
}

func (b Backend) FindBy(modelType, field string, value interface{}) ([]db.Model, db.DbError) {
	return b.Q(modelType).Filter(field, value).Find()
}

func (b Backend) FindOneBy(modelType, field string, value interface{}) (db.Model, db.DbError) {
	return b.Q(modelType).Filter(field, value).First()
}

// Convenience methods.

func (b Backend) Create(m db.Model) db.DbError {
	if err := b.Db.Create(m).Error; err != nil {
		return db.Error{Code: "gorm_error", Message: err.Error()}
	}

	return nil
}

func (b Backend) Update(m db.Model) db.DbError {
	if err := b.Db.Save(m).Error; err != nil {
		return db.Error{Code: "gorm_error", Message: err.Error()}
	}

	return nil
}

func (b Backend) Delete(m db.Model) db.DbError {
	if err := b.Db.Delete(m).Error; err != nil {
		return db.Error{Code: "gorm_error", Message: err.Error()}
	}

	return nil
}

func (b Backend) DeleteMany(q *db.Query) db.DbError {
	gormQ, err := b.buildQuery(q)
	if err != nil {
		return err
	}

	// Existence is checked in buildQuery!
	item := b.GetModelInfo(q.Model).Item
	if err := gormQ.Delete(item).Error; err != nil {
		return db.Error{
			Code: "delete_many gorm_error",
			Message: err.Error(),
		}
	}

	return nil
}

/**
 * M2M
 */

func (b Backend) GetM2MCollection(obj db.Model, name string) (db.M2MCollection, db.	DbError) {
	info := b.GetModelInfo(obj.GetCollection())
	fieldInfo := info.FieldInfo[name]

	items, _ := b.NewModelSlice(fieldInfo.RelationItem.GetCollection())

	assoc := b.Db.Model(obj).Association(name)
	col := M2MCollection{
		Association: assoc,
	}

	if err := assoc.Find(items).Error; err != nil {
		return nil, db.Error{
			Code: "gorm_error",
			Message: err.Error(),
		}
	}

	modelSlice := db.InterfaceToModelSlice(items)
	col.Items = modelSlice

	return &col, nil
}

type M2MCollection struct {
	db.BaseM2MCollection
	Association *gorm.Association
}

func (c *M2MCollection) Add(items ...db.Model) db.DbError {
	if err := c.Association.Append(db.ModelToInterfaceSlice(items)).Error; err != nil {
		return db.Error{
			Code: "gorm_error",
			Message: err.Error(),
		}
	}

	for _, item := range items {
		if !c.Contains(item) {
			c.Items = append(c.Items, item)
		}
	}
	return nil
}

func (c *M2MCollection) Delete(items ...db.Model) db.DbError {
	if err := c.Association.Delete(db.ModelToInterfaceSlice(items)).Error; err != nil {
		return db.Error{
			Code: "gorm_error",
			Message: err.Error(),
		}
	}

	for _, item := range items {
		for index, curItem := range c.Items {
			if curItem.GetID() == item.GetID() {
				c.Items = append(c.Items[:index], c.Items[index+1:]...)
				break
			}
		}
	}
	return nil
}

func (c *M2MCollection) Clear() db.DbError {
	if err := c.Association.Clear().Error; err != nil {
		return db.Error{
			Code: "gorm_error",
			Message: err.Error(),
		}
	}
	c.Items = make([]db.Model, 0)
	return nil
}
<commit_msg>gorm: implemented utils changes and added m2m replace()<commit_after>package gorm

import (
	"reflect"
	"fmt"

	"github.com/jinzhu/gorm"

	db "github.com/theduke/go-dukedb"
)

type Backend struct {
	db.BaseBackend
	Db *gorm.DB

	MigrationHandler *db.MigrationHandler
}

func New(gorm *gorm.DB) *Backend {
	b := Backend{
		Db: gorm,
	}

	b.ModelInfo = make(map[string]*db.ModelInfo)
	b.MigrationHandler = db.NewMigrationHandler(&b)

	b.RegisterModel(&MigrationAttempt{})

	return &b
}

func (b Backend) GetName() string {
	return "gorm"
}

func (b *Backend) SetDebug(d bool) {
	b.Debug = d
	if d {
		b.Db = b.Db.Debug()
	}
}

func (b Backend) Copy() db.Backend {
	copied := Backend{
		Db: b.Db,
	}
	copied.ModelInfo = b.ModelInfo
	copied.SetDebug(b.GetDebug())
	return &copied
}

func (b Backend) CreateCollection(name string) db.DbError {
	info := b.GetModelInfo(name)
	if info == nil {
		return db.Error{
			Code: "unknown_model",
			Message: fmt.Sprintf("Model %v not registered with GORM backend", name),
		}
	}

	if err := b.Db.CreateTable(info.Item).Error; err != nil {
		return db.Error {
			Code: "gorm_create_table_failed",
			Message: err.Error(),
		}
	}

	return nil
}

func (b Backend) DropCollection(name string) db.DbError {
	info := b.GetModelInfo(name)
	if info == nil {
		return db.Error{
			Code: "unknown_model",
			Message: fmt.Sprintf("Model %v not registered with GORM backend", name),
		}
	}

	if err := b.Db.DropTableIfExists(info.Item).Error; err != nil {
		return db.Error {
			Code: "gorm_create_table_failed",
			Message: err.Error(),
		}
	}

	return nil
}

func (b Backend) DropAllCollections() db.DbError {
	for name := range b.ModelInfo {
		if err := b.DropCollection(name); err != nil {
			return err
		}
	}

	return nil
}

func (b *Backend) Q(model string) *db.Query {
	q := db.Q(model)
	q.Backend = b
	return q
}

func filterManyToSql(filters []db.Filter, connector string) (string, []interface{}) {
	sql := "("
	args := make([]interface{}, 0)

	count := len(filters)
	for i := 0; i < count; i++ {
		subSql, subArgs := filterToSql(filters[i])

		sql += subSql
		args = append(args, subArgs...)

		if i < count -1 {
			sql += " " + connector + " "
		}
	}

	sql += ")"

	return sql, args
}

func filterToSql(filter db.Filter) (string, []interface{}) {
	filterType := reflect.TypeOf(filter).Elem().Name()
	filterName := filter.Type()

	sql := ""
	args := make([]interface{}, 0)

	if filterType == "FieldCondition" {
		// fieldCOnditions can easily be handled generically.
		cond := filter.(*db.FieldCondition)

		// TODO: return error from func.
		operator, err := db.FilterToSqlCondition(filterName)

		sql = cond.Field + " " + operator
		if filterName == "in" {
			sql += " (?)"
		} else {
			sql += " ?"
		}

		args = append(args, cond.Value)

		return sql, args
	}

	if filterName == "and" {
		and := filter.(*db.AndCondition)
		sql, args = filterManyToSql(and.Filters, "AND")
	} else if filterName == "or" {
		or := filter.(*db.OrCondition)
		sql, args = filterManyToSql(or.Filters, "OR")
	} else if filterName == "not" {
		not := filter.(*db.NotCondition)
		sql, args = filterToSql(not.Filter)
		sql = "NOT (" + sql + ")"
	} else {
		panic(fmt.Sprintf("GORM: Unhandled filter type '%v'", filterType))
	}

	return sql, args
}

func (b Backend) buildQuery(q *db.Query) (*gorm.DB, db.DbError) {
	info := b.GetModelInfo(q.Model)
	if info == nil {
		return nil, db.Error{
			Code: "unknown_model",
			Message: fmt.Sprintf("Model '%v' not registered with backend gorm", q.Model),
		}
	}

	gormQ := b.Db

	// Handle filters.
	for _, filter := range q.Filters {
		sql, args := filterToSql(filter)
		gormQ = gormQ.Where(sql, args...)
	}

	// Handle joins.
	/*
	if len(q.Joins) > 0 {
		for _, joinQ := range q.Joins {
			args := make([]interface{}, 0)

			// Join has filters!
			// Construct sql and arguments for gorm.Preload("name", "where", "args"...)
			if len(joinQ.Filters) > 0 {
				sql, queryArgs := filterManyToSql(joinQ.Filters, "AND")
				args = append(args, interface{}(sql))
				args = append(args, queryArgs...)
			}

			if joinQ.JoinField != "" || joinQ.JoinedField != "" {
				panic("gorm_backend_does_not_support_join_keys")
			}

			gormQ = gormQ.Preload(joinQ.JoinTargetField, args...)
		}
	}
	*/

	// Handle field specificiaton.
	if len(q.FieldSpec) > 0 {
		gormQ = gormQ.Select(q.FieldSpec)
	}

	// Ordering.
	if q.Orders != nil {
		sql := ""

		count := len(q.Orders)
		for i := 0; i < count; i++ {
			sql += q.Orders[i].String()
			if i < count - 1 {
				sql += ", "
			}
		}

		gormQ = gormQ.Order(sql)
	}

	// Limit & Offset.

	if q.LimitNum != 0 {
		gormQ = gormQ.Limit(q.LimitNum)
	}
	if q.OffsetNum != 0 {
		gormQ = gormQ.Offset(q.OffsetNum)
	}

	return gormQ, nil
}

func (b *Backend) BuildRelationQuery(q *db.RelationQuery) (*db.Query, db.DbError) {
	return db.BuildRelationQuery(b, nil, q)
}

// Perform a query.
func (b Backend) Query(q *db.Query) ([]db.Model, db.DbError) {
	slice, err := b.NewModelSlice(q.Model)
	if err != nil {
		return nil, err
	}

	gormQ, err := b.buildQuery(q)
	if err != nil {
		return nil, err
	}

	if err := gormQ.Find(&slice).Error; err != nil {
		return nil, db.Error{Code: "db_error", Message: err.Error()}
	}

	models := db.InterfaceToModelSlice(slice)

	// Do joins.
	if len(q.Joins) > 0 {
		if err := db.BackendDoJoins(&b, q.Model, models, q.Joins); err != nil {
			return nil, db.Error{
				Code: "join_error",
				Message: err.Error(),
			}
		}
	}

	return models, nil
}

func (b Backend) QueryOne(q *db.Query) (db.Model, db.DbError) {
	res, err := b.Query(q)
	if err != nil {
		return nil, err
	}

	if len(res) == 0 {
		return nil, nil
	}

	m := res[0].(db.Model)
	return m, nil
}

func (b Backend) Count(q *db.Query) (uint64, db.DbError) {
	var count int

	info := b.GetModelInfo(q.Model)
	if info == nil {
		return 0, db.Error{
			Code: "unknown_model",
			Message: fmt.Sprintf("Model %v was not registered with backend gorm", q.Model),
		}
	}

	b.Db.Model(info.Item).Count(&count)
	return uint64(count), nil
}

func (b Backend) Last(q *db.Query) (db.Model, db.DbError) {
	orders := len(q.Orders)
	if orders > 0 {
		for i := 0; i < orders; i++ {
			q.Orders[i].Ascending = !q.Orders[i].Ascending
		}
	} else {
		info := b.GetModelInfo(q.Model)
		q = q.Order(info.GetPkName(), false)
	}

	return b.QueryOne(q.Limit(1))
}

// Find first model with primary key ID.
func (b Backend) FindOne(modelType string, id string) (db.Model, db.DbError) {
	return db.BackendFindOne(&b, modelType, id)
}

func (b Backend) FindBy(modelType, field string, value interface{}) ([]db.Model, db.DbError) {
	return b.Q(modelType).Filter(field, value).Find()
}

func (b Backend) FindOneBy(modelType, field string, value interface{}) (db.Model, db.DbError) {
	return b.Q(modelType).Filter(field, value).First()
}

// Convenience methods.

func (b Backend) Create(m db.Model) db.DbError {
	if err := b.Db.Create(m).Error; err != nil {
		return db.Error{Code: "gorm_error", Message: err.Error()}
	}

	return nil
}

func (b Backend) Update(m db.Model) db.DbError {
	if err := b.Db.Save(m).Error; err != nil {
		return db.Error{Code: "gorm_error", Message: err.Error()}
	}

	return nil
}

func (b Backend) Delete(m db.Model) db.DbError {
	if err := b.Db.Delete(m).Error; err != nil {
		return db.Error{Code: "gorm_error", Message: err.Error()}
	}

	return nil
}

func (b Backend) DeleteMany(q *db.Query) db.DbError {
	gormQ, err := b.buildQuery(q)
	if err != nil {
		return err
	}

	// Existence is checked in buildQuery!
	item := b.GetModelInfo(q.Model).Item
	if err := gormQ.Delete(item).Error; err != nil {
		return db.Error{
			Code: "delete_many gorm_error",
			Message: err.Error(),
		}
	}

	return nil
}

/**
 * M2M
 */

func (b Backend) M2M(obj db.Model, name string) (db.M2MCollection, db.	DbError) {
	info := b.GetModelInfo(obj.GetCollection())
	fieldInfo := info.FieldInfo[name]

	items, _ := b.NewModelSlice(fieldInfo.RelationItem.GetCollection())

	assoc := b.Db.Model(obj).Association(name)
	col := M2MCollection{
		Association: assoc,
	}

	if err := assoc.Find(items).Error; err != nil {
		return nil, db.Error{
			Code: "gorm_error",
			Message: err.Error(),
		}
	}

	modelSlice := db.InterfaceToModelSlice(items)
	col.Items = modelSlice

	return &col, nil
}

type M2MCollection struct {
	db.BaseM2MCollection
	Association *gorm.Association
}

func (c *M2MCollection) Add(items ...db.Model) db.DbError {
	if err := c.Association.Append(db.ModelToInterfaceSlice(items)).Error; err != nil {
		return db.Error{
			Code: "gorm_error",
			Message: err.Error(),
		}
	}

	for _, item := range items {
		if !c.Contains(item) {
			c.Items = append(c.Items, item)
		}
	}
	return nil
}

func (c *M2MCollection) Delete(items ...db.Model) db.DbError {
	if err := c.Association.Delete(db.ModelToInterfaceSlice(items)).Error; err != nil {
		return db.Error{
			Code: "gorm_error",
			Message: err.Error(),
		}
	}

	for _, item := range items {
		for index, curItem := range c.Items {
			if curItem.GetID() == item.GetID() {
				c.Items = append(c.Items[:index], c.Items[index+1:]...)
				break
			}
		}
	}
	return nil
}

func (c *M2MCollection) Clear() db.DbError {
	if err := c.Association.Clear().Error; err != nil {
		return db.Error{
			Code: "gorm_error",
			Message: err.Error(),
		}
	}
	c.Items = make([]db.Model, 0)
	return nil
}

func (c *M2MCollection) Replace(items []db.Model) db.DbError {
	if err := c.Association.Replace(items).Error; err != nil {
		return db.Error{
			Code: "gorm_error",
			Message: err.Error(),
		}
	}
	c.Items = items
	return nil
}
<|endoftext|>


<commit_before>package main

import (
	"fmt"
	"log"
	"os"
	"strconv"
	"strings"
)

func (e *SetExpr) eval(app *App, args []string) {
	switch e.opt {
	case "hidden":
		gOpts.hidden = true
		app.nav.renew(app.nav.height)
	case "nohidden":
		gOpts.hidden = false
		app.nav.renew(app.nav.height)
	case "hidden!":
		gOpts.hidden = !gOpts.hidden
		app.nav.renew(app.nav.height)
	case "preview":
		gOpts.preview = true
	case "nopreview":
		gOpts.preview = false
	case "preview!":
		gOpts.preview = !gOpts.preview
	case "scrolloff":
		n, err := strconv.Atoi(e.val)
		if err != nil {
			msg := fmt.Sprintf("scrolloff: %s", err)
			app.ui.message = msg
			log.Print(msg)
			return
		}
		if n < 0 {
			msg := "scrolloff: value should be a non-negative number"
			app.ui.message = msg
			log.Print(msg)
			return
		}
		max := app.ui.wins[0].h/2 - 1
		if n > max {
			// TODO: stay at the same row while up/down in the middle
			n = max
		}
		gOpts.scrolloff = n
	case "tabstop":
		n, err := strconv.Atoi(e.val)
		if err != nil {
			msg := fmt.Sprintf("tabstop: %s", err)
			app.ui.message = msg
			log.Print(msg)
			return
		}
		if n <= 0 {
			msg := "tabstop: value should be a positive number"
			app.ui.message = msg
			log.Print(msg)
			return
		}
		gOpts.tabstop = n
	case "ifs":
		gOpts.ifs = e.val
	case "showinfo":
		if e.val != "none" && e.val != "size" && e.val != "time" {
			msg := "showinfo should either be 'none', 'size' or 'time'"
			app.ui.message = msg
			log.Print(msg)
			return
		}
		gOpts.showinfo = e.val
	case "sortby":
		if e.val != "name" && e.val != "size" && e.val != "time" {
			msg := "sortby should either be 'name', 'size' or 'time'"
			app.ui.message = msg
			log.Print(msg)
			return
		}
		gOpts.sortby = e.val
		app.nav.renew(app.nav.height)
	case "opener":
		gOpts.opener = e.val
	case "ratios":
		toks := strings.Split(e.val, ":")
		var rats []int
		for _, s := range toks {
			i, err := strconv.Atoi(s)
			if err != nil {
				msg := fmt.Sprintf("ratios: %s", err)
				app.ui.message = msg
				log.Print(msg)
				return
			}
			rats = append(rats, i)
		}
		gOpts.ratios = rats
		app.ui = newUI()
	default:
		msg := fmt.Sprintf("unknown option: %s", e.opt)
		app.ui.message = msg
		log.Print(msg)
	}
}

func (e *MapExpr) eval(app *App, args []string) {
	gOpts.keys[e.keys] = e.expr
}

func (e *CmdExpr) eval(app *App, args []string) {
	gOpts.cmds[e.name] = e.expr
}

func (e *CallExpr) eval(app *App, args []string) {
	// TODO: check for extra toks in each case
	switch e.name {
	case "quit":
		gExitFlag = true
	case "echo":
		app.ui.message = strings.Join(e.args, " ")
	case "down":
		app.nav.down()
		app.ui.echoFileInfo(app.nav)
	case "up":
		app.nav.up()
		app.ui.echoFileInfo(app.nav)
	case "updir":
		if err := app.nav.updir(); err != nil {
			app.ui.message = err.Error()
			log.Print(err)
			return
		}
		app.ui.echoFileInfo(app.nav)
	case "open":
		dir := app.nav.currDir()

		if len(dir.fi) == 0 {
			return
		}

		path := app.nav.currPath()

		f, err := os.Stat(path)
		if err != nil {
			msg := fmt.Sprintf("open: %s", err)
			app.ui.message = msg
			log.Print(msg)
			return
		}

		if f.IsDir() {
			if err := app.nav.open(); err != nil {
				app.ui.message = err.Error()
				log.Print(err)
				return
			}
			app.ui.echoFileInfo(app.nav)
			return
		}

		if gSelectionPath != "" {
			out, err := os.Create(gSelectionPath)
			if err != nil {
				log.Printf("opening selection file: %s", err)
			}
			defer out.Close()

			if len(app.nav.marks) != 0 {
				marks := app.nav.currMarks()
				path = strings.Join(marks, "\n")
			}

			_, err = out.WriteString(path)
			if err != nil {
				log.Printf("writing selection file: %s", err)
			}

			gExitFlag = true
			return
		}

		if len(app.nav.marks) == 0 {
			app.runShell(fmt.Sprintf("%s '%s'", gOpts.opener, path), nil, false, false)
		} else {
			s := gOpts.opener
			for m := range app.nav.marks {
				s += fmt.Sprintf(" '%s'", m)
			}
			app.runShell(s, nil, false, false)
		}
	case "bot":
		app.nav.bot()
		app.ui.echoFileInfo(app.nav)
	case "top":
		app.nav.top()
		app.ui.echoFileInfo(app.nav)
	case "cd":
		if err := app.nav.cd(e.args[0]); err != nil {
			app.ui.message = err.Error()
			log.Print(err)
			return
		}
		app.ui.echoFileInfo(app.nav)
	case "read":
		s := app.ui.prompt(":")
		if len(s) == 0 {
			app.ui.echoFileInfo(app.nav)
			return
		}
		log.Printf("command: %s", s)
		p := newParser(strings.NewReader(s))
		for p.parse() {
			p.expr.eval(app, nil)
		}
		if p.err != nil {
			app.ui.message = p.err.Error()
			log.Print(p.err)
		}
	case "read-shell":
		s := app.ui.prompt("$")
		log.Printf("shell: %s", s)
		app.runShell(s, nil, false, false)
	case "read-shell-wait":
		s := app.ui.prompt("!")
		log.Printf("shell-wait: %s", s)
		app.runShell(s, nil, true, false)
	case "read-shell-async":
		s := app.ui.prompt("&")
		log.Printf("shell-async: %s", s)
		app.runShell(s, nil, false, true)
	case "search":
		s := app.ui.prompt("/")
		log.Printf("search: %s", s)
		app.ui.message = "sorry, search is not implemented yet!"
		// TODO: implement
	case "search-back":
		s := app.ui.prompt("?")
		log.Printf("search-back: %s", s)
		app.ui.message = "sorry, search-back is not implemented yet!"
		// TODO: implement
	case "toggle":
		app.nav.toggle()
	case "yank":
		if err := app.nav.save(true); err != nil {
			msg := fmt.Sprintf("yank: %s", err)
			app.ui.message = msg
			log.Printf(msg)
			return
		}
		app.nav.marks = make(map[string]bool)
	case "delete":
		if err := app.nav.save(false); err != nil {
			msg := fmt.Sprintf("delete: %s", err)
			app.ui.message = msg
			log.Printf(msg)
			return
		}
		app.nav.marks = make(map[string]bool)
	case "paste":
		if err := app.nav.paste(); err != nil {
			msg := fmt.Sprintf("paste: %s", err)
			app.ui.message = msg
			log.Printf(msg)
			return
		}
		app.nav.renew(app.nav.height)
		app.nav.save(false)
		saveFiles(nil, false)
	case "redraw":
		app.ui.renew()
		app.nav.renew(app.ui.wins[0].h)
	default:
		cmd, ok := gOpts.cmds[e.name]
		if !ok {
			msg := fmt.Sprintf("command not found: %s", e.name)
			app.ui.message = msg
			log.Print(msg)
			return
		}
		cmd.eval(app, e.args)
	}
}

func (e *ExecExpr) eval(app *App, args []string) {
	switch e.pref {
	case "$":
		log.Printf("shell: %s -- %s", e, args)
		app.ui.clearMsg()
		app.runShell(e.expr, args, false, false)
		app.ui.echoFileInfo(app.nav)
	case "!":
		log.Printf("shell-wait: %s -- %s", e, args)
		app.runShell(e.expr, args, true, false)
	case "&":
		log.Printf("shell-async: %s -- %s", e, args)
		app.runShell(e.expr, args, false, true)
	case "/":
		log.Printf("search: %s -- %s", e, args)
		// TODO: implement
	case "?":
		log.Printf("search-back: %s -- %s", e, args)
		// TODO: implement
	default:
		log.Printf("unknown execution prefix: %q", e.pref)
	}
}

func (e *ListExpr) eval(app *App, args []string) {
	for _, expr := range e.exprs {
		expr.eval(app, nil)
	}
}
<commit_msg>fix cd command without any argument<commit_after>package main

import (
	"fmt"
	"log"
	"os"
	"strconv"
	"strings"
)

func (e *SetExpr) eval(app *App, args []string) {
	switch e.opt {
	case "hidden":
		gOpts.hidden = true
		app.nav.renew(app.nav.height)
	case "nohidden":
		gOpts.hidden = false
		app.nav.renew(app.nav.height)
	case "hidden!":
		gOpts.hidden = !gOpts.hidden
		app.nav.renew(app.nav.height)
	case "preview":
		gOpts.preview = true
	case "nopreview":
		gOpts.preview = false
	case "preview!":
		gOpts.preview = !gOpts.preview
	case "scrolloff":
		n, err := strconv.Atoi(e.val)
		if err != nil {
			msg := fmt.Sprintf("scrolloff: %s", err)
			app.ui.message = msg
			log.Print(msg)
			return
		}
		if n < 0 {
			msg := "scrolloff: value should be a non-negative number"
			app.ui.message = msg
			log.Print(msg)
			return
		}
		max := app.ui.wins[0].h/2 - 1
		if n > max {
			// TODO: stay at the same row while up/down in the middle
			n = max
		}
		gOpts.scrolloff = n
	case "tabstop":
		n, err := strconv.Atoi(e.val)
		if err != nil {
			msg := fmt.Sprintf("tabstop: %s", err)
			app.ui.message = msg
			log.Print(msg)
			return
		}
		if n <= 0 {
			msg := "tabstop: value should be a positive number"
			app.ui.message = msg
			log.Print(msg)
			return
		}
		gOpts.tabstop = n
	case "ifs":
		gOpts.ifs = e.val
	case "showinfo":
		if e.val != "none" && e.val != "size" && e.val != "time" {
			msg := "showinfo should either be 'none', 'size' or 'time'"
			app.ui.message = msg
			log.Print(msg)
			return
		}
		gOpts.showinfo = e.val
	case "sortby":
		if e.val != "name" && e.val != "size" && e.val != "time" {
			msg := "sortby should either be 'name', 'size' or 'time'"
			app.ui.message = msg
			log.Print(msg)
			return
		}
		gOpts.sortby = e.val
		app.nav.renew(app.nav.height)
	case "opener":
		gOpts.opener = e.val
	case "ratios":
		toks := strings.Split(e.val, ":")
		var rats []int
		for _, s := range toks {
			i, err := strconv.Atoi(s)
			if err != nil {
				msg := fmt.Sprintf("ratios: %s", err)
				app.ui.message = msg
				log.Print(msg)
				return
			}
			rats = append(rats, i)
		}
		gOpts.ratios = rats
		app.ui = newUI()
	default:
		msg := fmt.Sprintf("unknown option: %s", e.opt)
		app.ui.message = msg
		log.Print(msg)
	}
}

func (e *MapExpr) eval(app *App, args []string) {
	gOpts.keys[e.keys] = e.expr
}

func (e *CmdExpr) eval(app *App, args []string) {
	gOpts.cmds[e.name] = e.expr
}

func (e *CallExpr) eval(app *App, args []string) {
	// TODO: check for extra toks in each case
	switch e.name {
	case "quit":
		gExitFlag = true
	case "echo":
		app.ui.message = strings.Join(e.args, " ")
	case "down":
		app.nav.down()
		app.ui.echoFileInfo(app.nav)
	case "up":
		app.nav.up()
		app.ui.echoFileInfo(app.nav)
	case "updir":
		if err := app.nav.updir(); err != nil {
			app.ui.message = err.Error()
			log.Print(err)
			return
		}
		app.ui.echoFileInfo(app.nav)
	case "open":
		dir := app.nav.currDir()

		if len(dir.fi) == 0 {
			return
		}

		path := app.nav.currPath()

		f, err := os.Stat(path)
		if err != nil {
			msg := fmt.Sprintf("open: %s", err)
			app.ui.message = msg
			log.Print(msg)
			return
		}

		if f.IsDir() {
			if err := app.nav.open(); err != nil {
				app.ui.message = err.Error()
				log.Print(err)
				return
			}
			app.ui.echoFileInfo(app.nav)
			return
		}

		if gSelectionPath != "" {
			out, err := os.Create(gSelectionPath)
			if err != nil {
				log.Printf("opening selection file: %s", err)
			}
			defer out.Close()

			if len(app.nav.marks) != 0 {
				marks := app.nav.currMarks()
				path = strings.Join(marks, "\n")
			}

			_, err = out.WriteString(path)
			if err != nil {
				log.Printf("writing selection file: %s", err)
			}

			gExitFlag = true
			return
		}

		if len(app.nav.marks) == 0 {
			app.runShell(fmt.Sprintf("%s '%s'", gOpts.opener, path), nil, false, false)
		} else {
			s := gOpts.opener
			for m := range app.nav.marks {
				s += fmt.Sprintf(" '%s'", m)
			}
			app.runShell(s, nil, false, false)
		}
	case "bot":
		app.nav.bot()
		app.ui.echoFileInfo(app.nav)
	case "top":
		app.nav.top()
		app.ui.echoFileInfo(app.nav)
	case "cd":
		path := "~"
		if len(e.args) > 0 {
			path = e.args[0]
		}
		if err := app.nav.cd(path); err != nil {
			app.ui.message = err.Error()
			log.Print(err)
			return
		}
		app.ui.echoFileInfo(app.nav)
	case "read":
		s := app.ui.prompt(":")
		if len(s) == 0 {
			app.ui.echoFileInfo(app.nav)
			return
		}
		log.Printf("command: %s", s)
		p := newParser(strings.NewReader(s))
		for p.parse() {
			p.expr.eval(app, nil)
		}
		if p.err != nil {
			app.ui.message = p.err.Error()
			log.Print(p.err)
		}
	case "read-shell":
		s := app.ui.prompt("$")
		log.Printf("shell: %s", s)
		app.runShell(s, nil, false, false)
	case "read-shell-wait":
		s := app.ui.prompt("!")
		log.Printf("shell-wait: %s", s)
		app.runShell(s, nil, true, false)
	case "read-shell-async":
		s := app.ui.prompt("&")
		log.Printf("shell-async: %s", s)
		app.runShell(s, nil, false, true)
	case "search":
		s := app.ui.prompt("/")
		log.Printf("search: %s", s)
		app.ui.message = "sorry, search is not implemented yet!"
		// TODO: implement
	case "search-back":
		s := app.ui.prompt("?")
		log.Printf("search-back: %s", s)
		app.ui.message = "sorry, search-back is not implemented yet!"
		// TODO: implement
	case "toggle":
		app.nav.toggle()
	case "yank":
		if err := app.nav.save(true); err != nil {
			msg := fmt.Sprintf("yank: %s", err)
			app.ui.message = msg
			log.Printf(msg)
			return
		}
		app.nav.marks = make(map[string]bool)
	case "delete":
		if err := app.nav.save(false); err != nil {
			msg := fmt.Sprintf("delete: %s", err)
			app.ui.message = msg
			log.Printf(msg)
			return
		}
		app.nav.marks = make(map[string]bool)
	case "paste":
		if err := app.nav.paste(); err != nil {
			msg := fmt.Sprintf("paste: %s", err)
			app.ui.message = msg
			log.Printf(msg)
			return
		}
		app.nav.renew(app.nav.height)
		app.nav.save(false)
		saveFiles(nil, false)
	case "redraw":
		app.ui.renew()
		app.nav.renew(app.ui.wins[0].h)
	default:
		cmd, ok := gOpts.cmds[e.name]
		if !ok {
			msg := fmt.Sprintf("command not found: %s", e.name)
			app.ui.message = msg
			log.Print(msg)
			return
		}
		cmd.eval(app, e.args)
	}
}

func (e *ExecExpr) eval(app *App, args []string) {
	switch e.pref {
	case "$":
		log.Printf("shell: %s -- %s", e, args)
		app.ui.clearMsg()
		app.runShell(e.expr, args, false, false)
		app.ui.echoFileInfo(app.nav)
	case "!":
		log.Printf("shell-wait: %s -- %s", e, args)
		app.runShell(e.expr, args, true, false)
	case "&":
		log.Printf("shell-async: %s -- %s", e, args)
		app.runShell(e.expr, args, false, true)
	case "/":
		log.Printf("search: %s -- %s", e, args)
		// TODO: implement
	case "?":
		log.Printf("search-back: %s -- %s", e, args)
		// TODO: implement
	default:
		log.Printf("unknown execution prefix: %q", e.pref)
	}
}

func (e *ListExpr) eval(app *App, args []string) {
	for _, expr := range e.exprs {
		expr.eval(app, nil)
	}
}
<|endoftext|>


<commit_before>package wsl

import (
	"database/sql"
	"errors"
	"fmt"
	"log"
	"strings"

	"github.com/elgs/gosplitargs"
	"github.com/elgs/gosqljson"
)

func (this *WSL) exec(qID string, db *sql.DB, scripts string, params map[string]interface{}, context map[string]interface{}) (interface{}, error) {

	context["scripts"] = &scripts
	context["params"] = params
	context["app"] = this

	queryResult := []interface{}{}

	tx, err := db.Begin()
	if err != nil {
		return nil, err
	}

	for _, gi := range globalInterceptors {
		err := gi.Before(tx, context)
		if err != nil {
			tx.Rollback()
			return nil, err
		}
	}

	for _, li := range queryInterceptors[qID] {
		err := li.Before(tx, context)
		if err != nil {
			tx.Rollback()
			return nil, err
		}
	}

	// log.Println(script)
	if scripts != "" {
		format := ""
		theCase := ""
		if v, ok := params["format"].(string); ok {
			format = v
		}
		if v, ok := params["case"].(string); ok {
			theCase = v
		}

		scriptsArray, err := gosplitargs.SplitArgs(scripts, ";", true)
		if err != nil {
			tx.Rollback()
			return nil, err
		}

		// single underscore
		sqlParams := extractParamsFromMap(params)
		totalCount := 0
		for index, s := range scriptsArray {

			sqlNormalize(&s)
			if len(s) == 0 {
				continue
			}

			// double underscore
			scriptParams := extractScriptParamsFromMap(params)
			for k, v := range scriptParams {
				s = strings.Replace(s, k, v.(string), -1)
			}

			count, err := gosplitargs.CountSeparators(s, "\\?")
			totalCount += count
			if err != nil {
				tx.Rollback()
				return nil, err
			}
			if len(sqlParams) < totalCount {
				tx.Rollback()
				return nil, errors.New(fmt.Sprint(s, "Incorrect param count. Expected: ", totalCount, " actual: ", len(sqlParams)))
			}

			skipSql := false
			for _, li := range queryInterceptors[qID] {
				skip, err := li.BeforeEach(tx, context, &s, sqlParams[totalCount-count:totalCount], index)
				if err != nil {
					tx.Rollback()
					return nil, err
				}
				if skip == true {
					skipSql = true
				}
			}

			if skipSql {
				continue
			}

			export := shouldExport(s)
			if isQuery(s) {
				if format == "array" {
					header, data, err := gosqljson.QueryTxToArray(tx, theCase, s, sqlParams[totalCount-count:totalCount]...)
					data = append([][]string{header}, data...)
					if err != nil {
						tx.Rollback()
						ierr := this.interceptError(qID, &err)
						if ierr != nil {
							log.Println(ierr)
						}
						return nil, err
					}
					if export {
						queryResult = append(queryResult, data)
					}
				} else {
					data, err := gosqljson.QueryTxToMap(tx, theCase, s, sqlParams[totalCount-count:totalCount]...)
					if err != nil {
						tx.Rollback()
						ierr := this.interceptError(qID, &err)
						if ierr != nil {
							log.Println(ierr)
						}
						return nil, err
					}
					if export {
						queryResult = append(queryResult, data)
					}
				}
			} else {
				rowsAffected, err := gosqljson.ExecTx(tx, s, sqlParams[totalCount-count:totalCount]...)
				if err != nil {
					tx.Rollback()
					ierr := this.interceptError(qID, &err)
					if ierr != nil {
						log.Println(ierr)
					}
					return nil, err
				}
				if export {
					queryResult = append(queryResult, rowsAffected)
				}
			}

			for index, li := range queryInterceptors[qID] {
				err := li.AfterEach(tx, context, queryResult, index)
				if err != nil {
					tx.Rollback()
					return nil, err
				}
			}
		}
	}

	var results interface{}
	if len(queryResult) == 0 {
		results = []interface{}{}
	} else if len(queryResult) == 1 {
		results = queryResult[0]
	} else {
		results = queryResult
	}

	for _, li := range queryInterceptors[qID] {
		err := li.After(tx, context, results)
		if err != nil {
			tx.Rollback()
			return nil, err
		}
	}

	for _, gi := range globalInterceptors {
		err := gi.After(tx, context, results)
		if err != nil {
			tx.Rollback()
			return nil, err
		}
	}

	tx.Commit()
	return results, nil
}

func (this *WSL) interceptError(qID string, err *error) error {
	for _, li := range queryInterceptors[qID] {
		err := li.OnError(err)
		if err != nil {
			return err
		}
	}

	for _, gi := range globalInterceptors {
		err := gi.OnError(err)
		if err != nil {
			return err
		}
	}
	return nil
}
<commit_msg>Update.<commit_after>package wsl

import (
	"database/sql"
	"errors"
	"fmt"
	"log"
	"strings"

	"github.com/elgs/gosplitargs"
	"github.com/elgs/gosqljson"
)

func (this *WSL) exec(qID string, db *sql.DB, scripts string, params map[string]interface{}, context map[string]interface{}) (interface{}, error) {

	context["scripts"] = &scripts
	context["params"] = params
	context["app"] = this

	results := []interface{}{}
	var result interface{}

	tx, err := db.Begin()
	if err != nil {
		return nil, err
	}

	for _, gi := range globalInterceptors {
		err := gi.Before(tx, context)
		if err != nil {
			tx.Rollback()
			return nil, err
		}
	}

	for _, li := range queryInterceptors[qID] {
		err := li.Before(tx, context)
		if err != nil {
			tx.Rollback()
			return nil, err
		}
	}

	// log.Println(script)
	if scripts != "" {
		format := ""
		theCase := ""
		if v, ok := params["format"].(string); ok {
			format = v
		}
		if v, ok := params["case"].(string); ok {
			theCase = v
		}

		scriptsArray, err := gosplitargs.SplitArgs(scripts, ";", true)
		if err != nil {
			tx.Rollback()
			return nil, err
		}

		// single underscore
		sqlParams := extractParamsFromMap(params)
		totalCount := 0
		for index, s := range scriptsArray {

			sqlNormalize(&s)
			if len(s) == 0 {
				continue
			}

			// double underscore
			scriptParams := extractScriptParamsFromMap(params)
			for k, v := range scriptParams {
				s = strings.Replace(s, k, v.(string), -1)
			}

			count, err := gosplitargs.CountSeparators(s, "\\?")
			totalCount += count
			if err != nil {
				tx.Rollback()
				return nil, err
			}
			if len(sqlParams) < totalCount {
				tx.Rollback()
				return nil, errors.New(fmt.Sprint(s, "Incorrect param count. Expected: ", totalCount, " actual: ", len(sqlParams)))
			}

			skipSql := false
			for _, li := range queryInterceptors[qID] {
				skip, err := li.BeforeEach(tx, context, &s, sqlParams[totalCount-count:totalCount], index)
				if err != nil {
					tx.Rollback()
					return nil, err
				}
				if skip == true {
					skipSql = true
				}
			}

			if skipSql {
				continue
			}

			export := shouldExport(s)
			if isQuery(s) {
				if format == "array" {
					header, data, err := gosqljson.QueryTxToArray(tx, theCase, s, sqlParams[totalCount-count:totalCount]...)
					data = append([][]string{header}, data...)
					if err != nil {
						tx.Rollback()
						ierr := this.interceptError(qID, &err)
						if ierr != nil {
							log.Println(ierr)
						}
						return nil, err
					}
					if export {
						results = append(results, data)
					}
					result = data
				} else {
					result, err = gosqljson.QueryTxToMap(tx, theCase, s, sqlParams[totalCount-count:totalCount]...)
					if err != nil {
						tx.Rollback()
						ierr := this.interceptError(qID, &err)
						if ierr != nil {
							log.Println(ierr)
						}
						return nil, err
					}
					if export {
						results = append(results, result)
					}
				}
			} else {
				result, err = gosqljson.ExecTx(tx, s, sqlParams[totalCount-count:totalCount]...)
				if err != nil {
					tx.Rollback()
					ierr := this.interceptError(qID, &err)
					if ierr != nil {
						log.Println(ierr)
					}
					return nil, err
				}
				if export {
					results = append(results, result)
				}
			}

			for _, li := range queryInterceptors[qID] {
				err := li.AfterEach(tx, context, result, index)
				if err != nil {
					tx.Rollback()
					return nil, err
				}
			}
		}
	}

	var ret interface{}
	if len(results) == 1 {
		ret = results[0]
	}

	for _, li := range queryInterceptors[qID] {
		err := li.After(tx, context, ret)
		if err != nil {
			tx.Rollback()
			return nil, err
		}
	}

	for _, gi := range globalInterceptors {
		err := gi.After(tx, context, ret)
		if err != nil {
			tx.Rollback()
			return nil, err
		}
	}

	tx.Commit()
	return ret, nil
}

func (this *WSL) interceptError(qID string, err *error) error {
	for _, li := range queryInterceptors[qID] {
		err := li.OnError(err)
		if err != nil {
			return err
		}
	}

	for _, gi := range globalInterceptors {
		err := gi.OnError(err)
		if err != nil {
			return err
		}
	}
	return nil
}
<|endoftext|>