Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7dd69b2442 | ||
|
|
235ea7a327 | ||
|
|
3e7f792549 | ||
|
|
b53b3bb232 | ||
|
|
64ae300e6e | ||
|
|
27726868b3 | ||
|
|
7b6cadf646 | ||
|
|
bb0cfbc1d9 | ||
|
|
cd83556e8f | ||
|
|
896df7435d | ||
|
|
082f685ce2 | ||
|
|
ea77415cfe | ||
|
|
93caa071d1 | ||
|
|
c24d8f7431 | ||
|
|
814f8abe19 | ||
|
|
6f8c1d9fe4 | ||
|
|
ff29e3aa79 | ||
|
|
f4580a4b14 | ||
|
|
4d539f7d81 | ||
|
|
991b83c55b | ||
|
|
9ea3efd576 | ||
|
|
e3d8ac7f50 | ||
|
|
3b520dd125 |
12
.bingo/.gitignore
vendored
Normal file
12
.bingo/.gitignore
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
|
||||
# Ignore everything
|
||||
*
|
||||
|
||||
# But not these files:
|
||||
!.gitignore
|
||||
!*.mod
|
||||
!README.md
|
||||
!Variables.mk
|
||||
!variables.env
|
||||
|
||||
*tmp.mod
|
||||
14
.bingo/README.md
Normal file
14
.bingo/README.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# Project Development Dependencies.
|
||||
|
||||
This is directory which stores Go modules with pinned buildable package that is used within this repository, managed by https://github.com/bwplotka/bingo.
|
||||
|
||||
* Run `bingo get` to install all tools having each own module file in this directory.
|
||||
* Run `bingo get <tool>` to install <tool> that have own module file in this directory.
|
||||
* For Makefile: Make sure to put `include .bingo/Variables.mk` in your Makefile, then use $(<upper case tool name>) variable where <tool> is the .bingo/<tool>.mod.
|
||||
* For shell: Run `source .bingo/variables.env` to source all environment variable for each tool.
|
||||
* For go: Import `.bingo/variables.go` to for variable names.
|
||||
* See https://github.com/bwplotka/bingo or -h on how to add, remove or change binaries dependencies.
|
||||
|
||||
## Requirements
|
||||
|
||||
* Go 1.14+
|
||||
25
.bingo/Variables.mk
Normal file
25
.bingo/Variables.mk
Normal file
@@ -0,0 +1,25 @@
|
||||
# Auto generated binary variables helper managed by https://github.com/bwplotka/bingo v0.5.1. DO NOT EDIT.
|
||||
# All tools are designed to be build inside $GOBIN.
|
||||
BINGO_DIR := $(dir $(lastword $(MAKEFILE_LIST)))
|
||||
GOPATH ?= $(shell go env GOPATH)
|
||||
GOBIN ?= $(firstword $(subst :, ,${GOPATH}))/bin
|
||||
GO ?= $(shell which go)
|
||||
|
||||
# Below generated variables ensure that every time a tool under each variable is invoked, the correct version
|
||||
# will be used; reinstalling only if needed.
|
||||
# For example for drone variable:
|
||||
#
|
||||
# In your main Makefile (for non array binaries):
|
||||
#
|
||||
#include .bingo/Variables.mk # Assuming -dir was set to .bingo .
|
||||
#
|
||||
#command: $(DRONE)
|
||||
# @echo "Running drone"
|
||||
# @$(DRONE) <flags/args..>
|
||||
#
|
||||
DRONE := $(GOBIN)/drone-v1.2.4
|
||||
$(DRONE): $(BINGO_DIR)/drone.mod
|
||||
@# Install binary/ries using Go 1.14+ build command. This is using bwplotka/bingo-controlled, separate go module with pinned dependencies.
|
||||
@echo "(re)installing $(GOBIN)/drone-v1.2.4"
|
||||
@cd $(BINGO_DIR) && $(GO) build -mod=mod -modfile=drone.mod -o=$(GOBIN)/drone-v1.2.4 "github.com/drone/drone-cli/drone"
|
||||
|
||||
7
.bingo/drone.mod
Normal file
7
.bingo/drone.mod
Normal file
@@ -0,0 +1,7 @@
|
||||
module _ // Auto generated by https://github.com/bwplotka/bingo. DO NOT EDIT
|
||||
|
||||
go 1.17
|
||||
|
||||
replace github.com/docker/docker => github.com/docker/engine v17.12.0-ce-rc1.0.20200309214505-aa6a9891b09c+incompatible
|
||||
|
||||
require github.com/drone/drone-cli v1.2.4 // drone
|
||||
1
.bingo/go.mod
Normal file
1
.bingo/go.mod
Normal file
@@ -0,0 +1 @@
|
||||
module _ // Fake go.mod auto-created by 'bingo' for go -moddir compatibility with non-Go projects. Commit this file, together with other .mod files.
|
||||
12
.bingo/variables.env
Normal file
12
.bingo/variables.env
Normal file
@@ -0,0 +1,12 @@
|
||||
# Auto generated binary variables helper managed by https://github.com/bwplotka/bingo v0.5.1. DO NOT EDIT.
|
||||
# All tools are designed to be build inside $GOBIN.
|
||||
# Those variables will work only until 'bingo get' was invoked, or if tools were installed via Makefile's Variables.mk.
|
||||
GOBIN=${GOBIN:=$(go env GOBIN)}
|
||||
|
||||
if [ -z "$GOBIN" ]; then
|
||||
GOBIN="$(go env GOPATH)/bin"
|
||||
fi
|
||||
|
||||
|
||||
DRONE="${GOBIN}/drone-v1.2.4"
|
||||
|
||||
133
.drone.yml
133
.drone.yml
@@ -17,12 +17,14 @@ steps:
|
||||
image: grafana/build-container:1.4.1
|
||||
commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v0.5.58/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v0.5.60/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
- ./bin/grabpl verify-drone
|
||||
- curl -fLO https://github.com/jwilder/dockerize/releases/download/v$${DOCKERIZE_VERSION}/dockerize-linux-amd64-v$${DOCKERIZE_VERSION}.tar.gz
|
||||
- tar -C bin -xzvf dockerize-linux-amd64-v$${DOCKERIZE_VERSION}.tar.gz
|
||||
- rm dockerize-linux-amd64-v$${DOCKERIZE_VERSION}.tar.gz
|
||||
- mv /etc/apt/sources.list.d/nodesource.list /etc/apt/sources.list.d/nodesource.list.disabled; apt-get update; apt-get -y upgrade; apt-get install -y ca-certificates libgnutls30
|
||||
- mv /etc/apt/sources.list.d/nodesource.list.disabled /etc/apt/sources.list.d/nodesource.list
|
||||
- yarn install --frozen-lockfile --no-progress
|
||||
environment:
|
||||
DOCKERIZE_VERSION: 0.6.1
|
||||
@@ -190,7 +192,8 @@ steps:
|
||||
- name: postgres-integration-tests
|
||||
image: grafana/build-container:1.4.1
|
||||
commands:
|
||||
- apt-get update
|
||||
- mv /etc/apt/sources.list.d/nodesource.list /etc/apt/sources.list.d/nodesource.list.disabled; apt-get update; apt-get -y upgrade; apt-get install -y ca-certificates libgnutls30
|
||||
- mv /etc/apt/sources.list.d/nodesource.list.disabled /etc/apt/sources.list.d/nodesource.list
|
||||
- apt-get install -yq postgresql-client
|
||||
- ./bin/dockerize -wait tcp://postgres:5432 -timeout 120s
|
||||
- psql -p 5432 -h postgres -U grafanatest -d grafanatest -f devenv/docker/blocks/postgres_tests/setup.sql
|
||||
@@ -207,7 +210,8 @@ steps:
|
||||
- name: mysql-integration-tests
|
||||
image: grafana/build-container:1.4.1
|
||||
commands:
|
||||
- apt-get update
|
||||
- mv /etc/apt/sources.list.d/nodesource.list /etc/apt/sources.list.d/nodesource.list.disabled; apt-get update; apt-get -y upgrade; apt-get install -y ca-certificates libgnutls30
|
||||
- mv /etc/apt/sources.list.d/nodesource.list.disabled /etc/apt/sources.list.d/nodesource.list
|
||||
- apt-get install -yq default-mysql-client
|
||||
- ./bin/dockerize -wait tcp://mysql:3306 -timeout 120s
|
||||
- cat devenv/docker/blocks/mysql_tests/setup.sql | mysql -h mysql -P 3306 -u root -prootpass
|
||||
@@ -236,6 +240,9 @@ services:
|
||||
MYSQL_ROOT_PASSWORD: rootpass
|
||||
MYSQL_USER: grafana
|
||||
|
||||
node:
|
||||
type: no-parallel
|
||||
|
||||
trigger:
|
||||
event:
|
||||
- pull_request
|
||||
@@ -259,12 +266,14 @@ steps:
|
||||
image: grafana/build-container:1.4.1
|
||||
commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v0.5.58/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v0.5.60/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
- ./bin/grabpl verify-drone
|
||||
- curl -fLO https://github.com/jwilder/dockerize/releases/download/v$${DOCKERIZE_VERSION}/dockerize-linux-amd64-v$${DOCKERIZE_VERSION}.tar.gz
|
||||
- tar -C bin -xzvf dockerize-linux-amd64-v$${DOCKERIZE_VERSION}.tar.gz
|
||||
- rm dockerize-linux-amd64-v$${DOCKERIZE_VERSION}.tar.gz
|
||||
- mv /etc/apt/sources.list.d/nodesource.list /etc/apt/sources.list.d/nodesource.list.disabled; apt-get update; apt-get -y upgrade; apt-get install -y ca-certificates libgnutls30
|
||||
- mv /etc/apt/sources.list.d/nodesource.list.disabled /etc/apt/sources.list.d/nodesource.list
|
||||
- yarn install --frozen-lockfile --no-progress
|
||||
environment:
|
||||
DOCKERIZE_VERSION: 0.6.1
|
||||
@@ -486,7 +495,8 @@ steps:
|
||||
- name: postgres-integration-tests
|
||||
image: grafana/build-container:1.4.1
|
||||
commands:
|
||||
- apt-get update
|
||||
- mv /etc/apt/sources.list.d/nodesource.list /etc/apt/sources.list.d/nodesource.list.disabled; apt-get update; apt-get -y upgrade; apt-get install -y ca-certificates libgnutls30
|
||||
- mv /etc/apt/sources.list.d/nodesource.list.disabled /etc/apt/sources.list.d/nodesource.list
|
||||
- apt-get install -yq postgresql-client
|
||||
- ./bin/dockerize -wait tcp://postgres:5432 -timeout 120s
|
||||
- psql -p 5432 -h postgres -U grafanatest -d grafanatest -f devenv/docker/blocks/postgres_tests/setup.sql
|
||||
@@ -503,7 +513,8 @@ steps:
|
||||
- name: mysql-integration-tests
|
||||
image: grafana/build-container:1.4.1
|
||||
commands:
|
||||
- apt-get update
|
||||
- mv /etc/apt/sources.list.d/nodesource.list /etc/apt/sources.list.d/nodesource.list.disabled; apt-get update; apt-get -y upgrade; apt-get install -y ca-certificates libgnutls30
|
||||
- mv /etc/apt/sources.list.d/nodesource.list.disabled /etc/apt/sources.list.d/nodesource.list
|
||||
- apt-get install -yq default-mysql-client
|
||||
- ./bin/dockerize -wait tcp://mysql:3306 -timeout 120s
|
||||
- cat devenv/docker/blocks/mysql_tests/setup.sql | mysql -h mysql -P 3306 -u root -prootpass
|
||||
@@ -565,6 +576,9 @@ services:
|
||||
MYSQL_ROOT_PASSWORD: rootpass
|
||||
MYSQL_USER: grafana
|
||||
|
||||
node:
|
||||
type: no-parallel
|
||||
|
||||
trigger:
|
||||
branch:
|
||||
- master
|
||||
@@ -591,7 +605,7 @@ steps:
|
||||
image: grafana/ci-wix:0.1.1
|
||||
commands:
|
||||
- $$ProgressPreference = "SilentlyContinue"
|
||||
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v0.5.58/windows/grabpl.exe -OutFile grabpl.exe
|
||||
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v0.5.60/windows/grabpl.exe -OutFile grabpl.exe
|
||||
|
||||
- name: build-windows-installer
|
||||
image: grafana/ci-wix:0.1.1
|
||||
@@ -640,7 +654,7 @@ steps:
|
||||
image: grafana/build-container:1.4.1
|
||||
commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v0.5.58/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v0.5.60/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
- ./bin/grabpl verify-drone
|
||||
environment:
|
||||
@@ -665,6 +679,9 @@ steps:
|
||||
depends_on:
|
||||
- initialize
|
||||
|
||||
node:
|
||||
type: no-parallel
|
||||
|
||||
trigger:
|
||||
branch:
|
||||
- master
|
||||
@@ -725,13 +742,15 @@ steps:
|
||||
image: grafana/build-container:1.4.1
|
||||
commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v0.5.58/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v0.5.60/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
- ./bin/grabpl verify-drone
|
||||
- ./bin/grabpl verify-version ${DRONE_TAG}
|
||||
- curl -fLO https://github.com/jwilder/dockerize/releases/download/v$${DOCKERIZE_VERSION}/dockerize-linux-amd64-v$${DOCKERIZE_VERSION}.tar.gz
|
||||
- tar -C bin -xzvf dockerize-linux-amd64-v$${DOCKERIZE_VERSION}.tar.gz
|
||||
- rm dockerize-linux-amd64-v$${DOCKERIZE_VERSION}.tar.gz
|
||||
- mv /etc/apt/sources.list.d/nodesource.list /etc/apt/sources.list.d/nodesource.list.disabled; apt-get update; apt-get -y upgrade; apt-get install -y ca-certificates libgnutls30
|
||||
- mv /etc/apt/sources.list.d/nodesource.list.disabled /etc/apt/sources.list.d/nodesource.list
|
||||
- yarn install --frozen-lockfile --no-progress
|
||||
environment:
|
||||
DOCKERIZE_VERSION: 0.6.1
|
||||
@@ -913,7 +932,8 @@ steps:
|
||||
- name: postgres-integration-tests
|
||||
image: grafana/build-container:1.4.1
|
||||
commands:
|
||||
- apt-get update
|
||||
- mv /etc/apt/sources.list.d/nodesource.list /etc/apt/sources.list.d/nodesource.list.disabled; apt-get update; apt-get -y upgrade; apt-get install -y ca-certificates libgnutls30
|
||||
- mv /etc/apt/sources.list.d/nodesource.list.disabled /etc/apt/sources.list.d/nodesource.list
|
||||
- apt-get install -yq postgresql-client
|
||||
- ./bin/dockerize -wait tcp://postgres:5432 -timeout 120s
|
||||
- psql -p 5432 -h postgres -U grafanatest -d grafanatest -f devenv/docker/blocks/postgres_tests/setup.sql
|
||||
@@ -930,7 +950,8 @@ steps:
|
||||
- name: mysql-integration-tests
|
||||
image: grafana/build-container:1.4.1
|
||||
commands:
|
||||
- apt-get update
|
||||
- mv /etc/apt/sources.list.d/nodesource.list /etc/apt/sources.list.d/nodesource.list.disabled; apt-get update; apt-get -y upgrade; apt-get install -y ca-certificates libgnutls30
|
||||
- mv /etc/apt/sources.list.d/nodesource.list.disabled /etc/apt/sources.list.d/nodesource.list
|
||||
- apt-get install -yq default-mysql-client
|
||||
- ./bin/dockerize -wait tcp://mysql:3306 -timeout 120s
|
||||
- cat devenv/docker/blocks/mysql_tests/setup.sql | mysql -h mysql -P 3306 -u root -prootpass
|
||||
@@ -1008,6 +1029,9 @@ services:
|
||||
MYSQL_ROOT_PASSWORD: rootpass
|
||||
MYSQL_USER: grafana
|
||||
|
||||
node:
|
||||
type: no-parallel
|
||||
|
||||
trigger:
|
||||
ref:
|
||||
- refs/tags/v*
|
||||
@@ -1032,7 +1056,7 @@ steps:
|
||||
image: grafana/ci-wix:0.1.1
|
||||
commands:
|
||||
- $$ProgressPreference = "SilentlyContinue"
|
||||
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v0.5.58/windows/grabpl.exe -OutFile grabpl.exe
|
||||
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v0.5.60/windows/grabpl.exe -OutFile grabpl.exe
|
||||
|
||||
- name: build-windows-installer
|
||||
image: grafana/ci-wix:0.1.1
|
||||
@@ -1082,7 +1106,7 @@ steps:
|
||||
image: grafana/build-container:1.4.1
|
||||
commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v0.5.58/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v0.5.60/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
- git clone "https://$${GITHUB_TOKEN}@github.com/grafana/grafana-enterprise.git"
|
||||
- cd grafana-enterprise
|
||||
@@ -1105,6 +1129,8 @@ steps:
|
||||
- curl -fLO https://github.com/jwilder/dockerize/releases/download/v$${DOCKERIZE_VERSION}/dockerize-linux-amd64-v$${DOCKERIZE_VERSION}.tar.gz
|
||||
- tar -C bin -xzvf dockerize-linux-amd64-v$${DOCKERIZE_VERSION}.tar.gz
|
||||
- rm dockerize-linux-amd64-v$${DOCKERIZE_VERSION}.tar.gz
|
||||
- mv /etc/apt/sources.list.d/nodesource.list /etc/apt/sources.list.d/nodesource.list.disabled; apt-get update; apt-get -y upgrade; apt-get install -y ca-certificates libgnutls30
|
||||
- mv /etc/apt/sources.list.d/nodesource.list.disabled /etc/apt/sources.list.d/nodesource.list
|
||||
- yarn install --frozen-lockfile --no-progress
|
||||
environment:
|
||||
DOCKERIZE_VERSION: 0.6.1
|
||||
@@ -1313,7 +1339,8 @@ steps:
|
||||
- name: postgres-integration-tests
|
||||
image: grafana/build-container:1.4.1
|
||||
commands:
|
||||
- apt-get update
|
||||
- mv /etc/apt/sources.list.d/nodesource.list /etc/apt/sources.list.d/nodesource.list.disabled; apt-get update; apt-get -y upgrade; apt-get install -y ca-certificates libgnutls30
|
||||
- mv /etc/apt/sources.list.d/nodesource.list.disabled /etc/apt/sources.list.d/nodesource.list
|
||||
- apt-get install -yq postgresql-client
|
||||
- ./bin/dockerize -wait tcp://postgres:5432 -timeout 120s
|
||||
- psql -p 5432 -h postgres -U grafanatest -d grafanatest -f devenv/docker/blocks/postgres_tests/setup.sql
|
||||
@@ -1330,7 +1357,8 @@ steps:
|
||||
- name: mysql-integration-tests
|
||||
image: grafana/build-container:1.4.1
|
||||
commands:
|
||||
- apt-get update
|
||||
- mv /etc/apt/sources.list.d/nodesource.list /etc/apt/sources.list.d/nodesource.list.disabled; apt-get update; apt-get -y upgrade; apt-get install -y ca-certificates libgnutls30
|
||||
- mv /etc/apt/sources.list.d/nodesource.list.disabled /etc/apt/sources.list.d/nodesource.list
|
||||
- apt-get install -yq default-mysql-client
|
||||
- ./bin/dockerize -wait tcp://mysql:3306 -timeout 120s
|
||||
- cat devenv/docker/blocks/mysql_tests/setup.sql | mysql -h mysql -P 3306 -u root -prootpass
|
||||
@@ -1445,6 +1473,9 @@ services:
|
||||
MYSQL_ROOT_PASSWORD: rootpass
|
||||
MYSQL_USER: grafana
|
||||
|
||||
node:
|
||||
type: no-parallel
|
||||
|
||||
trigger:
|
||||
ref:
|
||||
- refs/tags/v*
|
||||
@@ -1472,7 +1503,7 @@ steps:
|
||||
image: grafana/ci-wix:0.1.1
|
||||
commands:
|
||||
- $$ProgressPreference = "SilentlyContinue"
|
||||
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v0.5.58/windows/grabpl.exe -OutFile grabpl.exe
|
||||
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v0.5.60/windows/grabpl.exe -OutFile grabpl.exe
|
||||
- git clone "https://$$env:GITHUB_TOKEN@github.com/grafana/grafana-enterprise.git"
|
||||
- cd grafana-enterprise
|
||||
- git checkout ${DRONE_TAG}
|
||||
@@ -1537,7 +1568,7 @@ steps:
|
||||
image: grafana/build-container:1.4.1
|
||||
commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v0.5.58/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v0.5.60/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
- ./bin/grabpl verify-drone
|
||||
- ./bin/grabpl verify-version ${DRONE_TAG}
|
||||
@@ -1582,6 +1613,9 @@ steps:
|
||||
depends_on:
|
||||
- initialize
|
||||
|
||||
node:
|
||||
type: no-parallel
|
||||
|
||||
trigger:
|
||||
ref:
|
||||
- refs/tags/v*
|
||||
@@ -1642,13 +1676,15 @@ steps:
|
||||
image: grafana/build-container:1.4.1
|
||||
commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v0.5.58/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v0.5.60/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
- ./bin/grabpl verify-drone
|
||||
- ./bin/grabpl verify-version v7.3.0-test
|
||||
- curl -fLO https://github.com/jwilder/dockerize/releases/download/v$${DOCKERIZE_VERSION}/dockerize-linux-amd64-v$${DOCKERIZE_VERSION}.tar.gz
|
||||
- tar -C bin -xzvf dockerize-linux-amd64-v$${DOCKERIZE_VERSION}.tar.gz
|
||||
- rm dockerize-linux-amd64-v$${DOCKERIZE_VERSION}.tar.gz
|
||||
- mv /etc/apt/sources.list.d/nodesource.list /etc/apt/sources.list.d/nodesource.list.disabled; apt-get update; apt-get -y upgrade; apt-get install -y ca-certificates libgnutls30
|
||||
- mv /etc/apt/sources.list.d/nodesource.list.disabled /etc/apt/sources.list.d/nodesource.list
|
||||
- yarn install --frozen-lockfile --no-progress
|
||||
environment:
|
||||
DOCKERIZE_VERSION: 0.6.1
|
||||
@@ -1824,7 +1860,8 @@ steps:
|
||||
- name: postgres-integration-tests
|
||||
image: grafana/build-container:1.4.1
|
||||
commands:
|
||||
- apt-get update
|
||||
- mv /etc/apt/sources.list.d/nodesource.list /etc/apt/sources.list.d/nodesource.list.disabled; apt-get update; apt-get -y upgrade; apt-get install -y ca-certificates libgnutls30
|
||||
- mv /etc/apt/sources.list.d/nodesource.list.disabled /etc/apt/sources.list.d/nodesource.list
|
||||
- apt-get install -yq postgresql-client
|
||||
- ./bin/dockerize -wait tcp://postgres:5432 -timeout 120s
|
||||
- psql -p 5432 -h postgres -U grafanatest -d grafanatest -f devenv/docker/blocks/postgres_tests/setup.sql
|
||||
@@ -1841,7 +1878,8 @@ steps:
|
||||
- name: mysql-integration-tests
|
||||
image: grafana/build-container:1.4.1
|
||||
commands:
|
||||
- apt-get update
|
||||
- mv /etc/apt/sources.list.d/nodesource.list /etc/apt/sources.list.d/nodesource.list.disabled; apt-get update; apt-get -y upgrade; apt-get install -y ca-certificates libgnutls30
|
||||
- mv /etc/apt/sources.list.d/nodesource.list.disabled /etc/apt/sources.list.d/nodesource.list
|
||||
- apt-get install -yq default-mysql-client
|
||||
- ./bin/dockerize -wait tcp://mysql:3306 -timeout 120s
|
||||
- cat devenv/docker/blocks/mysql_tests/setup.sql | mysql -h mysql -P 3306 -u root -prootpass
|
||||
@@ -1914,6 +1952,9 @@ services:
|
||||
MYSQL_ROOT_PASSWORD: rootpass
|
||||
MYSQL_USER: grafana
|
||||
|
||||
node:
|
||||
type: no-parallel
|
||||
|
||||
trigger:
|
||||
event:
|
||||
- custom
|
||||
@@ -1938,7 +1979,7 @@ steps:
|
||||
image: grafana/ci-wix:0.1.1
|
||||
commands:
|
||||
- $$ProgressPreference = "SilentlyContinue"
|
||||
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v0.5.58/windows/grabpl.exe -OutFile grabpl.exe
|
||||
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v0.5.60/windows/grabpl.exe -OutFile grabpl.exe
|
||||
|
||||
- name: build-windows-installer
|
||||
image: grafana/ci-wix:0.1.1
|
||||
@@ -1988,7 +2029,7 @@ steps:
|
||||
image: grafana/build-container:1.4.1
|
||||
commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v0.5.58/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v0.5.60/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
- git clone "https://$${GITHUB_TOKEN}@github.com/grafana/grafana-enterprise.git"
|
||||
- cd grafana-enterprise
|
||||
@@ -2011,6 +2052,8 @@ steps:
|
||||
- curl -fLO https://github.com/jwilder/dockerize/releases/download/v$${DOCKERIZE_VERSION}/dockerize-linux-amd64-v$${DOCKERIZE_VERSION}.tar.gz
|
||||
- tar -C bin -xzvf dockerize-linux-amd64-v$${DOCKERIZE_VERSION}.tar.gz
|
||||
- rm dockerize-linux-amd64-v$${DOCKERIZE_VERSION}.tar.gz
|
||||
- mv /etc/apt/sources.list.d/nodesource.list /etc/apt/sources.list.d/nodesource.list.disabled; apt-get update; apt-get -y upgrade; apt-get install -y ca-certificates libgnutls30
|
||||
- mv /etc/apt/sources.list.d/nodesource.list.disabled /etc/apt/sources.list.d/nodesource.list
|
||||
- yarn install --frozen-lockfile --no-progress
|
||||
environment:
|
||||
DOCKERIZE_VERSION: 0.6.1
|
||||
@@ -2213,7 +2256,8 @@ steps:
|
||||
- name: postgres-integration-tests
|
||||
image: grafana/build-container:1.4.1
|
||||
commands:
|
||||
- apt-get update
|
||||
- mv /etc/apt/sources.list.d/nodesource.list /etc/apt/sources.list.d/nodesource.list.disabled; apt-get update; apt-get -y upgrade; apt-get install -y ca-certificates libgnutls30
|
||||
- mv /etc/apt/sources.list.d/nodesource.list.disabled /etc/apt/sources.list.d/nodesource.list
|
||||
- apt-get install -yq postgresql-client
|
||||
- ./bin/dockerize -wait tcp://postgres:5432 -timeout 120s
|
||||
- psql -p 5432 -h postgres -U grafanatest -d grafanatest -f devenv/docker/blocks/postgres_tests/setup.sql
|
||||
@@ -2230,7 +2274,8 @@ steps:
|
||||
- name: mysql-integration-tests
|
||||
image: grafana/build-container:1.4.1
|
||||
commands:
|
||||
- apt-get update
|
||||
- mv /etc/apt/sources.list.d/nodesource.list /etc/apt/sources.list.d/nodesource.list.disabled; apt-get update; apt-get -y upgrade; apt-get install -y ca-certificates libgnutls30
|
||||
- mv /etc/apt/sources.list.d/nodesource.list.disabled /etc/apt/sources.list.d/nodesource.list
|
||||
- apt-get install -yq default-mysql-client
|
||||
- ./bin/dockerize -wait tcp://mysql:3306 -timeout 120s
|
||||
- cat devenv/docker/blocks/mysql_tests/setup.sql | mysql -h mysql -P 3306 -u root -prootpass
|
||||
@@ -2345,6 +2390,9 @@ services:
|
||||
MYSQL_ROOT_PASSWORD: rootpass
|
||||
MYSQL_USER: grafana
|
||||
|
||||
node:
|
||||
type: no-parallel
|
||||
|
||||
trigger:
|
||||
event:
|
||||
- custom
|
||||
@@ -2372,7 +2420,7 @@ steps:
|
||||
image: grafana/ci-wix:0.1.1
|
||||
commands:
|
||||
- $$ProgressPreference = "SilentlyContinue"
|
||||
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v0.5.58/windows/grabpl.exe -OutFile grabpl.exe
|
||||
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v0.5.60/windows/grabpl.exe -OutFile grabpl.exe
|
||||
- git clone "https://$$env:GITHUB_TOKEN@github.com/grafana/grafana-enterprise.git"
|
||||
- cd grafana-enterprise
|
||||
- git checkout main
|
||||
@@ -2437,7 +2485,7 @@ steps:
|
||||
image: grafana/build-container:1.4.1
|
||||
commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v0.5.58/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v0.5.60/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
- ./bin/grabpl verify-drone
|
||||
- ./bin/grabpl verify-version v7.3.0-test
|
||||
@@ -2482,6 +2530,9 @@ steps:
|
||||
depends_on:
|
||||
- initialize
|
||||
|
||||
node:
|
||||
type: no-parallel
|
||||
|
||||
trigger:
|
||||
event:
|
||||
- custom
|
||||
@@ -2542,12 +2593,14 @@ steps:
|
||||
image: grafana/build-container:1.4.1
|
||||
commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v0.5.58/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v0.5.60/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
- ./bin/grabpl verify-drone
|
||||
- curl -fLO https://github.com/jwilder/dockerize/releases/download/v$${DOCKERIZE_VERSION}/dockerize-linux-amd64-v$${DOCKERIZE_VERSION}.tar.gz
|
||||
- tar -C bin -xzvf dockerize-linux-amd64-v$${DOCKERIZE_VERSION}.tar.gz
|
||||
- rm dockerize-linux-amd64-v$${DOCKERIZE_VERSION}.tar.gz
|
||||
- mv /etc/apt/sources.list.d/nodesource.list /etc/apt/sources.list.d/nodesource.list.disabled; apt-get update; apt-get -y upgrade; apt-get install -y ca-certificates libgnutls30
|
||||
- mv /etc/apt/sources.list.d/nodesource.list.disabled /etc/apt/sources.list.d/nodesource.list
|
||||
- yarn install --frozen-lockfile --no-progress
|
||||
environment:
|
||||
DOCKERIZE_VERSION: 0.6.1
|
||||
@@ -2720,7 +2773,8 @@ steps:
|
||||
- name: postgres-integration-tests
|
||||
image: grafana/build-container:1.4.1
|
||||
commands:
|
||||
- apt-get update
|
||||
- mv /etc/apt/sources.list.d/nodesource.list /etc/apt/sources.list.d/nodesource.list.disabled; apt-get update; apt-get -y upgrade; apt-get install -y ca-certificates libgnutls30
|
||||
- mv /etc/apt/sources.list.d/nodesource.list.disabled /etc/apt/sources.list.d/nodesource.list
|
||||
- apt-get install -yq postgresql-client
|
||||
- ./bin/dockerize -wait tcp://postgres:5432 -timeout 120s
|
||||
- psql -p 5432 -h postgres -U grafanatest -d grafanatest -f devenv/docker/blocks/postgres_tests/setup.sql
|
||||
@@ -2737,7 +2791,8 @@ steps:
|
||||
- name: mysql-integration-tests
|
||||
image: grafana/build-container:1.4.1
|
||||
commands:
|
||||
- apt-get update
|
||||
- mv /etc/apt/sources.list.d/nodesource.list /etc/apt/sources.list.d/nodesource.list.disabled; apt-get update; apt-get -y upgrade; apt-get install -y ca-certificates libgnutls30
|
||||
- mv /etc/apt/sources.list.d/nodesource.list.disabled /etc/apt/sources.list.d/nodesource.list
|
||||
- apt-get install -yq default-mysql-client
|
||||
- ./bin/dockerize -wait tcp://mysql:3306 -timeout 120s
|
||||
- cat devenv/docker/blocks/mysql_tests/setup.sql | mysql -h mysql -P 3306 -u root -prootpass
|
||||
@@ -2789,6 +2844,9 @@ services:
|
||||
MYSQL_ROOT_PASSWORD: rootpass
|
||||
MYSQL_USER: grafana
|
||||
|
||||
node:
|
||||
type: no-parallel
|
||||
|
||||
trigger:
|
||||
ref:
|
||||
- refs/heads/v*
|
||||
@@ -2813,7 +2871,7 @@ steps:
|
||||
image: grafana/ci-wix:0.1.1
|
||||
commands:
|
||||
- $$ProgressPreference = "SilentlyContinue"
|
||||
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v0.5.58/windows/grabpl.exe -OutFile grabpl.exe
|
||||
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v0.5.60/windows/grabpl.exe -OutFile grabpl.exe
|
||||
|
||||
- name: build-windows-installer
|
||||
image: grafana/ci-wix:0.1.1
|
||||
@@ -2859,7 +2917,7 @@ steps:
|
||||
image: grafana/build-container:1.4.1
|
||||
commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v0.5.58/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v0.5.60/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
- git clone "https://$${GITHUB_TOKEN}@github.com/grafana/grafana-enterprise.git"
|
||||
- cd grafana-enterprise
|
||||
@@ -2881,6 +2939,8 @@ steps:
|
||||
- curl -fLO https://github.com/jwilder/dockerize/releases/download/v$${DOCKERIZE_VERSION}/dockerize-linux-amd64-v$${DOCKERIZE_VERSION}.tar.gz
|
||||
- tar -C bin -xzvf dockerize-linux-amd64-v$${DOCKERIZE_VERSION}.tar.gz
|
||||
- rm dockerize-linux-amd64-v$${DOCKERIZE_VERSION}.tar.gz
|
||||
- mv /etc/apt/sources.list.d/nodesource.list /etc/apt/sources.list.d/nodesource.list.disabled; apt-get update; apt-get -y upgrade; apt-get install -y ca-certificates libgnutls30
|
||||
- mv /etc/apt/sources.list.d/nodesource.list.disabled /etc/apt/sources.list.d/nodesource.list
|
||||
- yarn install --frozen-lockfile --no-progress
|
||||
environment:
|
||||
DOCKERIZE_VERSION: 0.6.1
|
||||
@@ -3087,7 +3147,8 @@ steps:
|
||||
- name: postgres-integration-tests
|
||||
image: grafana/build-container:1.4.1
|
||||
commands:
|
||||
- apt-get update
|
||||
- mv /etc/apt/sources.list.d/nodesource.list /etc/apt/sources.list.d/nodesource.list.disabled; apt-get update; apt-get -y upgrade; apt-get install -y ca-certificates libgnutls30
|
||||
- mv /etc/apt/sources.list.d/nodesource.list.disabled /etc/apt/sources.list.d/nodesource.list
|
||||
- apt-get install -yq postgresql-client
|
||||
- ./bin/dockerize -wait tcp://postgres:5432 -timeout 120s
|
||||
- psql -p 5432 -h postgres -U grafanatest -d grafanatest -f devenv/docker/blocks/postgres_tests/setup.sql
|
||||
@@ -3104,7 +3165,8 @@ steps:
|
||||
- name: mysql-integration-tests
|
||||
image: grafana/build-container:1.4.1
|
||||
commands:
|
||||
- apt-get update
|
||||
- mv /etc/apt/sources.list.d/nodesource.list /etc/apt/sources.list.d/nodesource.list.disabled; apt-get update; apt-get -y upgrade; apt-get install -y ca-certificates libgnutls30
|
||||
- mv /etc/apt/sources.list.d/nodesource.list.disabled /etc/apt/sources.list.d/nodesource.list
|
||||
- apt-get install -yq default-mysql-client
|
||||
- ./bin/dockerize -wait tcp://mysql:3306 -timeout 120s
|
||||
- cat devenv/docker/blocks/mysql_tests/setup.sql | mysql -h mysql -P 3306 -u root -prootpass
|
||||
@@ -3219,6 +3281,9 @@ services:
|
||||
MYSQL_ROOT_PASSWORD: rootpass
|
||||
MYSQL_USER: grafana
|
||||
|
||||
node:
|
||||
type: no-parallel
|
||||
|
||||
trigger:
|
||||
ref:
|
||||
- refs/heads/v*
|
||||
@@ -3246,7 +3311,7 @@ steps:
|
||||
image: grafana/ci-wix:0.1.1
|
||||
commands:
|
||||
- $$ProgressPreference = "SilentlyContinue"
|
||||
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v0.5.58/windows/grabpl.exe -OutFile grabpl.exe
|
||||
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v0.5.60/windows/grabpl.exe -OutFile grabpl.exe
|
||||
- git clone "https://$$env:GITHUB_TOKEN@github.com/grafana/grafana-enterprise.git"
|
||||
- cd grafana-enterprise
|
||||
- git checkout $$env:DRONE_BRANCH
|
||||
|
||||
10
CHANGELOG.md
10
CHANGELOG.md
@@ -130,6 +130,16 @@ The InfoBox & FeatureInfoBox are now deprecated please use the Alert component i
|
||||
|
||||
<!-- 8.0.0-beta1 END -->
|
||||
|
||||
<!-- 7.5.10 START -->
|
||||
|
||||
# 7.5.10 (2021-07-15)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* **[v7.5.x] Transformations:** add 'prepare time series' transformer. [#36749](https://github.com/grafana/grafana/pull/36749), [@mckn](https://github.com/mckn)
|
||||
|
||||
<!-- 7.5.10 END -->
|
||||
|
||||
<!-- 7.5.9 START -->
|
||||
|
||||
# 7.5.9 (2021-06-23)
|
||||
|
||||
8
Makefile
8
Makefile
@@ -3,6 +3,7 @@
|
||||
## For more information, refer to https://suva.sh/posts/well-documented-makefiles/
|
||||
|
||||
-include local/Makefile
|
||||
include .bingo/Variables.mk
|
||||
|
||||
.PHONY: all deps-go deps-js deps build-go build-server build-cli build-js build build-docker-dev build-docker-full lint-go revive golangci-lint test-go test-js test run run-frontend clean devenv devenv-down revive-strict protobuf help
|
||||
|
||||
@@ -156,5 +157,12 @@ clean: ## Clean up intermediate build artifacts.
|
||||
rm -rf node_modules
|
||||
rm -rf public/build
|
||||
|
||||
# This repository's configuration is protected (https://readme.drone.io/signature/).
|
||||
# Use this make target to regenerate the configuration YAML files when
|
||||
# you modify starlark files.
|
||||
drone: $(DRONE)
|
||||
$(DRONE) starlark --format
|
||||
$(DRONE) lint .drone.yml --trusted
|
||||
|
||||
help: ## Display this help.
|
||||
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
|
||||
|
||||
@@ -120,6 +120,11 @@ Defaults to `false`.
|
||||
oauth_auto_login = true
|
||||
```
|
||||
|
||||
### Avoid automatic OAuth login
|
||||
|
||||
To sign in with a username and password and avoid automatic OAuth login, add the `disableAutoLogin` parameter to your login URL.
|
||||
For example: `grafana.example.com/login?disableAutoLogin` or `grafana.example.com/login?disableAutoLogin=true`
|
||||
|
||||
### Hide sign-out menu
|
||||
|
||||
Set the option detailed below to true to hide sign-out menu link. Useful if you use an auth proxy or JWT authentication.
|
||||
|
||||
@@ -30,17 +30,17 @@ In essence, a data frame is a collection of _fields_, where each field correspon
|
||||
|
||||
```ts
|
||||
interface Field {
|
||||
name: string;
|
||||
// Prometheus like Labels / Tags
|
||||
labels?: Record<string, string>;
|
||||
name: string;
|
||||
// Prometheus like Labels / Tags
|
||||
labels?: Record<string, string>;
|
||||
|
||||
// For example string, number, time (or more specific primitives in the backend)
|
||||
type: FieldType;
|
||||
// Array of values all of the same type
|
||||
values: Vector<T>;
|
||||
// For example string, number, time (or more specific primitives in the backend)
|
||||
type: FieldType;
|
||||
// Array of values all of the same type
|
||||
values: Vector<T>;
|
||||
|
||||
// Optional display data for the field (e.g. unit, name over-ride, etc)
|
||||
config: FieldConfig;
|
||||
// Optional display data for the field (e.g. unit, name over-ride, etc)
|
||||
config: FieldConfig;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -163,6 +163,8 @@ Dimensions: 5 fields by 2 rows
|
||||
+---------------------+------------------+------------------+------------------+------------------+
|
||||
```
|
||||
|
||||
> **Note:** Not all panels support the wide time series data frame format. To keep full backward compatibility we have introduced a transformation that can be used to convert from the wide to the long format. Read more about how to use it here: [Prepare time series-transformation]({{< relref "../../panels/transformations/types-options.md#prepare-time-series" >}}).
|
||||
|
||||
## Technical references
|
||||
|
||||
This section contains links to technical reference and implementations of data frames.
|
||||
|
||||
@@ -176,7 +176,7 @@ Content-Type: application/json
|
||||
|
||||
```
|
||||
|
||||
**Example response**:
|
||||
**Example response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
|
||||
@@ -7,7 +7,13 @@ aliases = ["/docs/grafana/latest/http_api/team/"]
|
||||
|
||||
# Team API
|
||||
|
||||
This API can be used to create/update/delete Teams and to add/remove users to Teams. All actions require that the user has the Admin role for the organization.
|
||||
This API can be used to manage Teams and Team Memberships.
|
||||
|
||||
Access to these API endpoints is restricted as follows:
|
||||
|
||||
- All authenticated users are able to view details of teams they are a member of.
|
||||
- Organization Admins are able to manage all teams and team members.
|
||||
- If the `editors_can_admin` configuration flag is enabled, Organization Editors are able to view details of all teams and to manage teams that they are Admin members of.
|
||||
|
||||
## Team Search With Paging
|
||||
|
||||
|
||||
@@ -24,60 +24,60 @@ Grafana comes with the following transformations:
|
||||
- [Series to rows](#series-to-rows)
|
||||
- [Filter data by value](#filter-data-by-value)
|
||||
- [Rename by regex](#rename-by-regex)
|
||||
- [Prepare-time-series](#prepare-time-series)
|
||||
|
||||
Keep reading for detailed descriptions of each type of transformation and the options available for each, as well as suggestions on how to use them.
|
||||
|
||||
## Reduce
|
||||
|
||||
The _Reduce_ transformation will apply a calculation to each field in the frame and return a single value. Time fields are removed when applying
|
||||
The _Reduce_ transformation applies a calculation to each field in the frame and return a single value. Time fields are removed when applying
|
||||
this transformation.
|
||||
|
||||
Consider the input:
|
||||
|
||||
Query A:
|
||||
|
||||
| Time | Temp | Uptime |
|
||||
| ------------------- | ------- | ------- |
|
||||
| 2020-07-07 11:34:20 | 12.3 | 256122 |
|
||||
| 2020-07-07 11:24:20 | 15.4 | 1230233 |
|
||||
| Time | Temp | Uptime |
|
||||
| ------------------- | ---- | ------- |
|
||||
| 2020-07-07 11:34:20 | 12.3 | 256122 |
|
||||
| 2020-07-07 11:24:20 | 15.4 | 1230233 |
|
||||
|
||||
Query B:
|
||||
|
||||
| Time | AQI | Errors |
|
||||
| ------------------- | ------- | ------ |
|
||||
| 2020-07-07 11:34:20 | 6.5 | 15 |
|
||||
| 2020-07-07 11:24:20 | 3.2 | 5 |
|
||||
| Time | AQI | Errors |
|
||||
| ------------------- | --- | ------ |
|
||||
| 2020-07-07 11:34:20 | 6.5 | 15 |
|
||||
| 2020-07-07 11:24:20 | 3.2 | 5 |
|
||||
|
||||
The reduce transformer has two modes:
|
||||
|
||||
- **Series to rows -** Creates a row for each field and a column for each calculation.
|
||||
- **Reduce fields -** Keeps the existing frame structure, but collapses each field into a single value.
|
||||
|
||||
For example, if you used the **First** and **Last** calculation with a **Series to rows** transformation, then
|
||||
the result would be:
|
||||
|
||||
| Field | First | Last |
|
||||
| ------- | ------- | ------- |
|
||||
| Temp | 12.3 | 15.4 |
|
||||
| Uptime | 256122 | 1230233 |
|
||||
| AQI | 6.5 | 3.2 |
|
||||
| Errors | 15 | 5 |
|
||||
| Field | First | Last |
|
||||
| ------ | ------ | ------- |
|
||||
| Temp | 12.3 | 15.4 |
|
||||
| Uptime | 256122 | 1230233 |
|
||||
| AQI | 6.5 | 3.2 |
|
||||
| Errors | 15 | 5 |
|
||||
|
||||
The **Reduce fields** with the **Last** calculation,
|
||||
results in two frames, each with one row:
|
||||
|
||||
Query A:
|
||||
|
||||
| Temp | Uptime |
|
||||
| ------- | ------- |
|
||||
| 15.4 | 1230233 |
|
||||
| Temp | Uptime |
|
||||
| ---- | ------- |
|
||||
| 15.4 | 1230233 |
|
||||
|
||||
Query B:
|
||||
|
||||
| AQI | Errors |
|
||||
| ------- | ------ |
|
||||
| 3.2 | 5 |
|
||||
|
||||
|
||||
| AQI | Errors |
|
||||
| --- | ------ |
|
||||
| 3.2 | 5 |
|
||||
|
||||
## Merge
|
||||
|
||||
@@ -184,7 +184,7 @@ Use this transformation to add a new field calculated from two other fields. Eac
|
||||
- **Binary option -** Apply basic math operation(sum, multiply, etc) on values in a single row from two selected fields.
|
||||
- **Field name -** Select the names of fields you want to use in the calculation for the new field.
|
||||
- **Calculation -** If you select **Reduce row** mode, then the **Calculation** field appears. Click in the field to see a list of calculation choices you can use to create the new field. For information about available calculations, refer to the [Calculation list]({{< relref "../calculations-list.md" >}}).
|
||||
- **Operation -** If you select **Binary option** mode, then the **Operation** fields appear. These fields allow you to do basic math operations on values in a single row from two selected fields. You can also use numerical values for binary operations.
|
||||
- **Operation -** If you select **Binary option** mode, then the **Operation** fields appear. These fields allow you to do basic math operations on values in a single row from two selected fields. You can also use numerical values for binary operations.
|
||||
- **Alias -** (Optional) Enter the name of your new field. If you leave this blank, then the field will be named to match the calculation.
|
||||
- **Replace all fields -** (Optional) Select this option if you want to hide all other fields and display only your calculated field in the visualization.
|
||||
|
||||
@@ -198,8 +198,8 @@ This transformation changes time series results that include labels or tags into
|
||||
|
||||
Given a query result of two time series:
|
||||
|
||||
* Series 1: labels Server=Server A, Datacenter=EU
|
||||
* Series 2: labels Server=Server B, Datacenter=EU
|
||||
- Series 1: labels Server=Server A, Datacenter=EU
|
||||
- Series 2: labels Server=Server B, Datacenter=EU
|
||||
|
||||
This would result in a table like this:
|
||||
|
||||
@@ -222,18 +222,18 @@ The labels to fields transformer is internally two separate transformations. The
|
||||
|
||||
To illustrate this, here is an example where you have two queries that return time series with no overlapping labels.
|
||||
|
||||
* Series 1: labels Server=ServerA
|
||||
* Series 2: labels Datacenter=EU
|
||||
- Series 1: labels Server=ServerA
|
||||
- Series 2: labels Datacenter=EU
|
||||
|
||||
This will first result in these two tables:
|
||||
|
||||
| Time | Server | Value |
|
||||
| ------------------- | ------- | ----- |
|
||||
| 2020-07-07 11:34:20 | ServerA | 10
|
||||
| 2020-07-07 11:34:20 | ServerA | 10 |
|
||||
|
||||
| Time | Datacenter | Value |
|
||||
| ------------------- | ---------- | ----- |
|
||||
| 2020-07-07 11:34:20 | EU | 20
|
||||
| 2020-07-07 11:34:20 | EU | 20 |
|
||||
|
||||
After merge:
|
||||
|
||||
@@ -249,7 +249,6 @@ After merge:
|
||||
This transformation will sort each frame by the configured field, When `reverse` is checked, the values will return in
|
||||
the opposite order.
|
||||
|
||||
|
||||
## Group by
|
||||
|
||||
> **Note:** This transformation is available in Grafana 7.2+.
|
||||
@@ -314,26 +313,25 @@ This transformation allows you to extract some key information out of your time
|
||||
|
||||
> **Note:** This transformation is available in Grafana 7.3+.
|
||||
|
||||
This transformation combines all fields from all frames into one result. Consider:
|
||||
This transformation combines all fields from all frames into one result. Consider:
|
||||
|
||||
Query A:
|
||||
|
||||
| Temp | Uptime |
|
||||
| ------- | ------- |
|
||||
| 15.4 | 1230233 |
|
||||
| Temp | Uptime |
|
||||
| ---- | ------- |
|
||||
| 15.4 | 1230233 |
|
||||
|
||||
Query B:
|
||||
|
||||
| AQI | Errors |
|
||||
| ------- | ------ |
|
||||
| 3.2 | 5 |
|
||||
|
||||
| AQI | Errors |
|
||||
| --- | ------ |
|
||||
| 3.2 | 5 |
|
||||
|
||||
After you concatenate the fields, the data frame would be:
|
||||
|
||||
| Temp | Uptime | AQI | Errors |
|
||||
| ------- | ------- | ------- | ------ |
|
||||
| 15.4 | 1230233 | 3.2 | 5 |
|
||||
| Temp | Uptime | AQI | Errors |
|
||||
| ---- | ------- | --- | ------ |
|
||||
| 15.4 | 1230233 | 3.2 | 5 |
|
||||
|
||||
## Series to rows
|
||||
|
||||
@@ -450,3 +448,15 @@ In the following example, we are stripping the prefix from event types. In the b
|
||||
With the transformation applied, you can see we are left with just the remainder of the string.
|
||||
|
||||
{{< figure src="/static/img/docs/transformations/rename-by-regex-after-7-3.png" class="docs-image--no-shadow" max-width= "1100px" >}}
|
||||
|
||||
## Prepare time series
|
||||
|
||||
> **Note:** This transformation is available in Grafana 7.5.10+ and Grafana 8.0.6+.
|
||||
|
||||
Prepare time series transformation is useful when a data source returns time series data in a format that isn't supported by the panel you want to use. [Read more about the different data frame formats here]({{< relref "../../developers/plugins/data-frames.md" >}}).
|
||||
|
||||
This transformation helps you resolve this issue by converting the time series data from either the wide format to the long format or the other way around.
|
||||
|
||||
Select the `Multi-frame time series` option to transform the time series data frame from the wide to the long format.
|
||||
|
||||
Select the `Wide time series` option to transform the time series data frame from the long to the wide format.
|
||||
|
||||
@@ -8,6 +8,8 @@ weight = 10000
|
||||
Here you can find detailed release notes that list everything that is included in every release as well as notices
|
||||
about deprecations, breaking changes as well as changes that relate to plugin development.
|
||||
|
||||
- [Release notes for 7.5.11]({{< relref "release-notes-7-5-11" >}})
|
||||
- [Release notes for 7.5.10]({{< relref "release-notes-7-5-10" >}})
|
||||
- [Release notes for 7.5.9]({{< relref "release-notes-7-5-9" >}})
|
||||
- [Release notes for 7.5.8]({{< relref "release-notes-7-5-8" >}})
|
||||
- [Release notes for 7.5.7]({{< relref "release-notes-7-5-7" >}})
|
||||
|
||||
14
docs/sources/release-notes/release-notes-7-5-10.md
Normal file
14
docs/sources/release-notes/release-notes-7-5-10.md
Normal file
@@ -0,0 +1,14 @@
|
||||
+++
|
||||
title = "Release notes for Grafana 7.5.10"
|
||||
[_build]
|
||||
list = false
|
||||
+++
|
||||
|
||||
<!-- Auto generated by update changelog github action -->
|
||||
|
||||
# Release notes for Grafana 7.5.10
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* **[v7.5.x] Transformations:** add 'prepare time series' transformer. [#36749](https://github.com/grafana/grafana/pull/36749), [@mckn](https://github.com/mckn)
|
||||
|
||||
13
docs/sources/release-notes/release-notes-7-5-11.md
Normal file
13
docs/sources/release-notes/release-notes-7-5-11.md
Normal file
@@ -0,0 +1,13 @@
|
||||
+++
|
||||
title = "Release notes for Grafana 7.5.11"
|
||||
[_build]
|
||||
list = false
|
||||
+++
|
||||
|
||||
<!-- Auto generated by update changelog github action -->
|
||||
|
||||
# Release notes for Grafana 7.5.11
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- **Security**: Fixes CVE-2021-39226. For more information, see our [blog](https://grafana.com/blog/2021/10/05/grafana-7.5.11-and-8.1.6-released-with-critical-security-fix/)
|
||||
@@ -6,7 +6,7 @@ weight = 200
|
||||
|
||||
# Add a custom variable
|
||||
|
||||
Use a _custom_ variable for values that do not change. This might be numbers, strings, or even other variables.
|
||||
Use a _custom_ variable for a value that does not change, such as a number or a string.
|
||||
|
||||
For example, if you have server names or region names that never change, then you might want to create them as custom variables rather than query variables. Because they do not change, you might use them in [chained variables]({{< relref "chained-variables.md" >}}) rather than other query variables. That would reduce the number of queries Grafana must send when chained variables are updated.
|
||||
|
||||
@@ -24,7 +24,7 @@ For example, if you have server names or region names that never change, then yo
|
||||
|
||||
## Enter Custom Options
|
||||
|
||||
1. In the **Values separated by comma** list, enter the values for this variable in a comma-separated list. You can include numbers, strings, other variables or key/value pairs separated by a space and a colon, i.e. `key1 : value1,key2 : value2`.
|
||||
1. In the **Values separated by comma** list, enter the values for this variable in a comma-separated list. You can include numbers, strings, or key/value pairs separated by a space and a colon. For example, `key1 : value1,key2 : value2`.
|
||||
1. (optional) Enter [Selection Options]({{< relref "../variable-selection-options.md" >}}).
|
||||
1. In **Preview of values**, Grafana displays a list of the current variable values. Review them to ensure they match what you expect.
|
||||
1. Click **Add** to add the variable to the dashboard.
|
||||
|
||||
4
go.mod
4
go.mod
@@ -107,3 +107,7 @@ require (
|
||||
)
|
||||
|
||||
replace github.com/apache/thrift => github.com/apache/thrift v0.14.1
|
||||
|
||||
replace gopkg.in/macaron.v1 v1.4.0 => ./pkg/macaron
|
||||
|
||||
replace github.com/go-macaron/binding => ./pkg/macaron/binding
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
"packages": [
|
||||
"packages/*"
|
||||
],
|
||||
"version": "7.5.10"
|
||||
"version": "7.5.14"
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"license": "Apache-2.0",
|
||||
"private": true,
|
||||
"name": "grafana",
|
||||
"version": "7.5.10",
|
||||
"version": "7.5.14",
|
||||
"repository": "github:grafana/grafana",
|
||||
"scripts": {
|
||||
"api-tests": "jest --notify --watch --config=devenv/e2e-api-tests/jest.js",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"author": "Grafana Labs",
|
||||
"license": "Apache-2.0",
|
||||
"name": "@grafana/data",
|
||||
"version": "7.5.10",
|
||||
"version": "7.5.14",
|
||||
"description": "Grafana Data Library",
|
||||
"keywords": [
|
||||
"typescript"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"author": "Grafana Labs",
|
||||
"license": "Apache-2.0",
|
||||
"name": "@grafana/e2e-selectors",
|
||||
"version": "7.5.10",
|
||||
"version": "7.5.14",
|
||||
"description": "Grafana End-to-End Test Selectors Library",
|
||||
"keywords": [
|
||||
"cli",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"author": "Grafana Labs",
|
||||
"license": "Apache-2.0",
|
||||
"name": "@grafana/e2e",
|
||||
"version": "7.5.10",
|
||||
"version": "7.5.14",
|
||||
"description": "Grafana End-to-End Test Library",
|
||||
"keywords": [
|
||||
"cli",
|
||||
@@ -44,7 +44,7 @@
|
||||
"types": "src/index.ts",
|
||||
"dependencies": {
|
||||
"@cypress/webpack-preprocessor": "4.1.3",
|
||||
"@grafana/e2e-selectors": "7.5.10",
|
||||
"@grafana/e2e-selectors": "7.5.14",
|
||||
"@grafana/tsconfig": "^1.0.0-rc1",
|
||||
"@mochajs/json-file-reporter": "^1.2.0",
|
||||
"blink-diff": "1.0.13",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"author": "Grafana Labs",
|
||||
"license": "Apache-2.0",
|
||||
"name": "@grafana/runtime",
|
||||
"version": "7.5.10",
|
||||
"version": "7.5.14",
|
||||
"description": "Grafana Runtime Library",
|
||||
"keywords": [
|
||||
"grafana",
|
||||
@@ -22,8 +22,8 @@
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@grafana/data": "7.5.10",
|
||||
"@grafana/ui": "7.5.10",
|
||||
"@grafana/data": "7.5.14",
|
||||
"@grafana/ui": "7.5.14",
|
||||
"systemjs": "0.20.19",
|
||||
"systemjs-plugin-css": "0.1.37"
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"author": "Grafana Labs",
|
||||
"license": "Apache-2.0",
|
||||
"name": "@grafana/toolkit",
|
||||
"version": "7.5.10",
|
||||
"version": "7.5.14",
|
||||
"description": "Grafana Toolkit",
|
||||
"keywords": [
|
||||
"grafana",
|
||||
@@ -28,10 +28,10 @@
|
||||
"dependencies": {
|
||||
"@babel/core": "7.9.0",
|
||||
"@babel/preset-env": "7.9.0",
|
||||
"@grafana/data": "7.5.10",
|
||||
"@grafana/data": "7.5.14",
|
||||
"@grafana/eslint-config": "2.3.0",
|
||||
"@grafana/tsconfig": "^1.0.0-rc1",
|
||||
"@grafana/ui": "7.5.10",
|
||||
"@grafana/ui": "7.5.14",
|
||||
"@types/command-exists": "^1.2.0",
|
||||
"@types/expect-puppeteer": "3.3.1",
|
||||
"@types/fs-extra": "^8.1.0",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"author": "Grafana Labs",
|
||||
"license": "Apache-2.0",
|
||||
"name": "@grafana/ui",
|
||||
"version": "7.5.10",
|
||||
"version": "7.5.14",
|
||||
"description": "Grafana Components Library",
|
||||
"keywords": [
|
||||
"grafana",
|
||||
@@ -28,8 +28,8 @@
|
||||
"dependencies": {
|
||||
"@emotion/core": "10.0.27",
|
||||
"@grafana/aws-sdk": "0.0.3",
|
||||
"@grafana/data": "7.5.10",
|
||||
"@grafana/e2e-selectors": "7.5.10",
|
||||
"@grafana/data": "7.5.14",
|
||||
"@grafana/e2e-selectors": "7.5.14",
|
||||
"@grafana/slate-react": "0.22.9-grafana",
|
||||
"@grafana/tsconfig": "^1.0.0-rc1",
|
||||
"@iconscout/react-unicons": "1.1.4",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@jaegertracing/jaeger-ui-components",
|
||||
"version": "7.5.10",
|
||||
"version": "7.5.14",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -14,8 +14,8 @@
|
||||
"typescript": "4.1.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@grafana/data": "7.5.10",
|
||||
"@grafana/ui": "7.5.10",
|
||||
"@grafana/data": "7.5.14",
|
||||
"@grafana/ui": "7.5.14",
|
||||
"@types/classnames": "^2.2.7",
|
||||
"@types/deep-freeze": "^0.1.1",
|
||||
"@types/hoist-non-react-statics": "^3.3.1",
|
||||
|
||||
@@ -25,7 +25,7 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
reqGrafanaAdmin := middleware.ReqGrafanaAdmin
|
||||
reqEditorRole := middleware.ReqEditorRole
|
||||
reqOrgAdmin := middleware.ReqOrgAdmin
|
||||
reqCanAccessTeams := middleware.AdminOrFeatureEnabled(hs.Cfg.EditorsCanAdmin)
|
||||
reqCanAccessTeams := middleware.AdminOrEditorAndFeatureEnabled(hs.Cfg.EditorsCanAdmin)
|
||||
reqSnapshotPublicModeOrSignedIn := middleware.SnapshotPublicModeOrSignedIn(hs.Cfg)
|
||||
redirectFromLegacyDashboardURL := middleware.RedirectFromLegacyDashboardURL()
|
||||
redirectFromLegacyDashboardSoloURL := middleware.RedirectFromLegacyDashboardSoloURL(hs.Cfg)
|
||||
|
||||
@@ -144,6 +144,9 @@ func CreateDashboardSnapshot(c *models.ReqContext, cmd models.CreateDashboardSna
|
||||
// GET /api/snapshots/:key
|
||||
func GetDashboardSnapshot(c *models.ReqContext) response.Response {
|
||||
key := c.Params(":key")
|
||||
if len(key) == 0 {
|
||||
return response.Error(404, "Snapshot not found", nil)
|
||||
}
|
||||
query := &models.GetDashboardSnapshotQuery{Key: key}
|
||||
|
||||
err := bus.Dispatch(query)
|
||||
@@ -210,6 +213,9 @@ func deleteExternalDashboardSnapshot(externalUrl string) error {
|
||||
// GET /api/snapshots-delete/:deleteKey
|
||||
func DeleteDashboardSnapshotByDeleteKey(c *models.ReqContext) response.Response {
|
||||
key := c.Params(":deleteKey")
|
||||
if len(key) == 0 {
|
||||
return response.Error(404, "Snapshot not found", nil)
|
||||
}
|
||||
|
||||
query := &models.GetDashboardSnapshotQuery{DeleteKey: key}
|
||||
|
||||
@@ -240,6 +246,9 @@ func DeleteDashboardSnapshotByDeleteKey(c *models.ReqContext) response.Response
|
||||
// DELETE /api/snapshots/:key
|
||||
func DeleteDashboardSnapshot(c *models.ReqContext) response.Response {
|
||||
key := c.Params(":key")
|
||||
if len(key) == 0 {
|
||||
return response.Error(404, "Snapshot not found", nil)
|
||||
}
|
||||
|
||||
query := &models.GetDashboardSnapshotQuery{Key: key}
|
||||
|
||||
|
||||
@@ -312,6 +312,7 @@ func (hs *HTTPServer) addMiddlewaresAndStaticRoutes() {
|
||||
}
|
||||
|
||||
m.Use(middleware.Recovery(hs.Cfg))
|
||||
m.Use(middleware.CSRF(hs.Cfg.LoginCookieName))
|
||||
|
||||
for _, route := range plugins.StaticRoutes {
|
||||
pluginRoute := path.Join("/public/plugins/", route.PluginId)
|
||||
|
||||
@@ -49,6 +49,7 @@ func (t *handleResponseTransport) RoundTrip(req *http.Request) (*http.Response,
|
||||
return nil, err
|
||||
}
|
||||
res.Header.Del("Set-Cookie")
|
||||
proxyutil.SetProxyResponseHeaders(res.Header)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -605,6 +605,18 @@ func TestDataSourceProxy_requestHandling(t *testing.T) {
|
||||
assert.Equal(t, "important_cookie=important_value", proxy.ctx.Resp.Header().Get("Set-Cookie"))
|
||||
})
|
||||
|
||||
t.Run("When response should set Content-Security-Policy header", func(t *testing.T) {
|
||||
ctx, ds := setUp(t)
|
||||
dsPlugin := &plugins.DataSourcePlugin{}
|
||||
proxy, err := NewDataSourceProxy(ds, dsPlugin, ctx, "/render", &setting.Cfg{})
|
||||
require.NoError(t, err)
|
||||
|
||||
proxy.HandleRequest()
|
||||
|
||||
require.NoError(t, writeErr)
|
||||
assert.Equal(t, "sandbox", proxy.ctx.Resp.Header().Get("Content-Security-Policy"))
|
||||
})
|
||||
|
||||
t.Run("Data source returns status code 401", func(t *testing.T) {
|
||||
ctx, ds := setUp(t, setUpCfg{
|
||||
writeCb: func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -71,5 +71,11 @@ func NewApiPluginProxy(ctx *models.ReqContext, proxyPath string, route *plugins.
|
||||
}
|
||||
}
|
||||
|
||||
return &httputil.ReverseProxy{Director: director}
|
||||
return &httputil.ReverseProxy{Director: director, ModifyResponse: modifyResponse}
|
||||
}
|
||||
|
||||
func modifyResponse(resp *http.Response) error {
|
||||
proxyutil.SetProxyResponseHeaders(resp.Header)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package pluginproxy
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
@@ -11,6 +12,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
macaron "gopkg.in/macaron.v1"
|
||||
)
|
||||
|
||||
func TestPluginProxy(t *testing.T) {
|
||||
@@ -148,6 +150,48 @@ func TestPluginProxy(t *testing.T) {
|
||||
)
|
||||
assert.Equal(t, "https://example.com", req.URL.String())
|
||||
})
|
||||
|
||||
t.Run("When proxying a request should set expected response headers", func(t *testing.T) {
|
||||
requestHandled := false
|
||||
backendServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(200)
|
||||
_, _ = w.Write([]byte("I am the backend"))
|
||||
requestHandled = true
|
||||
}))
|
||||
|
||||
responseRecorder := &closeNotifierResponseRecorder{
|
||||
ResponseRecorder: httptest.NewRecorder(),
|
||||
}
|
||||
responseWriter := macaron.NewResponseWriter("GET", responseRecorder)
|
||||
|
||||
t.Cleanup(responseRecorder.Close)
|
||||
t.Cleanup(backendServer.Close)
|
||||
|
||||
route := &plugins.AppPluginRoute{
|
||||
Path: "/",
|
||||
URL: backendServer.URL,
|
||||
}
|
||||
|
||||
ctx := &models.ReqContext{
|
||||
SignedInUser: &models.SignedInUser{},
|
||||
Context: &macaron.Context{
|
||||
Req: macaron.Request{
|
||||
Request: httptest.NewRequest("GET", "/", nil),
|
||||
},
|
||||
Resp: responseWriter,
|
||||
},
|
||||
}
|
||||
proxy := NewApiPluginProxy(ctx, "", route, "", &setting.Cfg{})
|
||||
proxy.ServeHTTP(ctx.Resp, ctx.Req.Request)
|
||||
|
||||
for {
|
||||
if requestHandled {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
require.Equal(t, "sandbox", ctx.Resp.Header().Get("Content-Security-Policy"))
|
||||
})
|
||||
}
|
||||
|
||||
// getPluginProxiedRequest is a helper for easier setup of tests based on global config and ReqContext.
|
||||
|
||||
@@ -101,16 +101,11 @@ func (hs *HTTPServer) SearchTeams(c *models.ReqContext) response.Response {
|
||||
page = 1
|
||||
}
|
||||
|
||||
var userIdFilter int64
|
||||
if hs.Cfg.EditorsCanAdmin && c.OrgRole != models.ROLE_ADMIN {
|
||||
userIdFilter = c.SignedInUser.UserId
|
||||
}
|
||||
|
||||
query := models.SearchTeamsQuery{
|
||||
OrgId: c.OrgId,
|
||||
Query: c.Query("query"),
|
||||
Name: c.Query("name"),
|
||||
UserIdFilter: userIdFilter,
|
||||
UserIdFilter: userFilter(hs.Cfg.EditorsCanAdmin, c),
|
||||
Page: page,
|
||||
Limit: perPage,
|
||||
SignedInUser: c.SignedInUser,
|
||||
@@ -131,6 +126,19 @@ func (hs *HTTPServer) SearchTeams(c *models.ReqContext) response.Response {
|
||||
return response.JSON(200, query.Result)
|
||||
}
|
||||
|
||||
// UserFilter returns the user ID used in a filter when querying a team
|
||||
// 1. If the user is a viewer or editor, this will return the user's ID.
|
||||
// 2. If EditorsCanAdmin is enabled and the user is an editor, this will return models.FilterIgnoreUser (0)
|
||||
// 3. If the user is an admin, this will return models.FilterIgnoreUser (0)
|
||||
func userFilter(editorsCanAdmin bool, c *models.ReqContext) int64 {
|
||||
userIdFilter := c.SignedInUser.UserId
|
||||
if (editorsCanAdmin && c.OrgRole == models.ROLE_EDITOR) || c.OrgRole == models.ROLE_ADMIN {
|
||||
userIdFilter = models.FilterIgnoreUser
|
||||
}
|
||||
|
||||
return userIdFilter
|
||||
}
|
||||
|
||||
// GET /api/teams/:teamId
|
||||
func (hs *HTTPServer) GetTeamByID(c *models.ReqContext) response.Response {
|
||||
query := models.GetTeamByIdQuery{
|
||||
@@ -138,6 +146,7 @@ func (hs *HTTPServer) GetTeamByID(c *models.ReqContext) response.Response {
|
||||
Id: c.ParamsInt64(":teamId"),
|
||||
SignedInUser: c.SignedInUser,
|
||||
HiddenUsers: hs.Cfg.HiddenUsers,
|
||||
UserIdFilter: userFilter(hs.Cfg.EditorsCanAdmin, c),
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
|
||||
@@ -15,6 +15,10 @@ import (
|
||||
func (hs *HTTPServer) GetTeamMembers(c *models.ReqContext) response.Response {
|
||||
query := models.GetTeamMembersQuery{OrgId: c.OrgId, TeamId: c.ParamsInt64(":teamId")}
|
||||
|
||||
if err := teamguardian.CanAdmin(hs.Bus, query.OrgId, query.TeamId, c.SignedInUser); err != nil {
|
||||
return response.Error(403, "Not allowed to list team members", err)
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
return response.Error(500, "Failed to get Team Members", err)
|
||||
}
|
||||
|
||||
191
pkg/macaron/LICENSE
Executable file
191
pkg/macaron/LICENSE
Executable file
@@ -0,0 +1,191 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction, and
|
||||
distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by the copyright
|
||||
owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all other entities
|
||||
that control, are controlled by, or are under common control with that entity.
|
||||
For the purposes of this definition, "control" means (i) the power, direct or
|
||||
indirect, to cause the direction or management of such entity, whether by
|
||||
contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity exercising
|
||||
permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications, including
|
||||
but not limited to software source code, documentation source, and configuration
|
||||
files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical transformation or
|
||||
translation of a Source form, including but not limited to compiled object code,
|
||||
generated documentation, and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or Object form, made
|
||||
available under the License, as indicated by a copyright notice that is included
|
||||
in or attached to the work (an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object form, that
|
||||
is based on (or derived from) the Work and for which the editorial revisions,
|
||||
annotations, elaborations, or other modifications represent, as a whole, an
|
||||
original work of authorship. For the purposes of this License, Derivative Works
|
||||
shall not include works that remain separable from, or merely link (or bind by
|
||||
name) to the interfaces of, the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including the original version
|
||||
of the Work and any modifications or additions to that Work or Derivative Works
|
||||
thereof, that is intentionally submitted to Licensor for inclusion in the Work
|
||||
by the copyright owner or by an individual or Legal Entity authorized to submit
|
||||
on behalf of the copyright owner. For the purposes of this definition,
|
||||
"submitted" means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems, and
|
||||
issue tracking systems that are managed by, or on behalf of, the Licensor for
|
||||
the purpose of discussing and improving the Work, but excluding communication
|
||||
that is conspicuously marked or otherwise designated in writing by the copyright
|
||||
owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf
|
||||
of whom a Contribution has been received by Licensor and subsequently
|
||||
incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License.
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the Work and such
|
||||
Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License.
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable (except as stated in this section) patent license to make, have
|
||||
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
|
||||
such license applies only to those patent claims licensable by such Contributor
|
||||
that are necessarily infringed by their Contribution(s) alone or by combination
|
||||
of their Contribution(s) with the Work to which such Contribution(s) was
|
||||
submitted. If You institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
|
||||
Contribution incorporated within the Work constitutes direct or contributory
|
||||
patent infringement, then any patent licenses granted to You under this License
|
||||
for that Work shall terminate as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution.
|
||||
|
||||
You may reproduce and distribute copies of the Work or Derivative Works thereof
|
||||
in any medium, with or without modifications, and in Source or Object form,
|
||||
provided that You meet the following conditions:
|
||||
|
||||
You must give any other recipients of the Work or Derivative Works a copy of
|
||||
this License; and
|
||||
You must cause any modified files to carry prominent notices stating that You
|
||||
changed the files; and
|
||||
You must retain, in the Source form of any Derivative Works that You distribute,
|
||||
all copyright, patent, trademark, and attribution notices from the Source form
|
||||
of the Work, excluding those notices that do not pertain to any part of the
|
||||
Derivative Works; and
|
||||
If the Work includes a "NOTICE" text file as part of its distribution, then any
|
||||
Derivative Works that You distribute must include a readable copy of the
|
||||
attribution notices contained within such NOTICE file, excluding those notices
|
||||
that do not pertain to any part of the Derivative Works, in at least one of the
|
||||
following places: within a NOTICE text file distributed as part of the
|
||||
Derivative Works; within the Source form or documentation, if provided along
|
||||
with the Derivative Works; or, within a display generated by the Derivative
|
||||
Works, if and wherever such third-party notices normally appear. The contents of
|
||||
the NOTICE file are for informational purposes only and do not modify the
|
||||
License. You may add Your own attribution notices within Derivative Works that
|
||||
You distribute, alongside or as an addendum to the NOTICE text from the Work,
|
||||
provided that such additional attribution notices cannot be construed as
|
||||
modifying the License.
|
||||
You may add Your own copyright statement to Your modifications and may provide
|
||||
additional or different license terms and conditions for use, reproduction, or
|
||||
distribution of Your modifications, or for any such Derivative Works as a whole,
|
||||
provided Your use, reproduction, and distribution of the Work otherwise complies
|
||||
with the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions.
|
||||
|
||||
Unless You explicitly state otherwise, any Contribution intentionally submitted
|
||||
for inclusion in the Work by You to the Licensor shall be under the terms and
|
||||
conditions of this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify the terms of
|
||||
any separate license agreement you may have executed with Licensor regarding
|
||||
such Contributions.
|
||||
|
||||
6. Trademarks.
|
||||
|
||||
This License does not grant permission to use the trade names, trademarks,
|
||||
service marks, or product names of the Licensor, except as required for
|
||||
reasonable and customary use in describing the origin of the Work and
|
||||
reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty.
|
||||
|
||||
Unless required by applicable law or agreed to in writing, Licensor provides the
|
||||
Work (and each Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
|
||||
including, without limitation, any warranties or conditions of TITLE,
|
||||
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
|
||||
solely responsible for determining the appropriateness of using or
|
||||
redistributing the Work and assume any risks associated with Your exercise of
|
||||
permissions under this License.
|
||||
|
||||
8. Limitation of Liability.
|
||||
|
||||
In no event and under no legal theory, whether in tort (including negligence),
|
||||
contract, or otherwise, unless required by applicable law (such as deliberate
|
||||
and grossly negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special, incidental,
|
||||
or consequential damages of any character arising as a result of this License or
|
||||
out of the use or inability to use the Work (including but not limited to
|
||||
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
|
||||
any and all other commercial damages or losses), even if such Contributor has
|
||||
been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability.
|
||||
|
||||
While redistributing the Work or Derivative Works thereof, You may choose to
|
||||
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
|
||||
other liability obligations and/or rights consistent with this License. However,
|
||||
in accepting such obligations, You may act only on Your own behalf and on Your
|
||||
sole responsibility, not on behalf of any other Contributor, and only if You
|
||||
agree to indemnify, defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason of your
|
||||
accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work
|
||||
|
||||
To apply the Apache License to your work, attach the following boilerplate
|
||||
notice, with the fields enclosed by brackets "[]" replaced with your own
|
||||
identifying information. (Don't include the brackets!) The text should be
|
||||
enclosed in the appropriate comment syntax for the file format. We also
|
||||
recommend that a file or class name and description of purpose be included on
|
||||
the same "printed page" as the copyright notice for easier identification within
|
||||
third-party archives.
|
||||
|
||||
Copyright 2014 The Macaron Authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
96
pkg/macaron/README.md
Executable file
96
pkg/macaron/README.md
Executable file
@@ -0,0 +1,96 @@
|
||||
# Macaron
|
||||
|
||||
[](https://github.com/go-macaron/macaron/actions?query=workflow%3AGo)
|
||||
[](https://codecov.io/gh/go-macaron/macaron)
|
||||
[](https://pkg.go.dev/gopkg.in/macaron.v1?tab=doc)
|
||||
[](https://sourcegraph.com/github.com/go-macaron/macaron)
|
||||
|
||||

|
||||
|
||||
Package macaron is a high productive and modular web framework in Go.
|
||||
|
||||
## Getting Started
|
||||
|
||||
The minimum requirement of Go is **1.6**.
|
||||
|
||||
To install Macaron:
|
||||
|
||||
go get gopkg.in/macaron.v1
|
||||
|
||||
The very basic usage of Macaron:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import "gopkg.in/macaron.v1"
|
||||
|
||||
func main() {
|
||||
m := macaron.Classic()
|
||||
m.Get("/", func() string {
|
||||
return "Hello world!"
|
||||
})
|
||||
m.Run()
|
||||
}
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- Powerful routing with suburl.
|
||||
- Flexible routes combinations.
|
||||
- Unlimited nested group routers.
|
||||
- Directly integrate with existing services.
|
||||
- Dynamically change template files at runtime.
|
||||
- Allow to use in-memory template and static files.
|
||||
- Easy to plugin/unplugin features with modular design.
|
||||
- Handy dependency injection powered by [inject](https://github.com/codegangsta/inject).
|
||||
- Better router layer and less reflection make faster speed.
|
||||
|
||||
## Middlewares
|
||||
|
||||
Middlewares allow you easily plugin/unplugin features for your Macaron applications.
|
||||
|
||||
There are already many [middlewares](https://github.com/go-macaron) to simplify your work:
|
||||
|
||||
- render - Go template engine
|
||||
- static - Serves static files
|
||||
- [gzip](https://github.com/go-macaron/gzip) - Gzip compression to all responses
|
||||
- [binding](https://github.com/go-macaron/binding) - Request data binding and validation
|
||||
- [i18n](https://github.com/go-macaron/i18n) - Internationalization and Localization
|
||||
- [cache](https://github.com/go-macaron/cache) - Cache manager
|
||||
- [session](https://github.com/go-macaron/session) - Session manager
|
||||
- [csrf](https://github.com/go-macaron/csrf) - Generates and validates csrf tokens
|
||||
- [captcha](https://github.com/go-macaron/captcha) - Captcha service
|
||||
- [pongo2](https://github.com/go-macaron/pongo2) - Pongo2 template engine support
|
||||
- [sockets](https://github.com/go-macaron/sockets) - WebSockets channels binding
|
||||
- [bindata](https://github.com/go-macaron/bindata) - Embed binary data as static and template files
|
||||
- [toolbox](https://github.com/go-macaron/toolbox) - Health check, pprof, profile and statistic services
|
||||
- [oauth2](https://github.com/go-macaron/oauth2) - OAuth 2.0 backend
|
||||
- [authz](https://github.com/go-macaron/authz) - ACL/RBAC/ABAC authorization based on Casbin
|
||||
- [switcher](https://github.com/go-macaron/switcher) - Multiple-site support
|
||||
- [method](https://github.com/go-macaron/method) - HTTP method override
|
||||
- [permissions2](https://github.com/xyproto/permissions2) - Cookies, users and permissions
|
||||
- [renders](https://github.com/go-macaron/renders) - Beego-like render engine(Macaron has built-in template engine, this is another option)
|
||||
- [piwik](https://github.com/veecue/piwik-middleware) - Server-side piwik analytics
|
||||
|
||||
## Use Cases
|
||||
|
||||
- [Gogs](https://gogs.io): A painless self-hosted Git Service
|
||||
- [Grafana](http://grafana.org/): The open platform for beautiful analytics and monitoring
|
||||
- [Peach](https://peachdocs.org): A modern web documentation server
|
||||
- [Go Walker](https://gowalker.org): Go online API documentation
|
||||
- [Critical Stack Intel](https://intel.criticalstack.com/): A 100% free intel marketplace from Critical Stack, Inc.
|
||||
|
||||
## Getting Help
|
||||
|
||||
- [API Reference](https://gowalker.org/gopkg.in/macaron.v1)
|
||||
- [Documentation](https://go-macaron.com)
|
||||
- [FAQs](https://go-macaron.com/docs/faqs)
|
||||
|
||||
## Credits
|
||||
|
||||
- Basic design of [Martini](https://github.com/go-martini/martini).
|
||||
- Logo is modified by [@insionng](https://github.com/insionng) based on [Tribal Dragon](http://xtremeyamazaki.deviantart.com/art/Tribal-Dragon-27005087).
|
||||
|
||||
## License
|
||||
|
||||
This project is under the Apache License, Version 2.0. See the [LICENSE](LICENSE) file for the full license text.
|
||||
813
pkg/macaron/binding/binding.go
Normal file
813
pkg/macaron/binding/binding.go
Normal file
@@ -0,0 +1,813 @@
|
||||
// Copyright 2014 Martini Authors
|
||||
// Copyright 2014 The Macaron Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
// Package binding is a middleware that provides request data binding and validation for Macaron.
|
||||
package binding
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/unknwon/com"
|
||||
"gopkg.in/macaron.v1"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func bind(ctx *macaron.Context, obj interface{}, ifacePtr ...interface{}) {
|
||||
if ctx.Req.Method == "POST" || ctx.Req.Method == "PUT" || ctx.Req.Method == "PATCH" || ctx.Req.Method == "DELETE" {
|
||||
contentType, _, _ := mime.ParseMediaType(ctx.Req.Header.Get("Content-Type"))
|
||||
switch contentType {
|
||||
case "form-urlencoded":
|
||||
_, _ = ctx.Invoke(Form(obj, ifacePtr...))
|
||||
case "multipart/form-data":
|
||||
_, _ = ctx.Invoke(MultipartForm(obj, ifacePtr...))
|
||||
case "application/json":
|
||||
_, _ = ctx.Invoke(Json(obj, ifacePtr...))
|
||||
case "text/yaml":
|
||||
_, _ = ctx.Invoke(Yaml(obj, ifacePtr...))
|
||||
default:
|
||||
var errors Errors
|
||||
if contentType == "" {
|
||||
errors.Add([]string{}, ERR_CONTENT_TYPE, "Empty Content-Type")
|
||||
} else {
|
||||
errors.Add([]string{}, ERR_CONTENT_TYPE, "Unsupported Content-Type")
|
||||
}
|
||||
ctx.Map(errors)
|
||||
ctx.Map(obj) // Map a fake struct so handler won't panic.
|
||||
}
|
||||
} else {
|
||||
_, _ = ctx.Invoke(Form(obj, ifacePtr...))
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
_JSON_CONTENT_TYPE = "application/json; charset=utf-8"
|
||||
_YAML_CONTENT_TYPE = "text/yaml; charset=utf-8"
|
||||
STATUS_UNPROCESSABLE_ENTITY = 422
|
||||
)
|
||||
|
||||
// errorHandler simply counts the number of errors in the
|
||||
// context and, if more than 0, writes a response with an
|
||||
// error code and a JSON payload describing the errors.
|
||||
// The response will have a JSON content-type.
|
||||
// Middleware remaining on the stack will not even see the request
|
||||
// if, by this point, there are any errors.
|
||||
// This is a "default" handler, of sorts, and you are
|
||||
// welcome to use your own instead. The Bind middleware
|
||||
// invokes this automatically for convenience.
|
||||
func errorHandler(errs Errors, rw http.ResponseWriter) {
|
||||
if len(errs) > 0 {
|
||||
rw.Header().Set("Content-Type", _JSON_CONTENT_TYPE)
|
||||
if errs.Has(ERR_DESERIALIZATION) {
|
||||
rw.WriteHeader(http.StatusBadRequest)
|
||||
} else if errs.Has(ERR_CONTENT_TYPE) {
|
||||
rw.WriteHeader(http.StatusUnsupportedMediaType)
|
||||
} else {
|
||||
rw.WriteHeader(STATUS_UNPROCESSABLE_ENTITY)
|
||||
}
|
||||
errOutput, _ := json.Marshal(errs)
|
||||
_, _ = rw.Write(errOutput)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// CustomErrorHandler will be invoked if errors occured.
|
||||
var CustomErrorHandler func(*macaron.Context, Errors)
|
||||
|
||||
// Bind wraps up the functionality of the Form and Json middleware
|
||||
// according to the Content-Type and verb of the request.
|
||||
// A Content-Type is required for POST and PUT requests.
|
||||
// Bind invokes the ErrorHandler middleware to bail out if errors
|
||||
// occurred. If you want to perform your own error handling, use
|
||||
// Form or Json middleware directly. An interface pointer can
|
||||
// be added as a second argument in order to map the struct to
|
||||
// a specific interface.
|
||||
func Bind(obj interface{}, ifacePtr ...interface{}) macaron.Handler {
|
||||
return func(ctx *macaron.Context) {
|
||||
bind(ctx, obj, ifacePtr...)
|
||||
if handler, ok := obj.(ErrorHandler); ok {
|
||||
_, _ = ctx.Invoke(handler.Error)
|
||||
} else if CustomErrorHandler != nil {
|
||||
_, _ = ctx.Invoke(CustomErrorHandler)
|
||||
} else {
|
||||
_, _ = ctx.Invoke(errorHandler)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BindIgnErr will do the exactly same thing as Bind but without any
|
||||
// error handling, which user has freedom to deal with them.
|
||||
// This allows user take advantages of validation.
|
||||
func BindIgnErr(obj interface{}, ifacePtr ...interface{}) macaron.Handler {
|
||||
return func(ctx *macaron.Context) {
|
||||
bind(ctx, obj, ifacePtr...)
|
||||
}
|
||||
}
|
||||
|
||||
// Form is middleware to deserialize form-urlencoded data from the request.
|
||||
// It gets data from the form-urlencoded body, if present, or from the
|
||||
// query string. It uses the http.Request.ParseForm() method
|
||||
// to perform deserialization, then reflection is used to map each field
|
||||
// into the struct with the proper type. Structs with primitive slice types
|
||||
// (bool, float, int, string) can support deserialization of repeated form
|
||||
// keys, for example: key=val1&key=val2&key=val3
|
||||
// An interface pointer can be added as a second argument in order
|
||||
// to map the struct to a specific interface.
|
||||
func Form(formStruct interface{}, ifacePtr ...interface{}) macaron.Handler {
|
||||
return func(ctx *macaron.Context) {
|
||||
var errors Errors
|
||||
|
||||
ensureNotPointer(formStruct)
|
||||
formStruct := reflect.New(reflect.TypeOf(formStruct))
|
||||
parseErr := ctx.Req.ParseForm()
|
||||
|
||||
// Format validation of the request body or the URL would add considerable overhead,
|
||||
// and ParseForm does not complain when URL encoding is off.
|
||||
// Because an empty request body or url can also mean absence of all needed values,
|
||||
// it is not in all cases a bad request, so let's return 422.
|
||||
if parseErr != nil {
|
||||
errors.Add([]string{}, ERR_DESERIALIZATION, parseErr.Error())
|
||||
}
|
||||
errors = mapForm(formStruct, ctx.Req.Form, nil, errors)
|
||||
validateAndMap(formStruct, ctx, errors, ifacePtr...)
|
||||
}
|
||||
}
|
||||
|
||||
// Maximum amount of memory to use when parsing a multipart form.
|
||||
// Set this to whatever value you prefer; default is 10 MB.
|
||||
var MaxMemory = int64(1024 * 1024 * 10)
|
||||
|
||||
// MultipartForm works much like Form, except it can parse multipart forms
|
||||
// and handle file uploads. Like the other deserialization middleware handlers,
|
||||
// you can pass in an interface to make the interface available for injection
|
||||
// into other handlers later.
|
||||
func MultipartForm(formStruct interface{}, ifacePtr ...interface{}) macaron.Handler {
|
||||
return func(ctx *macaron.Context) {
|
||||
var errors Errors
|
||||
ensureNotPointer(formStruct)
|
||||
formStruct := reflect.New(reflect.TypeOf(formStruct))
|
||||
// This if check is necessary due to https://github.com/martini-contrib/csrf/issues/6
|
||||
if ctx.Req.MultipartForm == nil {
|
||||
// Workaround for multipart forms returning nil instead of an error
|
||||
// when content is not multipart; see https://code.google.com/p/go/issues/detail?id=6334
|
||||
if multipartReader, err := ctx.Req.MultipartReader(); err != nil {
|
||||
errors.Add([]string{}, ERR_DESERIALIZATION, err.Error())
|
||||
} else {
|
||||
form, parseErr := multipartReader.ReadForm(MaxMemory)
|
||||
if parseErr != nil {
|
||||
errors.Add([]string{}, ERR_DESERIALIZATION, parseErr.Error())
|
||||
}
|
||||
|
||||
if ctx.Req.Form == nil {
|
||||
_ = ctx.Req.ParseForm()
|
||||
}
|
||||
for k, v := range form.Value {
|
||||
ctx.Req.Form[k] = append(ctx.Req.Form[k], v...)
|
||||
}
|
||||
|
||||
ctx.Req.MultipartForm = form
|
||||
}
|
||||
}
|
||||
errors = mapForm(formStruct, ctx.Req.MultipartForm.Value, ctx.Req.MultipartForm.File, errors)
|
||||
validateAndMap(formStruct, ctx, errors, ifacePtr...)
|
||||
}
|
||||
}
|
||||
|
||||
// Json is middleware to deserialize a JSON payload from the request
|
||||
// into the struct that is passed in. The resulting struct is then
|
||||
// validated, but no error handling is actually performed here.
|
||||
// An interface pointer can be added as a second argument in order
|
||||
// to map the struct to a specific interface.
|
||||
func Json(jsonStruct interface{}, ifacePtr ...interface{}) macaron.Handler {
|
||||
return func(ctx *macaron.Context) {
|
||||
var errors Errors
|
||||
ensureNotPointer(jsonStruct)
|
||||
jsonStruct := reflect.New(reflect.TypeOf(jsonStruct))
|
||||
if ctx.Req.Request.Body != nil {
|
||||
defer ctx.Req.Request.Body.Close()
|
||||
err := json.NewDecoder(ctx.Req.Request.Body).Decode(jsonStruct.Interface())
|
||||
if err != nil && err != io.EOF {
|
||||
errors.Add([]string{}, ERR_DESERIALIZATION, err.Error())
|
||||
}
|
||||
}
|
||||
if errors != nil {
|
||||
ctx.Map(errors)
|
||||
return
|
||||
}
|
||||
validateAndMap(jsonStruct, ctx, errors, ifacePtr...)
|
||||
}
|
||||
}
|
||||
|
||||
// Yaml is middleware to deserialize a YAML payload from the request
|
||||
// into the struct that is passed in. The resulting struct is then
|
||||
// validated, but no error handling is actually performed here.
|
||||
// An interface pointer can be added as a second argument in order
|
||||
// to map the struct to a specific interface.
|
||||
func Yaml(yamlStruct interface{}, ifacePtr ...interface{}) macaron.Handler {
|
||||
return func(ctx *macaron.Context) {
|
||||
var errors Errors
|
||||
ensureNotPointer(yamlStruct)
|
||||
yamlStruct := reflect.New(reflect.TypeOf(yamlStruct))
|
||||
if ctx.Req.Request.Body != nil {
|
||||
defer ctx.Req.Request.Body.Close()
|
||||
err := yaml.NewDecoder(ctx.Req.Request.Body).Decode(yamlStruct.Interface())
|
||||
if err != nil && err != io.EOF {
|
||||
errors.Add([]string{}, ERR_DESERIALIZATION, err.Error())
|
||||
}
|
||||
}
|
||||
if errors != nil {
|
||||
ctx.Map(errors)
|
||||
return
|
||||
}
|
||||
validateAndMap(yamlStruct, ctx, errors, ifacePtr...)
|
||||
}
|
||||
}
|
||||
|
||||
// URL is the middleware to parse URL parameters into struct fields.
|
||||
func URL(obj interface{}, ifacePtr ...interface{}) macaron.Handler {
|
||||
return func(ctx *macaron.Context) {
|
||||
var errors Errors
|
||||
|
||||
ensureNotPointer(obj)
|
||||
obj := reflect.New(reflect.TypeOf(obj))
|
||||
|
||||
val := obj.Elem()
|
||||
for k, v := range ctx.AllParams() {
|
||||
field := val.FieldByName(k[1:])
|
||||
if field.IsValid() {
|
||||
errors = setWithProperType(field.Kind(), v, field, k, errors)
|
||||
}
|
||||
}
|
||||
validateAndMap(obj, ctx, errors, ifacePtr...)
|
||||
}
|
||||
}
|
||||
|
||||
// RawValidate is same as Validate but does not require a HTTP context,
|
||||
// and can be used independently just for validation.
|
||||
// This function does not support Validator interface.
|
||||
func RawValidate(obj interface{}) Errors {
|
||||
var errs Errors
|
||||
v := reflect.ValueOf(obj)
|
||||
k := v.Kind()
|
||||
if k == reflect.Interface || k == reflect.Ptr {
|
||||
v = v.Elem()
|
||||
k = v.Kind()
|
||||
}
|
||||
if k == reflect.Slice || k == reflect.Array {
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
e := v.Index(i).Interface()
|
||||
errs = validateStruct(errs, e)
|
||||
}
|
||||
} else {
|
||||
errs = validateStruct(errs, obj)
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
// Validate is middleware to enforce required fields. If the struct
|
||||
// passed in implements Validator, then the user-defined Validate method
|
||||
// is executed, and its errors are mapped to the context. This middleware
|
||||
// performs no error handling: it merely detects errors and maps them.
|
||||
func Validate(obj interface{}) macaron.Handler {
|
||||
return func(ctx *macaron.Context) {
|
||||
var errs Errors
|
||||
v := reflect.ValueOf(obj)
|
||||
k := v.Kind()
|
||||
if k == reflect.Interface || k == reflect.Ptr {
|
||||
v = v.Elem()
|
||||
k = v.Kind()
|
||||
}
|
||||
if k == reflect.Slice || k == reflect.Array {
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
e := v.Index(i).Interface()
|
||||
errs = validateStruct(errs, e)
|
||||
if validator, ok := e.(Validator); ok {
|
||||
errs = validator.Validate(ctx, errs)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
errs = validateStruct(errs, obj)
|
||||
if validator, ok := obj.(Validator); ok {
|
||||
errs = validator.Validate(ctx, errs)
|
||||
}
|
||||
}
|
||||
ctx.Map(errs)
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
AlphaDashPattern = regexp.MustCompile(`[^\d\w-_]`)
|
||||
AlphaDashDotPattern = regexp.MustCompile(`[^\d\w-_\.]`)
|
||||
EmailPattern = regexp.MustCompile("[\\w!#$%&'*+/=?^_`{|}~-]+(?:\\.[\\w!#$%&'*+/=?^_`{|}~-]+)*@(?:[\\w](?:[\\w-]*[\\w])?\\.)+[a-zA-Z0-9](?:[\\w-]*[\\w])?")
|
||||
)
|
||||
|
||||
// Copied from github.com/asaskevich/govalidator.
|
||||
const _MAX_URL_RUNE_COUNT = 2083
|
||||
const _MIN_URL_RUNE_COUNT = 3
|
||||
|
||||
var (
|
||||
urlSchemaRx = `((ftp|tcp|udp|wss?|https?):\/\/)`
|
||||
urlUsernameRx = `(\S+(:\S*)?@)`
|
||||
urlIPRx = `([1-9]\d?|1\d\d|2[01]\d|22[0-3])(\.(1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.([0-9]\d?|1\d\d|2[0-4]\d|25[0-4]))`
|
||||
ipRx = `(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))`
|
||||
urlSubdomainRx = `((www\.)|([a-zA-Z0-9]([-\.][-\._a-zA-Z0-9]+)*))`
|
||||
urlPortRx = `(:(\d{1,5}))`
|
||||
urlPathRx = `((\/|\?|#)[^\s]*)`
|
||||
URLPattern = regexp.MustCompile(`^` + urlSchemaRx + urlUsernameRx + `?` + `((` + urlIPRx + `|(\[` + ipRx + `\])|(([a-zA-Z0-9]([a-zA-Z0-9-_]+)?[a-zA-Z0-9]([-\.][a-zA-Z0-9]+)*)|(` + urlSubdomainRx + `?))?(([a-zA-Z\x{00a1}-\x{ffff}0-9]+-?-?)*[a-zA-Z\x{00a1}-\x{ffff}0-9]+)(?:\.([a-zA-Z\x{00a1}-\x{ffff}]{1,}))?))\.?` + urlPortRx + `?` + urlPathRx + `?$`)
|
||||
)
|
||||
|
||||
// IsURL check if the string is an URL.
|
||||
func isURL(str string) bool {
|
||||
if str == "" || utf8.RuneCountInString(str) >= _MAX_URL_RUNE_COUNT || len(str) <= _MIN_URL_RUNE_COUNT || strings.HasPrefix(str, ".") {
|
||||
return false
|
||||
}
|
||||
u, err := url.Parse(str)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if strings.HasPrefix(u.Host, ".") {
|
||||
return false
|
||||
}
|
||||
if u.Host == "" && (u.Path != "" && !strings.Contains(u.Path, ".")) {
|
||||
return false
|
||||
}
|
||||
return URLPattern.MatchString(str)
|
||||
|
||||
}
|
||||
|
||||
type (
|
||||
// Rule represents a validation rule.
|
||||
Rule struct {
|
||||
// IsMatch checks if rule matches.
|
||||
IsMatch func(string) bool
|
||||
// IsValid applies validation rule to condition.
|
||||
IsValid func(Errors, string, interface{}) (bool, Errors)
|
||||
}
|
||||
|
||||
// ParamRule does same thing as Rule but passes rule itself to IsValid method.
|
||||
ParamRule struct {
|
||||
// IsMatch checks if rule matches.
|
||||
IsMatch func(string) bool
|
||||
// IsValid applies validation rule to condition.
|
||||
IsValid func(Errors, string, string, interface{}) (bool, Errors)
|
||||
}
|
||||
|
||||
// RuleMapper and ParamRuleMapper represent validation rule mappers,
|
||||
// it allwos users to add custom validation rules.
|
||||
RuleMapper []*Rule
|
||||
ParamRuleMapper []*ParamRule
|
||||
)
|
||||
|
||||
var ruleMapper RuleMapper
|
||||
var paramRuleMapper ParamRuleMapper
|
||||
|
||||
// AddRule adds new validation rule.
|
||||
func AddRule(r *Rule) {
|
||||
ruleMapper = append(ruleMapper, r)
|
||||
}
|
||||
|
||||
// AddParamRule adds new validation rule.
|
||||
func AddParamRule(r *ParamRule) {
|
||||
paramRuleMapper = append(paramRuleMapper, r)
|
||||
}
|
||||
|
||||
func in(fieldValue interface{}, arr string) bool {
|
||||
val := fmt.Sprintf("%v", fieldValue)
|
||||
vals := strings.Split(arr, ",")
|
||||
isIn := false
|
||||
for _, v := range vals {
|
||||
if v == val {
|
||||
isIn = true
|
||||
break
|
||||
}
|
||||
}
|
||||
return isIn
|
||||
}
|
||||
|
||||
func parseFormName(raw, actual string) string {
|
||||
if len(actual) > 0 {
|
||||
return actual
|
||||
}
|
||||
return nameMapper(raw)
|
||||
}
|
||||
|
||||
// Performs required field checking on a struct
|
||||
func validateStruct(errors Errors, obj interface{}) Errors {
|
||||
typ := reflect.TypeOf(obj)
|
||||
val := reflect.ValueOf(obj)
|
||||
|
||||
if typ.Kind() == reflect.Ptr {
|
||||
typ = typ.Elem()
|
||||
val = val.Elem()
|
||||
}
|
||||
|
||||
for i := 0; i < typ.NumField(); i++ {
|
||||
field := typ.Field(i)
|
||||
|
||||
// Allow ignored fields in the struct
|
||||
if field.Tag.Get("form") == "-" || !val.Field(i).CanInterface() {
|
||||
continue
|
||||
}
|
||||
|
||||
fieldVal := val.Field(i)
|
||||
fieldValue := fieldVal.Interface()
|
||||
zero := reflect.Zero(field.Type).Interface()
|
||||
|
||||
// Validate nested and embedded structs (if pointer, only do so if not nil)
|
||||
if field.Type.Kind() == reflect.Struct ||
|
||||
(field.Type.Kind() == reflect.Ptr && !reflect.DeepEqual(zero, fieldValue) &&
|
||||
field.Type.Elem().Kind() == reflect.Struct) {
|
||||
errors = validateStruct(errors, fieldValue)
|
||||
}
|
||||
errors = validateField(errors, zero, field, fieldVal, fieldValue)
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
||||
func validateField(errors Errors, zero interface{}, field reflect.StructField, fieldVal reflect.Value, fieldValue interface{}) Errors {
|
||||
if fieldVal.Kind() == reflect.Slice {
|
||||
for i := 0; i < fieldVal.Len(); i++ {
|
||||
sliceVal := fieldVal.Index(i)
|
||||
if sliceVal.Kind() == reflect.Ptr {
|
||||
sliceVal = sliceVal.Elem()
|
||||
}
|
||||
|
||||
sliceValue := sliceVal.Interface()
|
||||
zero := reflect.Zero(sliceVal.Type()).Interface()
|
||||
if sliceVal.Kind() == reflect.Struct ||
|
||||
(sliceVal.Kind() == reflect.Ptr && !reflect.DeepEqual(zero, sliceValue) &&
|
||||
sliceVal.Elem().Kind() == reflect.Struct) {
|
||||
errors = validateStruct(errors, sliceValue)
|
||||
}
|
||||
/* Apply validation rules to each item in a slice. ISSUE #3
|
||||
else {
|
||||
errors = validateField(errors, zero, field, sliceVal, sliceValue)
|
||||
}*/
|
||||
}
|
||||
}
|
||||
|
||||
VALIDATE_RULES:
|
||||
for _, rule := range strings.Split(field.Tag.Get("binding"), ";") {
|
||||
if len(rule) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
switch {
|
||||
case rule == "OmitEmpty":
|
||||
if reflect.DeepEqual(zero, fieldValue) {
|
||||
break VALIDATE_RULES
|
||||
}
|
||||
case rule == "Required":
|
||||
v := reflect.ValueOf(fieldValue)
|
||||
if v.Kind() == reflect.Slice {
|
||||
if v.Len() == 0 {
|
||||
errors.Add([]string{field.Name}, ERR_REQUIRED, "Required")
|
||||
break VALIDATE_RULES
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if reflect.DeepEqual(zero, fieldValue) {
|
||||
errors.Add([]string{field.Name}, ERR_REQUIRED, "Required")
|
||||
break VALIDATE_RULES
|
||||
}
|
||||
case rule == "AlphaDash":
|
||||
if AlphaDashPattern.MatchString(fmt.Sprintf("%v", fieldValue)) {
|
||||
errors.Add([]string{field.Name}, ERR_ALPHA_DASH, "AlphaDash")
|
||||
break VALIDATE_RULES
|
||||
}
|
||||
case rule == "AlphaDashDot":
|
||||
if AlphaDashDotPattern.MatchString(fmt.Sprintf("%v", fieldValue)) {
|
||||
errors.Add([]string{field.Name}, ERR_ALPHA_DASH_DOT, "AlphaDashDot")
|
||||
break VALIDATE_RULES
|
||||
}
|
||||
case strings.HasPrefix(rule, "Size("):
|
||||
size, _ := strconv.Atoi(rule[5 : len(rule)-1])
|
||||
if str, ok := fieldValue.(string); ok && utf8.RuneCountInString(str) != size {
|
||||
errors.Add([]string{field.Name}, ERR_SIZE, "Size")
|
||||
break VALIDATE_RULES
|
||||
}
|
||||
v := reflect.ValueOf(fieldValue)
|
||||
if v.Kind() == reflect.Slice && v.Len() != size {
|
||||
errors.Add([]string{field.Name}, ERR_SIZE, "Size")
|
||||
break VALIDATE_RULES
|
||||
}
|
||||
case strings.HasPrefix(rule, "MinSize("):
|
||||
min, _ := strconv.Atoi(rule[8 : len(rule)-1])
|
||||
if str, ok := fieldValue.(string); ok && utf8.RuneCountInString(str) < min {
|
||||
errors.Add([]string{field.Name}, ERR_MIN_SIZE, "MinSize")
|
||||
break VALIDATE_RULES
|
||||
}
|
||||
v := reflect.ValueOf(fieldValue)
|
||||
if v.Kind() == reflect.Slice && v.Len() < min {
|
||||
errors.Add([]string{field.Name}, ERR_MIN_SIZE, "MinSize")
|
||||
break VALIDATE_RULES
|
||||
}
|
||||
case strings.HasPrefix(rule, "MaxSize("):
|
||||
max, _ := strconv.Atoi(rule[8 : len(rule)-1])
|
||||
if str, ok := fieldValue.(string); ok && utf8.RuneCountInString(str) > max {
|
||||
errors.Add([]string{field.Name}, ERR_MAX_SIZE, "MaxSize")
|
||||
break VALIDATE_RULES
|
||||
}
|
||||
v := reflect.ValueOf(fieldValue)
|
||||
if v.Kind() == reflect.Slice && v.Len() > max {
|
||||
errors.Add([]string{field.Name}, ERR_MAX_SIZE, "MaxSize")
|
||||
break VALIDATE_RULES
|
||||
}
|
||||
case strings.HasPrefix(rule, "Range("):
|
||||
nums := strings.Split(rule[6:len(rule)-1], ",")
|
||||
if len(nums) != 2 {
|
||||
break VALIDATE_RULES
|
||||
}
|
||||
val := com.StrTo(fmt.Sprintf("%v", fieldValue)).MustInt()
|
||||
if val < com.StrTo(nums[0]).MustInt() || val > com.StrTo(nums[1]).MustInt() {
|
||||
errors.Add([]string{field.Name}, ERR_RANGE, "Range")
|
||||
break VALIDATE_RULES
|
||||
}
|
||||
case rule == "Email":
|
||||
if !EmailPattern.MatchString(fmt.Sprintf("%v", fieldValue)) {
|
||||
errors.Add([]string{field.Name}, ERR_EMAIL, "Email")
|
||||
break VALIDATE_RULES
|
||||
}
|
||||
case rule == "Url":
|
||||
str := fmt.Sprintf("%v", fieldValue)
|
||||
if len(str) == 0 {
|
||||
continue
|
||||
} else if !isURL(str) {
|
||||
errors.Add([]string{field.Name}, ERR_URL, "Url")
|
||||
break VALIDATE_RULES
|
||||
}
|
||||
case strings.HasPrefix(rule, "In("):
|
||||
if !in(fieldValue, rule[3:len(rule)-1]) {
|
||||
errors.Add([]string{field.Name}, ERR_IN, "In")
|
||||
break VALIDATE_RULES
|
||||
}
|
||||
case strings.HasPrefix(rule, "NotIn("):
|
||||
if in(fieldValue, rule[6:len(rule)-1]) {
|
||||
errors.Add([]string{field.Name}, ERR_NOT_INT, "NotIn")
|
||||
break VALIDATE_RULES
|
||||
}
|
||||
case strings.HasPrefix(rule, "Include("):
|
||||
if !strings.Contains(fmt.Sprintf("%v", fieldValue), rule[8:len(rule)-1]) {
|
||||
errors.Add([]string{field.Name}, ERR_INCLUDE, "Include")
|
||||
break VALIDATE_RULES
|
||||
}
|
||||
case strings.HasPrefix(rule, "Exclude("):
|
||||
if strings.Contains(fmt.Sprintf("%v", fieldValue), rule[8:len(rule)-1]) {
|
||||
errors.Add([]string{field.Name}, ERR_EXCLUDE, "Exclude")
|
||||
break VALIDATE_RULES
|
||||
}
|
||||
case strings.HasPrefix(rule, "Default("):
|
||||
if reflect.DeepEqual(zero, fieldValue) {
|
||||
if fieldVal.CanAddr() {
|
||||
errors = setWithProperType(field.Type.Kind(), rule[8:len(rule)-1], fieldVal, field.Tag.Get("form"), errors)
|
||||
} else {
|
||||
errors.Add([]string{field.Name}, ERR_EXCLUDE, "Default")
|
||||
break VALIDATE_RULES
|
||||
}
|
||||
}
|
||||
default:
|
||||
// Apply custom validation rules
|
||||
var isValid bool
|
||||
for i := range ruleMapper {
|
||||
if ruleMapper[i].IsMatch(rule) {
|
||||
isValid, errors = ruleMapper[i].IsValid(errors, field.Name, fieldValue)
|
||||
if !isValid {
|
||||
break VALIDATE_RULES
|
||||
}
|
||||
}
|
||||
}
|
||||
for i := range paramRuleMapper {
|
||||
if paramRuleMapper[i].IsMatch(rule) {
|
||||
isValid, errors = paramRuleMapper[i].IsValid(errors, rule, field.Name, fieldValue)
|
||||
if !isValid {
|
||||
break VALIDATE_RULES
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
||||
// NameMapper represents a form tag name mapper.
|
||||
type NameMapper func(string) string
|
||||
|
||||
var (
|
||||
nameMapper = func(field string) string {
|
||||
newstr := make([]rune, 0, len(field))
|
||||
for i, chr := range field {
|
||||
if isUpper := 'A' <= chr && chr <= 'Z'; isUpper {
|
||||
if i > 0 {
|
||||
newstr = append(newstr, '_')
|
||||
}
|
||||
chr -= ('A' - 'a')
|
||||
}
|
||||
newstr = append(newstr, chr)
|
||||
}
|
||||
return string(newstr)
|
||||
}
|
||||
)
|
||||
|
||||
// SetNameMapper sets name mapper.
|
||||
func SetNameMapper(nm NameMapper) {
|
||||
nameMapper = nm
|
||||
}
|
||||
|
||||
// Takes values from the form data and puts them into a struct
|
||||
func mapForm(formStruct reflect.Value, form map[string][]string,
|
||||
formfile map[string][]*multipart.FileHeader, errors Errors) Errors {
|
||||
|
||||
if formStruct.Kind() == reflect.Ptr {
|
||||
formStruct = formStruct.Elem()
|
||||
}
|
||||
typ := formStruct.Type()
|
||||
|
||||
for i := 0; i < typ.NumField(); i++ {
|
||||
typeField := typ.Field(i)
|
||||
structField := formStruct.Field(i)
|
||||
|
||||
if typeField.Type.Kind() == reflect.Ptr && typeField.Anonymous {
|
||||
structField.Set(reflect.New(typeField.Type.Elem()))
|
||||
errors = mapForm(structField.Elem(), form, formfile, errors)
|
||||
if reflect.DeepEqual(structField.Elem().Interface(), reflect.Zero(structField.Elem().Type()).Interface()) {
|
||||
structField.Set(reflect.Zero(structField.Type()))
|
||||
}
|
||||
} else if typeField.Type.Kind() == reflect.Struct {
|
||||
errors = mapForm(structField, form, formfile, errors)
|
||||
}
|
||||
|
||||
inputFieldName := parseFormName(typeField.Name, typeField.Tag.Get("form"))
|
||||
if len(inputFieldName) == 0 || !structField.CanSet() {
|
||||
continue
|
||||
}
|
||||
|
||||
inputValue, exists := form[inputFieldName]
|
||||
if exists {
|
||||
numElems := len(inputValue)
|
||||
if structField.Kind() == reflect.Slice && numElems > 0 {
|
||||
sliceOf := structField.Type().Elem().Kind()
|
||||
slice := reflect.MakeSlice(structField.Type(), numElems, numElems)
|
||||
for i := 0; i < numElems; i++ {
|
||||
errors = setWithProperType(sliceOf, inputValue[i], slice.Index(i), inputFieldName, errors)
|
||||
}
|
||||
formStruct.Field(i).Set(slice)
|
||||
} else {
|
||||
errors = setWithProperType(typeField.Type.Kind(), inputValue[0], structField, inputFieldName, errors)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
inputFile, exists := formfile[inputFieldName]
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
fhType := reflect.TypeOf((*multipart.FileHeader)(nil))
|
||||
numElems := len(inputFile)
|
||||
if structField.Kind() == reflect.Slice && numElems > 0 && structField.Type().Elem() == fhType {
|
||||
slice := reflect.MakeSlice(structField.Type(), numElems, numElems)
|
||||
for i := 0; i < numElems; i++ {
|
||||
slice.Index(i).Set(reflect.ValueOf(inputFile[i]))
|
||||
}
|
||||
structField.Set(slice)
|
||||
} else if structField.Type() == fhType {
|
||||
structField.Set(reflect.ValueOf(inputFile[0]))
|
||||
}
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
||||
// This sets the value in a struct of an indeterminate type to the
|
||||
// matching value from the request (via Form middleware) in the
|
||||
// same type, so that not all deserialized values have to be strings.
|
||||
// Supported types are string, int, float, and bool.
|
||||
func setWithProperType(valueKind reflect.Kind, val string, structField reflect.Value, nameInTag string, errors Errors) Errors {
|
||||
switch valueKind {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
if val == "" {
|
||||
val = "0"
|
||||
}
|
||||
intVal, err := strconv.ParseInt(val, 10, 64)
|
||||
if err != nil {
|
||||
errors.Add([]string{nameInTag}, ERR_INTERGER_TYPE, "Value could not be parsed as integer")
|
||||
} else {
|
||||
structField.SetInt(intVal)
|
||||
}
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
if val == "" {
|
||||
val = "0"
|
||||
}
|
||||
uintVal, err := strconv.ParseUint(val, 10, 64)
|
||||
if err != nil {
|
||||
errors.Add([]string{nameInTag}, ERR_INTERGER_TYPE, "Value could not be parsed as unsigned integer")
|
||||
} else {
|
||||
structField.SetUint(uintVal)
|
||||
}
|
||||
case reflect.Bool:
|
||||
if val == "on" {
|
||||
structField.SetBool(true)
|
||||
break
|
||||
}
|
||||
|
||||
if val == "" {
|
||||
val = "false"
|
||||
}
|
||||
boolVal, err := strconv.ParseBool(val)
|
||||
if err != nil {
|
||||
errors.Add([]string{nameInTag}, ERR_BOOLEAN_TYPE, "Value could not be parsed as boolean")
|
||||
} else if boolVal {
|
||||
structField.SetBool(true)
|
||||
}
|
||||
case reflect.Float32:
|
||||
if val == "" {
|
||||
val = "0.0"
|
||||
}
|
||||
floatVal, err := strconv.ParseFloat(val, 32)
|
||||
if err != nil {
|
||||
errors.Add([]string{nameInTag}, ERR_FLOAT_TYPE, "Value could not be parsed as 32-bit float")
|
||||
} else {
|
||||
structField.SetFloat(floatVal)
|
||||
}
|
||||
case reflect.Float64:
|
||||
if val == "" {
|
||||
val = "0.0"
|
||||
}
|
||||
floatVal, err := strconv.ParseFloat(val, 64)
|
||||
if err != nil {
|
||||
errors.Add([]string{nameInTag}, ERR_FLOAT_TYPE, "Value could not be parsed as 64-bit float")
|
||||
} else {
|
||||
structField.SetFloat(floatVal)
|
||||
}
|
||||
case reflect.String:
|
||||
structField.SetString(val)
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
||||
// Don't pass in pointers to bind to. Can lead to bugs.
|
||||
func ensureNotPointer(obj interface{}) {
|
||||
if reflect.TypeOf(obj).Kind() == reflect.Ptr {
|
||||
panic("Pointers are not accepted as binding models")
|
||||
}
|
||||
}
|
||||
|
||||
// Performs validation and combines errors from validation
|
||||
// with errors from deserialization, then maps both the
|
||||
// resulting struct and the errors to the context.
|
||||
func validateAndMap(obj reflect.Value, ctx *macaron.Context, errors Errors, ifacePtr ...interface{}) {
|
||||
_, _ = ctx.Invoke(Validate(obj.Interface()))
|
||||
errors = append(errors, getErrors(ctx)...)
|
||||
ctx.Map(errors)
|
||||
ctx.Map(obj.Elem().Interface())
|
||||
if len(ifacePtr) > 0 {
|
||||
ctx.MapTo(obj.Elem().Interface(), ifacePtr[0])
|
||||
}
|
||||
}
|
||||
|
||||
// getErrors simply gets the errors from the context (it's kind of a chore)
|
||||
func getErrors(ctx *macaron.Context) Errors {
|
||||
return ctx.GetVal(reflect.TypeOf(Errors{})).Interface().(Errors)
|
||||
}
|
||||
|
||||
type (
|
||||
// ErrorHandler is the interface that has custom error handling process.
|
||||
ErrorHandler interface {
|
||||
// Error handles validation errors with custom process.
|
||||
Error(*macaron.Context, Errors)
|
||||
}
|
||||
|
||||
// Validator is the interface that handles some rudimentary
|
||||
// request validation logic so your application doesn't have to.
|
||||
Validator interface {
|
||||
// Validate validates that the request is OK. It is recommended
|
||||
// that validation be limited to checking values for syntax and
|
||||
// semantics, enough to know that you can make sense of the request
|
||||
// in your application. For example, you might verify that a credit
|
||||
// card number matches a valid pattern, but you probably wouldn't
|
||||
// perform an actual credit card authorization here.
|
||||
Validate(*macaron.Context, Errors) Errors
|
||||
}
|
||||
)
|
||||
159
pkg/macaron/binding/errors.go
Normal file
159
pkg/macaron/binding/errors.go
Normal file
@@ -0,0 +1,159 @@
|
||||
// Copyright 2014 Martini Authors
|
||||
// Copyright 2014 The Macaron Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package binding
|
||||
|
||||
const (
|
||||
// Type mismatch errors.
|
||||
ERR_CONTENT_TYPE = "ContentTypeError"
|
||||
ERR_DESERIALIZATION = "DeserializationError"
|
||||
ERR_INTERGER_TYPE = "IntegerTypeError"
|
||||
ERR_BOOLEAN_TYPE = "BooleanTypeError"
|
||||
ERR_FLOAT_TYPE = "FloatTypeError"
|
||||
|
||||
// Validation errors.
|
||||
ERR_REQUIRED = "RequiredError"
|
||||
ERR_ALPHA_DASH = "AlphaDashError"
|
||||
ERR_ALPHA_DASH_DOT = "AlphaDashDotError"
|
||||
ERR_SIZE = "SizeError"
|
||||
ERR_MIN_SIZE = "MinSizeError"
|
||||
ERR_MAX_SIZE = "MaxSizeError"
|
||||
ERR_RANGE = "RangeError"
|
||||
ERR_EMAIL = "EmailError"
|
||||
ERR_URL = "UrlError"
|
||||
ERR_IN = "InError"
|
||||
ERR_NOT_INT = "NotInError"
|
||||
ERR_INCLUDE = "IncludeError"
|
||||
ERR_EXCLUDE = "ExcludeError"
|
||||
ERR_DEFAULT = "DefaultError"
|
||||
)
|
||||
|
||||
type (
|
||||
// Errors may be generated during deserialization, binding,
|
||||
// or validation. This type is mapped to the context so you
|
||||
// can inject it into your own handlers and use it in your
|
||||
// application if you want all your errors to look the same.
|
||||
Errors []Error
|
||||
|
||||
Error struct {
|
||||
// An error supports zero or more field names, because an
|
||||
// error can morph three ways: (1) it can indicate something
|
||||
// wrong with the request as a whole, (2) it can point to a
|
||||
// specific problem with a particular input field, or (3) it
|
||||
// can span multiple related input fields.
|
||||
FieldNames []string `json:"fieldNames,omitempty"`
|
||||
|
||||
// The classification is like an error code, convenient to
|
||||
// use when processing or categorizing an error programmatically.
|
||||
// It may also be called the "kind" of error.
|
||||
Classification string `json:"classification,omitempty"`
|
||||
|
||||
// Message should be human-readable and detailed enough to
|
||||
// pinpoint and resolve the problem, but it should be brief. For
|
||||
// example, a payload of 100 objects in a JSON array might have
|
||||
// an error in the 41st object. The message should help the
|
||||
// end user find and fix the error with their request.
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
)
|
||||
|
||||
// Add adds an error associated with the fields indicated
|
||||
// by fieldNames, with the given classification and message.
|
||||
func (e *Errors) Add(fieldNames []string, classification, message string) {
|
||||
*e = append(*e, Error{
|
||||
FieldNames: fieldNames,
|
||||
Classification: classification,
|
||||
Message: message,
|
||||
})
|
||||
}
|
||||
|
||||
// Len returns the number of errors.
|
||||
func (e *Errors) Len() int {
|
||||
return len(*e)
|
||||
}
|
||||
|
||||
// Has determines whether an Errors slice has an Error with
|
||||
// a given classification in it; it does not search on messages
|
||||
// or field names.
|
||||
func (e *Errors) Has(class string) bool {
|
||||
for _, err := range *e {
|
||||
if err.Kind() == class {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/*
|
||||
// WithClass gets a copy of errors that are classified by the
|
||||
// the given classification.
|
||||
func (e *Errors) WithClass(classification string) Errors {
|
||||
var errs Errors
|
||||
for _, err := range *e {
|
||||
if err.Kind() == classification {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
// ForField gets a copy of errors that are associated with the
|
||||
// field by the given name.
|
||||
func (e *Errors) ForField(name string) Errors {
|
||||
var errs Errors
|
||||
for _, err := range *e {
|
||||
for _, fieldName := range err.Fields() {
|
||||
if fieldName == name {
|
||||
errs = append(errs, err)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
// Get gets errors of a particular class for the specified
|
||||
// field name.
|
||||
func (e *Errors) Get(class, fieldName string) Errors {
|
||||
var errs Errors
|
||||
for _, err := range *e {
|
||||
if err.Kind() == class {
|
||||
for _, nameOfField := range err.Fields() {
|
||||
if nameOfField == fieldName {
|
||||
errs = append(errs, err)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return errs
|
||||
}
|
||||
*/
|
||||
|
||||
// Fields returns the list of field names this error is
|
||||
// associated with.
|
||||
func (e Error) Fields() []string {
|
||||
return e.FieldNames
|
||||
}
|
||||
|
||||
// Kind returns this error's classification.
|
||||
func (e Error) Kind() string {
|
||||
return e.Classification
|
||||
}
|
||||
|
||||
// Error returns this error's message.
|
||||
func (e Error) Error() string {
|
||||
return e.Message
|
||||
}
|
||||
15
pkg/macaron/binding/go.mod
Normal file
15
pkg/macaron/binding/go.mod
Normal file
@@ -0,0 +1,15 @@
|
||||
module github.com/go-macaron/binding
|
||||
|
||||
go 1.17
|
||||
|
||||
require (
|
||||
github.com/unknwon/com v1.0.1
|
||||
gopkg.in/macaron.v1 v1.4.0
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/go-macaron/inject v0.0.0-20160627170012-d8a0b8677191 // indirect
|
||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 // indirect
|
||||
gopkg.in/ini.v1 v1.46.0 // indirect
|
||||
)
|
||||
36
pkg/macaron/binding/go.sum
Normal file
36
pkg/macaron/binding/go.sum
Normal file
@@ -0,0 +1,36 @@
|
||||
github.com/go-macaron/inject v0.0.0-20160627170012-d8a0b8677191 h1:NjHlg70DuOkcAMqgt0+XA+NHwtu66MkTVVgR4fFWbcI=
|
||||
github.com/go-macaron/inject v0.0.0-20160627170012-d8a0b8677191/go.mod h1:VFI2o2q9kYsC4o7VP1HrEVosiZZTd+MVT3YZx4gqvJw=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c h1:7lF+Vz0LqiRidnzC1Oq86fpX1q/iEv2KJdrCtttYjT4=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/assertions v1.0.1 h1:voD4ITNjPL5jjBfgR/r8fPIIBrliWrWHeiJApdr3r4w=
|
||||
github.com/smartystreets/assertions v1.0.1/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM=
|
||||
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
|
||||
github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337 h1:WN9BUFbdyOsSH/XohnWpXOlq9NBD5sGAB2FciQMUEe8=
|
||||
github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/unknwon/com v0.0.0-20190804042917-757f69c95f3e/go.mod h1:tOOxU81rwgoCLoOVVPHb6T/wt8HZygqH5id+GNnlCXM=
|
||||
github.com/unknwon/com v1.0.1 h1:3d1LTxD+Lnf3soQiD4Cp/0BRB+Rsa/+RTvz8GMMzIXs=
|
||||
github.com/unknwon/com v1.0.1/go.mod h1:tOOxU81rwgoCLoOVVPHb6T/wt8HZygqH5id+GNnlCXM=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc=
|
||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/ini.v1 v1.46.0 h1:VeDZbLYGaupuvIrsYCEOe/L/2Pcs5n7hdO1ZTjporag=
|
||||
gopkg.in/ini.v1 v1.46.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/macaron.v1 v1.4.0 h1:RJHC09fAnQ8tuGUiZNjG0uyL1BWSdSWd9SpufIcEArQ=
|
||||
gopkg.in/macaron.v1 v1.4.0/go.mod h1:uMZCFccv9yr5TipIalVOyAyZQuOH3OkmXvgcWwhJuP4=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
562
pkg/macaron/context.go
Executable file
562
pkg/macaron/context.go
Executable file
@@ -0,0 +1,562 @@
|
||||
// Copyright 2014 The Macaron Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package macaron
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"html/template"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-macaron/inject"
|
||||
"github.com/unknwon/com"
|
||||
"golang.org/x/crypto/pbkdf2"
|
||||
)
|
||||
|
||||
// Locale reprents a localization interface.
|
||||
type Locale interface {
|
||||
Language() string
|
||||
Tr(string, ...interface{}) string
|
||||
}
|
||||
|
||||
// RequestBody represents a request body.
|
||||
type RequestBody struct {
|
||||
reader io.ReadCloser
|
||||
}
|
||||
|
||||
// Bytes reads and returns content of request body in bytes.
|
||||
func (rb *RequestBody) Bytes() ([]byte, error) {
|
||||
return ioutil.ReadAll(rb.reader)
|
||||
}
|
||||
|
||||
// String reads and returns content of request body in string.
|
||||
func (rb *RequestBody) String() (string, error) {
|
||||
data, err := rb.Bytes()
|
||||
return string(data), err
|
||||
}
|
||||
|
||||
// ReadCloser returns a ReadCloser for request body.
|
||||
func (rb *RequestBody) ReadCloser() io.ReadCloser {
|
||||
return rb.reader
|
||||
}
|
||||
|
||||
// Request represents an HTTP request received by a server or to be sent by a client.
|
||||
type Request struct {
|
||||
*http.Request
|
||||
}
|
||||
|
||||
// Body returns a RequestBody for the request
|
||||
func (r *Request) Body() *RequestBody {
|
||||
return &RequestBody{r.Request.Body}
|
||||
}
|
||||
|
||||
// ContextInvoker is an inject.FastInvoker wrapper of func(ctx *Context).
|
||||
type ContextInvoker func(ctx *Context)
|
||||
|
||||
// Invoke implements inject.FastInvoker which simplifies calls of `func(ctx *Context)` function.
|
||||
func (invoke ContextInvoker) Invoke(params []interface{}) ([]reflect.Value, error) {
|
||||
invoke(params[0].(*Context))
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Context represents the runtime context of current request of Macaron instance.
|
||||
// It is the integration of most frequently used middlewares and helper methods.
|
||||
type Context struct {
|
||||
inject.Injector
|
||||
handlers []Handler
|
||||
action Handler
|
||||
index int
|
||||
|
||||
*Router
|
||||
Req Request
|
||||
Resp ResponseWriter
|
||||
params Params
|
||||
Render
|
||||
Locale
|
||||
Data map[string]interface{}
|
||||
}
|
||||
|
||||
func (ctx *Context) handler() Handler {
|
||||
if ctx.index < len(ctx.handlers) {
|
||||
return ctx.handlers[ctx.index]
|
||||
}
|
||||
if ctx.index == len(ctx.handlers) {
|
||||
return ctx.action
|
||||
}
|
||||
panic("invalid index for context handler")
|
||||
}
|
||||
|
||||
// Next runs the next handler in the context chain
|
||||
func (ctx *Context) Next() {
|
||||
ctx.index++
|
||||
ctx.run()
|
||||
}
|
||||
|
||||
// Written returns whether the context response has been written to
|
||||
func (ctx *Context) Written() bool {
|
||||
return ctx.Resp.Written()
|
||||
}
|
||||
|
||||
func (ctx *Context) run() {
|
||||
for ctx.index <= len(ctx.handlers) {
|
||||
vals, err := ctx.Invoke(ctx.handler())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
ctx.index++
|
||||
|
||||
// if the handler returned something, write it to the http response
|
||||
if len(vals) > 0 {
|
||||
ev := ctx.GetVal(reflect.TypeOf(ReturnHandler(nil)))
|
||||
handleReturn := ev.Interface().(ReturnHandler)
|
||||
handleReturn(ctx, vals)
|
||||
}
|
||||
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RemoteAddr returns more real IP address.
|
||||
func (ctx *Context) RemoteAddr() string {
|
||||
addr := ctx.Req.Header.Get("X-Real-IP")
|
||||
if len(addr) == 0 {
|
||||
addr = ctx.Req.Header.Get("X-Forwarded-For")
|
||||
if addr == "" {
|
||||
addr = ctx.Req.RemoteAddr
|
||||
if i := strings.LastIndex(addr, ":"); i > -1 {
|
||||
addr = addr[:i]
|
||||
}
|
||||
}
|
||||
}
|
||||
return addr
|
||||
}
|
||||
|
||||
func (ctx *Context) renderHTML(status int, setName, tplName string, data ...interface{}) {
|
||||
if len(data) <= 0 {
|
||||
ctx.Render.HTMLSet(status, setName, tplName, ctx.Data)
|
||||
} else if len(data) == 1 {
|
||||
ctx.Render.HTMLSet(status, setName, tplName, data[0])
|
||||
} else {
|
||||
ctx.Render.HTMLSet(status, setName, tplName, data[0], data[1].(HTMLOptions))
|
||||
}
|
||||
}
|
||||
|
||||
// HTML renders the HTML with default template set.
|
||||
func (ctx *Context) HTML(status int, name string, data ...interface{}) {
|
||||
ctx.renderHTML(status, DEFAULT_TPL_SET_NAME, name, data...)
|
||||
}
|
||||
|
||||
// HTMLSet renders the HTML with given template set name.
|
||||
func (ctx *Context) HTMLSet(status int, setName, tplName string, data ...interface{}) {
|
||||
ctx.renderHTML(status, setName, tplName, data...)
|
||||
}
|
||||
|
||||
// Redirect sends a redirect response
|
||||
func (ctx *Context) Redirect(location string, status ...int) {
|
||||
code := http.StatusFound
|
||||
if len(status) == 1 {
|
||||
code = status[0]
|
||||
}
|
||||
|
||||
http.Redirect(ctx.Resp, ctx.Req.Request, location, code)
|
||||
}
|
||||
|
||||
// MaxMemory is the maximum amount of memory to use when parsing a multipart form.
|
||||
// Set this to whatever value you prefer; default is 10 MB.
|
||||
var MaxMemory = int64(1024 * 1024 * 10)
|
||||
|
||||
func (ctx *Context) parseForm() {
|
||||
if ctx.Req.Form != nil {
|
||||
return
|
||||
}
|
||||
|
||||
contentType := ctx.Req.Header.Get(_CONTENT_TYPE)
|
||||
if (ctx.Req.Method == "POST" || ctx.Req.Method == "PUT") &&
|
||||
len(contentType) > 0 && strings.Contains(contentType, "multipart/form-data") {
|
||||
_ = ctx.Req.ParseMultipartForm(MaxMemory)
|
||||
} else {
|
||||
_ = ctx.Req.ParseForm()
|
||||
}
|
||||
}
|
||||
|
||||
// Query querys form parameter.
|
||||
func (ctx *Context) Query(name string) string {
|
||||
ctx.parseForm()
|
||||
return ctx.Req.Form.Get(name)
|
||||
}
|
||||
|
||||
// QueryTrim querys and trims spaces form parameter.
|
||||
func (ctx *Context) QueryTrim(name string) string {
|
||||
return strings.TrimSpace(ctx.Query(name))
|
||||
}
|
||||
|
||||
// QueryStrings returns a list of results by given query name.
|
||||
func (ctx *Context) QueryStrings(name string) []string {
|
||||
ctx.parseForm()
|
||||
|
||||
vals, ok := ctx.Req.Form[name]
|
||||
if !ok {
|
||||
return []string{}
|
||||
}
|
||||
return vals
|
||||
}
|
||||
|
||||
// QueryEscape returns escapred query result.
|
||||
func (ctx *Context) QueryEscape(name string) string {
|
||||
return template.HTMLEscapeString(ctx.Query(name))
|
||||
}
|
||||
|
||||
// QueryBool returns query result in bool type.
|
||||
func (ctx *Context) QueryBool(name string) bool {
|
||||
v, _ := strconv.ParseBool(ctx.Query(name))
|
||||
return v
|
||||
}
|
||||
|
||||
// QueryInt returns query result in int type.
|
||||
func (ctx *Context) QueryInt(name string) int {
|
||||
return com.StrTo(ctx.Query(name)).MustInt()
|
||||
}
|
||||
|
||||
// QueryInt64 returns query result in int64 type.
|
||||
func (ctx *Context) QueryInt64(name string) int64 {
|
||||
return com.StrTo(ctx.Query(name)).MustInt64()
|
||||
}
|
||||
|
||||
// QueryFloat64 returns query result in float64 type.
|
||||
func (ctx *Context) QueryFloat64(name string) float64 {
|
||||
v, _ := strconv.ParseFloat(ctx.Query(name), 64)
|
||||
return v
|
||||
}
|
||||
|
||||
// Params returns value of given param name.
|
||||
// e.g. ctx.Params(":uid") or ctx.Params("uid")
|
||||
func (ctx *Context) Params(name string) string {
|
||||
if len(name) == 0 {
|
||||
return ""
|
||||
}
|
||||
if len(name) > 1 && name[0] != ':' {
|
||||
name = ":" + name
|
||||
}
|
||||
return ctx.params[name]
|
||||
}
|
||||
|
||||
// AllParams returns all params.
|
||||
func (ctx *Context) AllParams() Params {
|
||||
return ctx.params
|
||||
}
|
||||
|
||||
// SetParams sets value of param with given name.
|
||||
func (ctx *Context) SetParams(name, val string) {
|
||||
if name != "*" && !strings.HasPrefix(name, ":") {
|
||||
name = ":" + name
|
||||
}
|
||||
ctx.params[name] = val
|
||||
}
|
||||
|
||||
// ReplaceAllParams replace all current params with given params
|
||||
func (ctx *Context) ReplaceAllParams(params Params) {
|
||||
ctx.params = params
|
||||
}
|
||||
|
||||
// ParamsEscape returns escapred params result.
|
||||
// e.g. ctx.ParamsEscape(":uname")
|
||||
func (ctx *Context) ParamsEscape(name string) string {
|
||||
return template.HTMLEscapeString(ctx.Params(name))
|
||||
}
|
||||
|
||||
// ParamsInt returns params result in int type.
|
||||
// e.g. ctx.ParamsInt(":uid")
|
||||
func (ctx *Context) ParamsInt(name string) int {
|
||||
return com.StrTo(ctx.Params(name)).MustInt()
|
||||
}
|
||||
|
||||
// ParamsInt64 returns params result in int64 type.
|
||||
// e.g. ctx.ParamsInt64(":uid")
|
||||
func (ctx *Context) ParamsInt64(name string) int64 {
|
||||
return com.StrTo(ctx.Params(name)).MustInt64()
|
||||
}
|
||||
|
||||
// ParamsFloat64 returns params result in int64 type.
|
||||
// e.g. ctx.ParamsFloat64(":uid")
|
||||
func (ctx *Context) ParamsFloat64(name string) float64 {
|
||||
v, _ := strconv.ParseFloat(ctx.Params(name), 64)
|
||||
return v
|
||||
}
|
||||
|
||||
// GetFile returns information about user upload file by given form field name.
|
||||
func (ctx *Context) GetFile(name string) (multipart.File, *multipart.FileHeader, error) {
|
||||
return ctx.Req.FormFile(name)
|
||||
}
|
||||
|
||||
// SaveToFile reads a file from request by field name and saves to given path.
|
||||
func (ctx *Context) SaveToFile(name, savePath string) error {
|
||||
fr, _, err := ctx.GetFile(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fr.Close()
|
||||
|
||||
fw, err := os.OpenFile(savePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fw.Close()
|
||||
|
||||
_, err = io.Copy(fw, fr)
|
||||
return err
|
||||
}
|
||||
|
||||
// SetCookie sets given cookie value to response header.
|
||||
// FIXME: IE support? http://golanghome.com/post/620#reply2
|
||||
func (ctx *Context) SetCookie(name string, value string, others ...interface{}) {
|
||||
cookie := http.Cookie{}
|
||||
cookie.Name = name
|
||||
cookie.Value = url.QueryEscape(value)
|
||||
|
||||
if len(others) > 0 {
|
||||
switch v := others[0].(type) {
|
||||
case int:
|
||||
cookie.MaxAge = v
|
||||
case int64:
|
||||
cookie.MaxAge = int(v)
|
||||
case int32:
|
||||
cookie.MaxAge = int(v)
|
||||
case func(*http.Cookie):
|
||||
v(&cookie)
|
||||
}
|
||||
}
|
||||
|
||||
cookie.Path = "/"
|
||||
if len(others) > 1 {
|
||||
if v, ok := others[1].(string); ok && len(v) > 0 {
|
||||
cookie.Path = v
|
||||
} else if v, ok := others[1].(func(*http.Cookie)); ok {
|
||||
v(&cookie)
|
||||
}
|
||||
}
|
||||
|
||||
if len(others) > 2 {
|
||||
if v, ok := others[2].(string); ok && len(v) > 0 {
|
||||
cookie.Domain = v
|
||||
} else if v, ok := others[1].(func(*http.Cookie)); ok {
|
||||
v(&cookie)
|
||||
}
|
||||
}
|
||||
|
||||
if len(others) > 3 {
|
||||
switch v := others[3].(type) {
|
||||
case bool:
|
||||
cookie.Secure = v
|
||||
case func(*http.Cookie):
|
||||
v(&cookie)
|
||||
default:
|
||||
if others[3] != nil {
|
||||
cookie.Secure = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(others) > 4 {
|
||||
if v, ok := others[4].(bool); ok && v {
|
||||
cookie.HttpOnly = true
|
||||
} else if v, ok := others[1].(func(*http.Cookie)); ok {
|
||||
v(&cookie)
|
||||
}
|
||||
}
|
||||
|
||||
if len(others) > 5 {
|
||||
if v, ok := others[5].(time.Time); ok {
|
||||
cookie.Expires = v
|
||||
cookie.RawExpires = v.Format(time.UnixDate)
|
||||
} else if v, ok := others[1].(func(*http.Cookie)); ok {
|
||||
v(&cookie)
|
||||
}
|
||||
}
|
||||
|
||||
if len(others) > 6 {
|
||||
for _, other := range others[6:] {
|
||||
if v, ok := other.(func(*http.Cookie)); ok {
|
||||
v(&cookie)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Resp.Header().Add("Set-Cookie", cookie.String())
|
||||
}
|
||||
|
||||
// GetCookie returns given cookie value from request header.
|
||||
func (ctx *Context) GetCookie(name string) string {
|
||||
cookie, err := ctx.Req.Cookie(name)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
val, _ := url.QueryUnescape(cookie.Value)
|
||||
return val
|
||||
}
|
||||
|
||||
// GetCookieInt returns cookie result in int type.
|
||||
func (ctx *Context) GetCookieInt(name string) int {
|
||||
return com.StrTo(ctx.GetCookie(name)).MustInt()
|
||||
}
|
||||
|
||||
// GetCookieInt64 returns cookie result in int64 type.
|
||||
func (ctx *Context) GetCookieInt64(name string) int64 {
|
||||
return com.StrTo(ctx.GetCookie(name)).MustInt64()
|
||||
}
|
||||
|
||||
// GetCookieFloat64 returns cookie result in float64 type.
|
||||
func (ctx *Context) GetCookieFloat64(name string) float64 {
|
||||
v, _ := strconv.ParseFloat(ctx.GetCookie(name), 64)
|
||||
return v
|
||||
}
|
||||
|
||||
var defaultCookieSecret string
|
||||
|
||||
// SetDefaultCookieSecret sets global default secure cookie secret.
|
||||
func (m *Macaron) SetDefaultCookieSecret(secret string) {
|
||||
defaultCookieSecret = secret
|
||||
}
|
||||
|
||||
// SetSecureCookie sets given cookie value to response header with default secret string.
|
||||
func (ctx *Context) SetSecureCookie(name, value string, others ...interface{}) {
|
||||
ctx.SetSuperSecureCookie(defaultCookieSecret, name, value, others...)
|
||||
}
|
||||
|
||||
// GetSecureCookie returns given cookie value from request header with default secret string.
|
||||
func (ctx *Context) GetSecureCookie(key string) (string, bool) {
|
||||
return ctx.GetSuperSecureCookie(defaultCookieSecret, key)
|
||||
}
|
||||
|
||||
// SetSuperSecureCookie sets given cookie value to response header with secret string.
|
||||
func (ctx *Context) SetSuperSecureCookie(secret, name, value string, others ...interface{}) {
|
||||
key := pbkdf2.Key([]byte(secret), []byte(secret), 1000, 16, sha256.New)
|
||||
text, err := com.AESGCMEncrypt(key, []byte(value))
|
||||
if err != nil {
|
||||
panic("error encrypting cookie: " + err.Error())
|
||||
}
|
||||
|
||||
ctx.SetCookie(name, hex.EncodeToString(text), others...)
|
||||
}
|
||||
|
||||
// GetSuperSecureCookie returns given cookie value from request header with secret string.
|
||||
func (ctx *Context) GetSuperSecureCookie(secret, name string) (string, bool) {
|
||||
val := ctx.GetCookie(name)
|
||||
if val == "" {
|
||||
return "", false
|
||||
}
|
||||
|
||||
text, err := hex.DecodeString(val)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
|
||||
key := pbkdf2.Key([]byte(secret), []byte(secret), 1000, 16, sha256.New)
|
||||
text, err = com.AESGCMDecrypt(key, text)
|
||||
return string(text), err == nil
|
||||
}
|
||||
|
||||
func (ctx *Context) setRawContentHeader() {
|
||||
ctx.Resp.Header().Set("Content-Description", "Raw content")
|
||||
ctx.Resp.Header().Set("Content-Type", "text/plain")
|
||||
ctx.Resp.Header().Set("Expires", "0")
|
||||
ctx.Resp.Header().Set("Cache-Control", "must-revalidate")
|
||||
ctx.Resp.Header().Set("Pragma", "public")
|
||||
}
|
||||
|
||||
// ServeContent serves given content to response.
|
||||
func (ctx *Context) ServeContent(name string, r io.ReadSeeker, params ...interface{}) {
|
||||
modtime := time.Now()
|
||||
for _, p := range params {
|
||||
switch v := p.(type) {
|
||||
case time.Time:
|
||||
modtime = v
|
||||
}
|
||||
}
|
||||
|
||||
ctx.setRawContentHeader()
|
||||
http.ServeContent(ctx.Resp, ctx.Req.Request, name, modtime, r)
|
||||
}
|
||||
|
||||
// ServeFileContent serves given file as content to response.
|
||||
func (ctx *Context) ServeFileContent(file string, names ...string) {
|
||||
var name string
|
||||
if len(names) > 0 {
|
||||
name = names[0]
|
||||
} else {
|
||||
name = path.Base(file)
|
||||
}
|
||||
|
||||
f, err := os.Open(file)
|
||||
if err != nil {
|
||||
if Env == PROD {
|
||||
http.Error(ctx.Resp, "Internal Server Error", 500)
|
||||
} else {
|
||||
http.Error(ctx.Resp, err.Error(), 500)
|
||||
}
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
ctx.setRawContentHeader()
|
||||
http.ServeContent(ctx.Resp, ctx.Req.Request, name, time.Now(), f)
|
||||
}
|
||||
|
||||
// ServeFile serves given file to response.
|
||||
func (ctx *Context) ServeFile(file string, names ...string) {
|
||||
var name string
|
||||
if len(names) > 0 {
|
||||
name = names[0]
|
||||
} else {
|
||||
name = path.Base(file)
|
||||
}
|
||||
ctx.Resp.Header().Set("Content-Description", "File Transfer")
|
||||
ctx.Resp.Header().Set("Content-Type", "application/octet-stream")
|
||||
ctx.Resp.Header().Set("Content-Disposition", "attachment; filename="+name)
|
||||
ctx.Resp.Header().Set("Content-Transfer-Encoding", "binary")
|
||||
ctx.Resp.Header().Set("Expires", "0")
|
||||
ctx.Resp.Header().Set("Cache-Control", "must-revalidate")
|
||||
ctx.Resp.Header().Set("Pragma", "public")
|
||||
http.ServeFile(ctx.Resp, ctx.Req.Request, file)
|
||||
}
|
||||
|
||||
// ChangeStaticPath changes static path from old to new one.
|
||||
func (ctx *Context) ChangeStaticPath(oldPath, newPath string) {
|
||||
if !filepath.IsAbs(oldPath) {
|
||||
oldPath = filepath.Join(Root, oldPath)
|
||||
}
|
||||
dir := statics.Get(oldPath)
|
||||
if dir != nil {
|
||||
statics.Delete(oldPath)
|
||||
|
||||
if !filepath.IsAbs(newPath) {
|
||||
newPath = filepath.Join(Root, newPath)
|
||||
}
|
||||
*dir = http.Dir(newPath)
|
||||
statics.Set(dir)
|
||||
}
|
||||
}
|
||||
423
pkg/macaron/context_test.go
Executable file
423
pkg/macaron/context_test.go
Executable file
@@ -0,0 +1,423 @@
|
||||
// Copyright 2014 The Macaron Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package macaron
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/unknwon/com"
|
||||
"gopkg.in/macaron.v1/cookie"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func Test_Context(t *testing.T) {
|
||||
Convey("Do advanced encapsulation operations", t, func() {
|
||||
m := Classic()
|
||||
m.Use(Renderers(RenderOptions{
|
||||
Directory: "fixtures/basic",
|
||||
}, "fixtures/basic2"))
|
||||
|
||||
Convey("Get request body", func() {
|
||||
m.Get("/body1", func(ctx *Context) {
|
||||
data, err := ioutil.ReadAll(ctx.Req.Body().ReadCloser())
|
||||
So(err, ShouldBeNil)
|
||||
So(string(data), ShouldEqual, "This is my request body")
|
||||
})
|
||||
m.Get("/body2", func(ctx *Context) {
|
||||
data, err := ctx.Req.Body().Bytes()
|
||||
So(err, ShouldBeNil)
|
||||
So(string(data), ShouldEqual, "This is my request body")
|
||||
})
|
||||
m.Get("/body3", func(ctx *Context) {
|
||||
data, err := ctx.Req.Body().String()
|
||||
So(err, ShouldBeNil)
|
||||
So(data, ShouldEqual, "This is my request body")
|
||||
})
|
||||
m.Get("/body4", ContextInvoker(func(ctx *Context) {
|
||||
data, err := ctx.Req.Body().String()
|
||||
So(err, ShouldBeNil)
|
||||
So(data, ShouldEqual, "This is my request body")
|
||||
}))
|
||||
|
||||
for i := 1; i <= 4; i++ {
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "/body"+com.ToStr(i), nil)
|
||||
req.Body = ioutil.NopCloser(bytes.NewBufferString("This is my request body"))
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
}
|
||||
})
|
||||
|
||||
Convey("Get remote IP address", func() {
|
||||
m.Get("/remoteaddr", func(ctx *Context) string {
|
||||
return ctx.RemoteAddr()
|
||||
})
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "/remoteaddr", nil)
|
||||
req.RemoteAddr = "127.0.0.1:3333"
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
So(resp.Body.String(), ShouldEqual, "127.0.0.1")
|
||||
})
|
||||
|
||||
Convey("Render HTML", func() {
|
||||
|
||||
Convey("Normal HTML", func() {
|
||||
m.Get("/html", func(ctx *Context) {
|
||||
ctx.HTML(304, "hello", "Unknwon") // 304 for logger test.
|
||||
})
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "/html", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
So(resp.Body.String(), ShouldEqual, "<h1>Hello Unknwon</h1>")
|
||||
})
|
||||
|
||||
Convey("HTML template set", func() {
|
||||
m.Get("/html2", func(ctx *Context) {
|
||||
ctx.Data["Name"] = "Unknwon"
|
||||
ctx.HTMLSet(200, "basic2", "hello2")
|
||||
})
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "/html2", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
So(resp.Body.String(), ShouldEqual, "<h1>Hello Unknwon</h1>")
|
||||
})
|
||||
|
||||
Convey("With layout", func() {
|
||||
m.Get("/layout", func(ctx *Context) {
|
||||
ctx.HTML(200, "hello", "Unknwon", HTMLOptions{"layout"})
|
||||
})
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "/layout", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
So(resp.Body.String(), ShouldEqual, "head<h1>Hello Unknwon</h1>foot")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Parse from and query", func() {
|
||||
m.Get("/query", func(ctx *Context) string {
|
||||
var buf bytes.Buffer
|
||||
buf.WriteString(ctx.QueryTrim("name") + " ")
|
||||
buf.WriteString(ctx.QueryEscape("name") + " ")
|
||||
buf.WriteString(com.ToStr(ctx.QueryBool("bool")) + " ")
|
||||
buf.WriteString(com.ToStr(ctx.QueryInt("int")) + " ")
|
||||
buf.WriteString(com.ToStr(ctx.QueryInt64("int64")) + " ")
|
||||
buf.WriteString(com.ToStr(ctx.QueryFloat64("float64")) + " ")
|
||||
return buf.String()
|
||||
})
|
||||
m.Get("/query2", func(ctx *Context) string {
|
||||
var buf bytes.Buffer
|
||||
buf.WriteString(strings.Join(ctx.QueryStrings("list"), ",") + " ")
|
||||
buf.WriteString(strings.Join(ctx.QueryStrings("404"), ",") + " ")
|
||||
return buf.String()
|
||||
})
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "/query?name=Unknwon&bool=t&int=12&int64=123&float64=1.25", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
So(resp.Body.String(), ShouldEqual, "Unknwon Unknwon true 12 123 1.25 ")
|
||||
|
||||
resp = httptest.NewRecorder()
|
||||
req, err = http.NewRequest("GET", "/query2?list=item1&list=item2", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
So(resp.Body.String(), ShouldEqual, "item1,item2 ")
|
||||
})
|
||||
|
||||
Convey("URL parameter", func() {
|
||||
m.Get("/:name/:int/:int64/:float64", func(ctx *Context) string {
|
||||
var buf bytes.Buffer
|
||||
ctx.SetParams("name", ctx.Params("name"))
|
||||
buf.WriteString(ctx.Params(""))
|
||||
buf.WriteString(ctx.Params(":name") + " ")
|
||||
buf.WriteString(ctx.ParamsEscape(":name") + " ")
|
||||
buf.WriteString(com.ToStr(ctx.ParamsInt(":int")) + " ")
|
||||
buf.WriteString(com.ToStr(ctx.ParamsInt64(":int64")) + " ")
|
||||
buf.WriteString(com.ToStr(ctx.ParamsFloat64(":float64")) + " ")
|
||||
return buf.String()
|
||||
})
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "/user/1/13/1.24", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
So(resp.Body.String(), ShouldEqual, "user user 1 13 1.24 ")
|
||||
})
|
||||
|
||||
Convey("Get all URL paramaters", func() {
|
||||
m.Get("/:arg/:param/:flag", func(ctx *Context) string {
|
||||
kvs := make([]string, 0, len(ctx.AllParams()))
|
||||
for k, v := range ctx.AllParams() {
|
||||
kvs = append(kvs, k+"="+v)
|
||||
}
|
||||
sort.Strings(kvs)
|
||||
return strings.Join(kvs, ",")
|
||||
})
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "/1/2/3", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
So(resp.Body.String(), ShouldEqual, ":arg=1,:flag=3,:param=2")
|
||||
})
|
||||
|
||||
Convey("Get file", func() {
|
||||
m.Post("/getfile", func(ctx *Context) {
|
||||
ctx.Query("")
|
||||
_, _, _ = ctx.GetFile("hi")
|
||||
})
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("POST", "/getfile", nil)
|
||||
So(err, ShouldBeNil)
|
||||
req.Header.Set("Content-Type", "multipart/form-data")
|
||||
m.ServeHTTP(resp, req)
|
||||
})
|
||||
|
||||
Convey("Set and get cookie", func() {
|
||||
m.Get("/set", func(ctx *Context) {
|
||||
t, err := time.Parse(time.RFC1123, "Sun, 13 Mar 2016 01:29:26 UTC")
|
||||
So(err, ShouldBeNil)
|
||||
ctx.SetCookie("user", "Unknwon", 1, "/", "localhost", true, true, t)
|
||||
ctx.SetCookie("user", "Unknwon", int32(1), "/", "localhost", 1)
|
||||
called := false
|
||||
ctx.SetCookie("user", "Unknwon", int64(1), func(c *http.Cookie) {
|
||||
called = true
|
||||
})
|
||||
So(called, ShouldBeTrue)
|
||||
ctx.SetCookie("user", "Unknown",
|
||||
cookie.Secure(true),
|
||||
cookie.HttpOnly(true),
|
||||
cookie.Path("/"),
|
||||
cookie.MaxAge(1),
|
||||
cookie.Domain("localhost"),
|
||||
)
|
||||
})
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "/set", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
So(resp.Header().Get("Set-Cookie"), ShouldEqual, "user=Unknwon; Path=/; Domain=localhost; Expires=Sun, 13 Mar 2016 01:29:26 GMT; Max-Age=1; HttpOnly; Secure")
|
||||
|
||||
m.Get("/get", func(ctx *Context) string {
|
||||
ctx.GetCookie("404")
|
||||
So(ctx.GetCookieInt("uid"), ShouldEqual, 1)
|
||||
So(ctx.GetCookieInt64("uid"), ShouldEqual, 1)
|
||||
So(ctx.GetCookieFloat64("balance"), ShouldEqual, 1.25)
|
||||
return ctx.GetCookie("user")
|
||||
})
|
||||
|
||||
resp = httptest.NewRecorder()
|
||||
req, err = http.NewRequest("GET", "/get", nil)
|
||||
So(err, ShouldBeNil)
|
||||
req.Header.Set("Cookie", "user=Unknwon; uid=1; balance=1.25")
|
||||
m.ServeHTTP(resp, req)
|
||||
So(resp.Body.String(), ShouldEqual, "Unknwon")
|
||||
})
|
||||
|
||||
Convey("Set and get secure cookie", func() {
|
||||
m.SetDefaultCookieSecret("macaron")
|
||||
m.Get("/set", func(ctx *Context) {
|
||||
ctx.SetSecureCookie("user", "Unknwon", 1)
|
||||
})
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "/set", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
|
||||
cookie := resp.Header().Get("Set-Cookie")
|
||||
|
||||
m.Get("/get", func(ctx *Context) string {
|
||||
name, ok := ctx.GetSecureCookie("user")
|
||||
So(ok, ShouldBeTrue)
|
||||
return name
|
||||
})
|
||||
|
||||
resp = httptest.NewRecorder()
|
||||
req, err = http.NewRequest("GET", "/get", nil)
|
||||
So(err, ShouldBeNil)
|
||||
req.Header.Set("Cookie", cookie)
|
||||
m.ServeHTTP(resp, req)
|
||||
So(resp.Body.String(), ShouldEqual, "Unknwon")
|
||||
})
|
||||
|
||||
Convey("Serve files", func() {
|
||||
m.Get("/file", func(ctx *Context) {
|
||||
ctx.ServeFile("fixtures/custom_funcs/index.tmpl")
|
||||
})
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "/file", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
So(resp.Body.String(), ShouldEqual, "{{ myCustomFunc }}")
|
||||
|
||||
m.Get("/file2", func(ctx *Context) {
|
||||
ctx.ServeFile("fixtures/custom_funcs/index.tmpl", "ok.tmpl")
|
||||
})
|
||||
|
||||
resp = httptest.NewRecorder()
|
||||
req, err = http.NewRequest("GET", "/file2", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
So(resp.Body.String(), ShouldEqual, "{{ myCustomFunc }}")
|
||||
})
|
||||
|
||||
Convey("Serve file content", func() {
|
||||
m.Get("/file", func(ctx *Context) {
|
||||
ctx.ServeFileContent("fixtures/custom_funcs/index.tmpl")
|
||||
})
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "/file", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
So(resp.Body.String(), ShouldEqual, "{{ myCustomFunc }}")
|
||||
|
||||
m.Get("/file2", func(ctx *Context) {
|
||||
ctx.ServeFileContent("fixtures/custom_funcs/index.tmpl", "ok.tmpl")
|
||||
})
|
||||
|
||||
resp = httptest.NewRecorder()
|
||||
req, err = http.NewRequest("GET", "/file2", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
So(resp.Body.String(), ShouldEqual, "{{ myCustomFunc }}")
|
||||
|
||||
m.Get("/file3", func(ctx *Context) {
|
||||
ctx.ServeFileContent("404.tmpl")
|
||||
})
|
||||
|
||||
resp = httptest.NewRecorder()
|
||||
req, err = http.NewRequest("GET", "/file3", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
So(resp.Body.String(), ShouldEqual, "open 404.tmpl: The system cannot find the file specified.\n")
|
||||
} else {
|
||||
So(resp.Body.String(), ShouldEqual, "open 404.tmpl: no such file or directory\n")
|
||||
}
|
||||
So(resp.Code, ShouldEqual, 500)
|
||||
})
|
||||
|
||||
Convey("Serve content", func() {
|
||||
m.Get("/content", func(ctx *Context) {
|
||||
ctx.ServeContent("content1", bytes.NewReader([]byte("Hello world!")))
|
||||
})
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "/content", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
So(resp.Body.String(), ShouldEqual, "Hello world!")
|
||||
|
||||
m.Get("/content2", func(ctx *Context) {
|
||||
ctx.ServeContent("content1", bytes.NewReader([]byte("Hello world!")), time.Now())
|
||||
})
|
||||
|
||||
resp = httptest.NewRecorder()
|
||||
req, err = http.NewRequest("GET", "/content2", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
So(resp.Body.String(), ShouldEqual, "Hello world!")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Context_Render(t *testing.T) {
|
||||
Convey("Invalid render", t, func() {
|
||||
defer func() {
|
||||
So(recover(), ShouldNotBeNil)
|
||||
}()
|
||||
|
||||
m := New()
|
||||
|
||||
m.Get("/", func(ctx *Context) {
|
||||
ctx.HTML(200, "hey")
|
||||
})
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "/", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
|
||||
m.Get("/f", ContextInvoker(func(ctx *Context) {
|
||||
ctx.HTML(200, "hey")
|
||||
}))
|
||||
req, err = http.NewRequest("GET", "/f", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Context_Redirect(t *testing.T) {
|
||||
Convey("Context with default redirect", t, func() {
|
||||
url, err := url.Parse("http://localhost/path/one")
|
||||
So(err, ShouldBeNil)
|
||||
resp := httptest.NewRecorder()
|
||||
req := http.Request{
|
||||
Method: "GET",
|
||||
URL: url,
|
||||
}
|
||||
ctx := &Context{
|
||||
Req: Request{&req},
|
||||
Resp: NewResponseWriter(req.Method, resp),
|
||||
Data: make(map[string]interface{}),
|
||||
}
|
||||
ctx.Redirect("two")
|
||||
|
||||
So(resp.Code, ShouldEqual, http.StatusFound)
|
||||
So(resp.Result().Header["Location"][0], ShouldEqual, "/path/two")
|
||||
})
|
||||
|
||||
Convey("Context with custom redirect", t, func() {
|
||||
url, err := url.Parse("http://localhost/path/one")
|
||||
So(err, ShouldBeNil)
|
||||
resp := httptest.NewRecorder()
|
||||
req := http.Request{
|
||||
Method: "GET",
|
||||
URL: url,
|
||||
}
|
||||
ctx := &Context{
|
||||
Req: Request{&req},
|
||||
Resp: NewResponseWriter(req.Method, resp),
|
||||
Data: make(map[string]interface{}),
|
||||
}
|
||||
ctx.Redirect("two", 307)
|
||||
|
||||
So(resp.Code, ShouldEqual, http.StatusTemporaryRedirect)
|
||||
So(resp.Result().Header["Location"][0], ShouldEqual, "/path/two")
|
||||
})
|
||||
}
|
||||
78
pkg/macaron/cookie/helper.go
Executable file
78
pkg/macaron/cookie/helper.go
Executable file
@@ -0,0 +1,78 @@
|
||||
// Copyright 2020 The Macaron Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
// Package cookie contains helper functions for setting cookie values.
|
||||
package cookie
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// MaxAge sets the maximum age for a provided cookie
|
||||
func MaxAge(maxAge int) func(*http.Cookie) {
|
||||
return func(c *http.Cookie) {
|
||||
c.MaxAge = maxAge
|
||||
}
|
||||
}
|
||||
|
||||
// Path sets the path for a provided cookie
|
||||
func Path(path string) func(*http.Cookie) {
|
||||
return func(c *http.Cookie) {
|
||||
c.Path = path
|
||||
}
|
||||
}
|
||||
|
||||
// Domain sets the domain for a provided cookie
|
||||
func Domain(domain string) func(*http.Cookie) {
|
||||
return func(c *http.Cookie) {
|
||||
c.Domain = domain
|
||||
}
|
||||
}
|
||||
|
||||
// Secure sets the secure setting for a provided cookie
|
||||
func Secure(secure bool) func(*http.Cookie) {
|
||||
return func(c *http.Cookie) {
|
||||
c.Secure = secure
|
||||
}
|
||||
}
|
||||
|
||||
// HttpOnly sets the HttpOnly setting for a provided cookie
|
||||
func HttpOnly(httpOnly bool) func(*http.Cookie) {
|
||||
return func(c *http.Cookie) {
|
||||
c.HttpOnly = httpOnly
|
||||
}
|
||||
}
|
||||
|
||||
// HTTPOnly sets the HttpOnly setting for a provided cookie
|
||||
func HTTPOnly(httpOnly bool) func(*http.Cookie) {
|
||||
return func(c *http.Cookie) {
|
||||
c.HttpOnly = httpOnly
|
||||
}
|
||||
}
|
||||
|
||||
// Expires sets the expires and rawexpires for a provided cookie
|
||||
func Expires(expires time.Time) func(*http.Cookie) {
|
||||
return func(c *http.Cookie) {
|
||||
c.Expires = expires
|
||||
c.RawExpires = expires.Format(time.UnixDate)
|
||||
}
|
||||
}
|
||||
|
||||
// SameSite sets the SameSite for a provided cookie
|
||||
func SameSite(sameSite http.SameSite) func(*http.Cookie) {
|
||||
return func(c *http.Cookie) {
|
||||
c.SameSite = sameSite
|
||||
}
|
||||
}
|
||||
1
pkg/macaron/fixtures/basic/admin/index.tmpl
Executable file
1
pkg/macaron/fixtures/basic/admin/index.tmpl
Executable file
@@ -0,0 +1 @@
|
||||
<h1>Admin {{.}}</h1>
|
||||
1
pkg/macaron/fixtures/basic/another_layout.tmpl
Executable file
1
pkg/macaron/fixtures/basic/another_layout.tmpl
Executable file
@@ -0,0 +1 @@
|
||||
another head{{ yield }}another foot
|
||||
1
pkg/macaron/fixtures/basic/content.tmpl
Executable file
1
pkg/macaron/fixtures/basic/content.tmpl
Executable file
@@ -0,0 +1 @@
|
||||
<h1>{{ . }}</h1>
|
||||
1
pkg/macaron/fixtures/basic/current_layout.tmpl
Executable file
1
pkg/macaron/fixtures/basic/current_layout.tmpl
Executable file
@@ -0,0 +1 @@
|
||||
{{ current }} head{{ yield }}{{ current }} foot
|
||||
1
pkg/macaron/fixtures/basic/custom/hello.tmpl
Executable file
1
pkg/macaron/fixtures/basic/custom/hello.tmpl
Executable file
@@ -0,0 +1 @@
|
||||
<h1>This is custom version of: Hello {{.}}</h1>
|
||||
1
pkg/macaron/fixtures/basic/delims.tmpl
Executable file
1
pkg/macaron/fixtures/basic/delims.tmpl
Executable file
@@ -0,0 +1 @@
|
||||
<h1>Hello {[{.}]}</h1>
|
||||
1
pkg/macaron/fixtures/basic/hello.tmpl
Executable file
1
pkg/macaron/fixtures/basic/hello.tmpl
Executable file
@@ -0,0 +1 @@
|
||||
<h1>Hello {{.}}</h1>
|
||||
1
pkg/macaron/fixtures/basic/hypertext.html
Executable file
1
pkg/macaron/fixtures/basic/hypertext.html
Executable file
@@ -0,0 +1 @@
|
||||
Hypertext!
|
||||
1
pkg/macaron/fixtures/basic/layout.tmpl
Executable file
1
pkg/macaron/fixtures/basic/layout.tmpl
Executable file
@@ -0,0 +1 @@
|
||||
head{{ yield }}foot
|
||||
1
pkg/macaron/fixtures/basic2/hello.tmpl
Executable file
1
pkg/macaron/fixtures/basic2/hello.tmpl
Executable file
@@ -0,0 +1 @@
|
||||
<h1>What's up, {{.}}</h1>
|
||||
1
pkg/macaron/fixtures/basic2/hello2.tmpl
Executable file
1
pkg/macaron/fixtures/basic2/hello2.tmpl
Executable file
@@ -0,0 +1 @@
|
||||
<h1>Hello {{.Name}}</h1>
|
||||
1
pkg/macaron/fixtures/custom_funcs/index.tmpl
Executable file
1
pkg/macaron/fixtures/custom_funcs/index.tmpl
Executable file
@@ -0,0 +1 @@
|
||||
{{ myCustomFunc }}
|
||||
13
pkg/macaron/go.mod
Executable file
13
pkg/macaron/go.mod
Executable file
@@ -0,0 +1,13 @@
|
||||
module gopkg.in/macaron.v1
|
||||
|
||||
go 1.12
|
||||
|
||||
require (
|
||||
github.com/go-macaron/inject v0.0.0-20160627170012-d8a0b8677191
|
||||
github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c // indirect
|
||||
github.com/smartystreets/assertions v1.0.1 // indirect
|
||||
github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337
|
||||
github.com/unknwon/com v0.0.0-20190804042917-757f69c95f3e
|
||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4
|
||||
gopkg.in/ini.v1 v1.46.0
|
||||
)
|
||||
32
pkg/macaron/go.sum
Executable file
32
pkg/macaron/go.sum
Executable file
@@ -0,0 +1,32 @@
|
||||
github.com/go-macaron/inject v0.0.0-20160627170012-d8a0b8677191 h1:NjHlg70DuOkcAMqgt0+XA+NHwtu66MkTVVgR4fFWbcI=
|
||||
github.com/go-macaron/inject v0.0.0-20160627170012-d8a0b8677191/go.mod h1:VFI2o2q9kYsC4o7VP1HrEVosiZZTd+MVT3YZx4gqvJw=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c h1:7lF+Vz0LqiRidnzC1Oq86fpX1q/iEv2KJdrCtttYjT4=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/jtolds/gls v4.2.1+incompatible h1:fSuqC+Gmlu6l/ZYAoZzx2pyucC8Xza35fpRVWLVmUEE=
|
||||
github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 h1:Jpy1PXuP99tXNrhbq2BaPz9B+jNAvH1JPQQpG/9GCXY=
|
||||
github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/assertions v1.0.1 h1:voD4ITNjPL5jjBfgR/r8fPIIBrliWrWHeiJApdr3r4w=
|
||||
github.com/smartystreets/assertions v1.0.1/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM=
|
||||
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c h1:Ho+uVpkel/udgjbwB5Lktg9BtvJSh2DT0Hi6LPSyI2w=
|
||||
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
|
||||
github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337 h1:WN9BUFbdyOsSH/XohnWpXOlq9NBD5sGAB2FciQMUEe8=
|
||||
github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/unknwon/com v0.0.0-20190804042917-757f69c95f3e h1:GSGeB9EAKY2spCABz6xOX5DbxZEXolK+nBSvmsQwRjM=
|
||||
github.com/unknwon/com v0.0.0-20190804042917-757f69c95f3e/go.mod h1:tOOxU81rwgoCLoOVVPHb6T/wt8HZygqH5id+GNnlCXM=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc=
|
||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
gopkg.in/ini.v1 v1.46.0 h1:VeDZbLYGaupuvIrsYCEOe/L/2Pcs5n7hdO1ZTjporag=
|
||||
gopkg.in/ini.v1 v1.46.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
73
pkg/macaron/logger.go
Executable file
73
pkg/macaron/logger.go
Executable file
@@ -0,0 +1,73 @@
|
||||
// Copyright 2013 Martini Authors
|
||||
// Copyright 2014 The Macaron Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package macaron
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
ColorLog = true
|
||||
LogTimeFormat = "2006-01-02 15:04:05"
|
||||
)
|
||||
|
||||
func init() {
|
||||
ColorLog = runtime.GOOS != "windows"
|
||||
}
|
||||
|
||||
// LoggerInvoker is an inject.FastInvoker wrapper of func(ctx *Context, log *log.Logger).
|
||||
type LoggerInvoker func(ctx *Context, log *log.Logger)
|
||||
|
||||
func (invoke LoggerInvoker) Invoke(params []interface{}) ([]reflect.Value, error) {
|
||||
invoke(params[0].(*Context), params[1].(*log.Logger))
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Logger returns a middleware handler that logs the request as it goes in and the response as it goes out.
|
||||
func Logger() Handler {
|
||||
return func(ctx *Context, log *log.Logger) {
|
||||
start := time.Now()
|
||||
|
||||
log.Printf("%s: Started %s %s for %s", time.Now().Format(LogTimeFormat), ctx.Req.Method, ctx.Req.RequestURI, ctx.RemoteAddr())
|
||||
|
||||
rw := ctx.Resp.(ResponseWriter)
|
||||
ctx.Next()
|
||||
|
||||
content := fmt.Sprintf("%s: Completed %s %s %v %s in %v", time.Now().Format(LogTimeFormat), ctx.Req.Method, ctx.Req.RequestURI, rw.Status(), http.StatusText(rw.Status()), time.Since(start))
|
||||
if ColorLog {
|
||||
switch rw.Status() {
|
||||
case 200, 201, 202:
|
||||
content = fmt.Sprintf("\033[1;32m%s\033[0m", content)
|
||||
case 301, 302:
|
||||
content = fmt.Sprintf("\033[1;37m%s\033[0m", content)
|
||||
case 304:
|
||||
content = fmt.Sprintf("\033[1;33m%s\033[0m", content)
|
||||
case 401, 403:
|
||||
content = fmt.Sprintf("\033[4;31m%s\033[0m", content)
|
||||
case 404:
|
||||
content = fmt.Sprintf("\033[1;31m%s\033[0m", content)
|
||||
case 500:
|
||||
content = fmt.Sprintf("\033[1;36m%s\033[0m", content)
|
||||
}
|
||||
}
|
||||
log.Println(content)
|
||||
}
|
||||
}
|
||||
67
pkg/macaron/logger_test.go
Executable file
67
pkg/macaron/logger_test.go
Executable file
@@ -0,0 +1,67 @@
|
||||
// Copyright 2013 Martini Authors
|
||||
// Copyright 2014 The Macaron Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package macaron
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/unknwon/com"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func Test_Logger(t *testing.T) {
|
||||
Convey("Global logger", t, func() {
|
||||
buf := bytes.NewBufferString("")
|
||||
m := New()
|
||||
m.Map(log.New(buf, "[Macaron] ", 0))
|
||||
m.Use(Logger())
|
||||
m.Use(func(res http.ResponseWriter) {
|
||||
res.WriteHeader(http.StatusNotFound)
|
||||
})
|
||||
m.Get("/", func() {})
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "http://localhost:4000/", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
So(resp.Code, ShouldEqual, http.StatusNotFound)
|
||||
So(len(buf.String()), ShouldBeGreaterThan, 0)
|
||||
})
|
||||
|
||||
if ColorLog {
|
||||
Convey("Color console output", t, func() {
|
||||
m := Classic()
|
||||
m.Get("/:code:int", func(ctx *Context) (int, string) {
|
||||
return ctx.ParamsInt(":code"), ""
|
||||
})
|
||||
|
||||
// Just for testing if logger would capture.
|
||||
codes := []int{200, 201, 202, 301, 302, 304, 401, 403, 404, 500}
|
||||
for _, code := range codes {
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "http://localhost:4000/"+com.ToStr(code), nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
So(resp.Code, ShouldEqual, code)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
334
pkg/macaron/macaron.go
Executable file
334
pkg/macaron/macaron.go
Executable file
@@ -0,0 +1,334 @@
|
||||
// +build go1.3
|
||||
|
||||
// Copyright 2014 The Macaron Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
// Package macaron is a high productive and modular web framework in Go.
|
||||
package macaron // import "gopkg.in/macaron.v1"
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/unknwon/com"
|
||||
"gopkg.in/ini.v1"
|
||||
|
||||
"github.com/go-macaron/inject"
|
||||
)
|
||||
|
||||
const _VERSION = "1.3.4.0805"
|
||||
|
||||
func Version() string {
|
||||
return _VERSION
|
||||
}
|
||||
|
||||
// Handler can be any callable function.
|
||||
// Macaron attempts to inject services into the handler's argument list,
|
||||
// and panics if an argument could not be fullfilled via dependency injection.
|
||||
type Handler interface{}
|
||||
|
||||
// handlerFuncInvoker is an inject.FastInvoker wrapper of func(http.ResponseWriter, *http.Request).
|
||||
type handlerFuncInvoker func(http.ResponseWriter, *http.Request)
|
||||
|
||||
func (invoke handlerFuncInvoker) Invoke(params []interface{}) ([]reflect.Value, error) {
|
||||
invoke(params[0].(http.ResponseWriter), params[1].(*http.Request))
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// internalServerErrorInvoker is an inject.FastInvoker wrapper of func(rw http.ResponseWriter, err error).
|
||||
type internalServerErrorInvoker func(rw http.ResponseWriter, err error)
|
||||
|
||||
func (invoke internalServerErrorInvoker) Invoke(params []interface{}) ([]reflect.Value, error) {
|
||||
invoke(params[0].(http.ResponseWriter), params[1].(error))
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// validateAndWrapHandler makes sure a handler is a callable function, it panics if not.
|
||||
// When the handler is also potential to be any built-in inject.FastInvoker,
|
||||
// it wraps the handler automatically to have some performance gain.
|
||||
func validateAndWrapHandler(h Handler) Handler {
|
||||
if reflect.TypeOf(h).Kind() != reflect.Func {
|
||||
panic("Macaron handler must be a callable function")
|
||||
}
|
||||
|
||||
if !inject.IsFastInvoker(h) {
|
||||
switch v := h.(type) {
|
||||
case func(*Context):
|
||||
return ContextInvoker(v)
|
||||
case func(*Context, *log.Logger):
|
||||
return LoggerInvoker(v)
|
||||
case func(http.ResponseWriter, *http.Request):
|
||||
return handlerFuncInvoker(v)
|
||||
case func(http.ResponseWriter, error):
|
||||
return internalServerErrorInvoker(v)
|
||||
}
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
// validateAndWrapHandlers preforms validation and wrapping for each input handler.
|
||||
// It accepts an optional wrapper function to perform custom wrapping on handlers.
|
||||
func validateAndWrapHandlers(handlers []Handler, wrappers ...func(Handler) Handler) []Handler {
|
||||
var wrapper func(Handler) Handler
|
||||
if len(wrappers) > 0 {
|
||||
wrapper = wrappers[0]
|
||||
}
|
||||
|
||||
wrappedHandlers := make([]Handler, len(handlers))
|
||||
for i, h := range handlers {
|
||||
h = validateAndWrapHandler(h)
|
||||
if wrapper != nil && !inject.IsFastInvoker(h) {
|
||||
h = wrapper(h)
|
||||
}
|
||||
wrappedHandlers[i] = h
|
||||
}
|
||||
|
||||
return wrappedHandlers
|
||||
}
|
||||
|
||||
// Macaron represents the top level web application.
|
||||
// inject.Injector methods can be invoked to map services on a global level.
|
||||
type Macaron struct {
|
||||
inject.Injector
|
||||
befores []BeforeHandler
|
||||
handlers []Handler
|
||||
action Handler
|
||||
|
||||
hasURLPrefix bool
|
||||
urlPrefix string // For suburl support.
|
||||
*Router
|
||||
|
||||
logger *log.Logger
|
||||
}
|
||||
|
||||
// NewWithLogger creates a bare bones Macaron instance.
|
||||
// Use this method if you want to have full control over the middleware that is used.
|
||||
// You can specify logger output writer with this function.
|
||||
func NewWithLogger(out io.Writer) *Macaron {
|
||||
m := &Macaron{
|
||||
Injector: inject.New(),
|
||||
action: func() {},
|
||||
Router: NewRouter(),
|
||||
logger: log.New(out, "[Macaron] ", 0),
|
||||
}
|
||||
m.Router.m = m
|
||||
m.Map(m.logger)
|
||||
m.Map(defaultReturnHandler())
|
||||
m.NotFound(http.NotFound)
|
||||
m.InternalServerError(func(rw http.ResponseWriter, err error) {
|
||||
http.Error(rw, err.Error(), 500)
|
||||
})
|
||||
return m
|
||||
}
|
||||
|
||||
// New creates a bare bones Macaron instance.
|
||||
// Use this method if you want to have full control over the middleware that is used.
|
||||
func New() *Macaron {
|
||||
return NewWithLogger(os.Stdout)
|
||||
}
|
||||
|
||||
// Classic creates a classic Macaron with some basic default middleware:
|
||||
// macaron.Logger, macaron.Recovery and macaron.Static.
|
||||
func Classic() *Macaron {
|
||||
m := New()
|
||||
m.Use(Logger())
|
||||
m.Use(Recovery())
|
||||
m.Use(Static("public"))
|
||||
return m
|
||||
}
|
||||
|
||||
// Handlers sets the entire middleware stack with the given Handlers.
|
||||
// This will clear any current middleware handlers,
|
||||
// and panics if any of the handlers is not a callable function
|
||||
func (m *Macaron) Handlers(handlers ...Handler) {
|
||||
m.handlers = make([]Handler, 0)
|
||||
for _, handler := range handlers {
|
||||
m.Use(handler)
|
||||
}
|
||||
}
|
||||
|
||||
// Action sets the handler that will be called after all the middleware has been invoked.
|
||||
// This is set to macaron.Router in a macaron.Classic().
|
||||
func (m *Macaron) Action(handler Handler) {
|
||||
handler = validateAndWrapHandler(handler)
|
||||
m.action = handler
|
||||
}
|
||||
|
||||
// BeforeHandler represents a handler executes at beginning of every request.
|
||||
// Macaron stops future process when it returns true.
|
||||
type BeforeHandler func(rw http.ResponseWriter, req *http.Request) bool
|
||||
|
||||
func (m *Macaron) Before(handler BeforeHandler) {
|
||||
m.befores = append(m.befores, handler)
|
||||
}
|
||||
|
||||
// Use adds a middleware Handler to the stack,
|
||||
// and panics if the handler is not a callable func.
|
||||
// Middleware Handlers are invoked in the order that they are added.
|
||||
func (m *Macaron) Use(handler Handler) {
|
||||
handler = validateAndWrapHandler(handler)
|
||||
m.handlers = append(m.handlers, handler)
|
||||
}
|
||||
|
||||
func (m *Macaron) createContext(rw http.ResponseWriter, req *http.Request) *Context {
|
||||
c := &Context{
|
||||
Injector: inject.New(),
|
||||
handlers: m.handlers,
|
||||
action: m.action,
|
||||
index: 0,
|
||||
Router: m.Router,
|
||||
Req: Request{req},
|
||||
Resp: NewResponseWriter(req.Method, rw),
|
||||
Render: &DummyRender{rw},
|
||||
Data: make(map[string]interface{}),
|
||||
}
|
||||
c.SetParent(m)
|
||||
c.Map(c)
|
||||
c.MapTo(c.Resp, (*http.ResponseWriter)(nil))
|
||||
c.Map(req)
|
||||
return c
|
||||
}
|
||||
|
||||
// ServeHTTP is the HTTP Entry point for a Macaron instance.
|
||||
// Useful if you want to control your own HTTP server.
|
||||
// Be aware that none of middleware will run without registering any router.
|
||||
func (m *Macaron) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
if m.hasURLPrefix {
|
||||
req.URL.Path = strings.TrimPrefix(req.URL.Path, m.urlPrefix)
|
||||
}
|
||||
for _, h := range m.befores {
|
||||
if h(rw, req) {
|
||||
return
|
||||
}
|
||||
}
|
||||
m.Router.ServeHTTP(rw, req)
|
||||
}
|
||||
|
||||
func GetDefaultListenInfo() (string, int) {
|
||||
host := os.Getenv("HOST")
|
||||
if len(host) == 0 {
|
||||
host = "0.0.0.0"
|
||||
}
|
||||
port := com.StrTo(os.Getenv("PORT")).MustInt()
|
||||
if port == 0 {
|
||||
port = 4000
|
||||
}
|
||||
return host, port
|
||||
}
|
||||
|
||||
// Run the http server. Listening on os.GetEnv("PORT") or 4000 by default.
|
||||
func (m *Macaron) Run(args ...interface{}) {
|
||||
host, port := GetDefaultListenInfo()
|
||||
if len(args) == 1 {
|
||||
switch arg := args[0].(type) {
|
||||
case string:
|
||||
host = arg
|
||||
case int:
|
||||
port = arg
|
||||
}
|
||||
} else if len(args) >= 2 {
|
||||
if arg, ok := args[0].(string); ok {
|
||||
host = arg
|
||||
}
|
||||
if arg, ok := args[1].(int); ok {
|
||||
port = arg
|
||||
}
|
||||
}
|
||||
|
||||
addr := host + ":" + com.ToStr(port)
|
||||
logger := m.GetVal(reflect.TypeOf(m.logger)).Interface().(*log.Logger)
|
||||
logger.Printf("listening on %s (%s)\n", addr, safeEnv())
|
||||
logger.Fatalln(http.ListenAndServe(addr, m))
|
||||
}
|
||||
|
||||
// SetURLPrefix sets URL prefix of router layer, so that it support suburl.
|
||||
func (m *Macaron) SetURLPrefix(prefix string) {
|
||||
m.urlPrefix = prefix
|
||||
m.hasURLPrefix = len(m.urlPrefix) > 0
|
||||
}
|
||||
|
||||
// ____ ____ .__ ___. .__
|
||||
// \ \ / /____ _______|__|____ \_ |__ | | ____ ______
|
||||
// \ Y /\__ \\_ __ \ \__ \ | __ \| | _/ __ \ / ___/
|
||||
// \ / / __ \| | \/ |/ __ \| \_\ \ |_\ ___/ \___ \
|
||||
// \___/ (____ /__| |__(____ /___ /____/\___ >____ >
|
||||
// \/ \/ \/ \/ \/
|
||||
|
||||
const (
|
||||
DEV = "development"
|
||||
PROD = "production"
|
||||
TEST = "test"
|
||||
)
|
||||
|
||||
var (
|
||||
// Env is the environment that Macaron is executing in.
|
||||
// The MACARON_ENV is read on initialization to set this variable.
|
||||
Env = DEV
|
||||
envLock sync.Mutex
|
||||
|
||||
// Path of work directory.
|
||||
Root string
|
||||
|
||||
// Flash applies to current request.
|
||||
FlashNow bool
|
||||
|
||||
// Configuration convention object.
|
||||
cfg *ini.File
|
||||
)
|
||||
|
||||
func setENV(e string) {
|
||||
envLock.Lock()
|
||||
defer envLock.Unlock()
|
||||
|
||||
if len(e) > 0 {
|
||||
Env = e
|
||||
}
|
||||
}
|
||||
|
||||
func safeEnv() string {
|
||||
envLock.Lock()
|
||||
defer envLock.Unlock()
|
||||
|
||||
return Env
|
||||
}
|
||||
|
||||
func init() {
|
||||
setENV(os.Getenv("MACARON_ENV"))
|
||||
|
||||
var err error
|
||||
Root, err = os.Getwd()
|
||||
if err != nil {
|
||||
panic("error getting work directory: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// SetConfig sets data sources for configuration.
|
||||
func SetConfig(source interface{}, others ...interface{}) (_ *ini.File, err error) {
|
||||
cfg, err = ini.Load(source, others...)
|
||||
return Config(), err
|
||||
}
|
||||
|
||||
// Config returns configuration convention object.
|
||||
// It returns an empty object if there is no one available.
|
||||
func Config() *ini.File {
|
||||
if cfg == nil {
|
||||
return ini.Empty()
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
218
pkg/macaron/macaron_test.go
Executable file
218
pkg/macaron/macaron_test.go
Executable file
@@ -0,0 +1,218 @@
|
||||
// Copyright 2013 Martini Authors
|
||||
// Copyright 2014 The Macaron Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package macaron
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func Test_Version(t *testing.T) {
|
||||
Convey("Get version", t, func() {
|
||||
So(Version(), ShouldEqual, _VERSION)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_New(t *testing.T) {
|
||||
Convey("Initialize a new instance", t, func() {
|
||||
So(New(), ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("Just test that Run doesn't bomb", t, func() {
|
||||
go New().Run()
|
||||
time.Sleep(1 * time.Second)
|
||||
os.Setenv("PORT", "4001")
|
||||
go New().Run("0.0.0.0")
|
||||
go New().Run(4002)
|
||||
go New().Run("0.0.0.0", 4003)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Macaron_Before(t *testing.T) {
|
||||
Convey("Register before handlers", t, func() {
|
||||
m := New()
|
||||
m.Before(func(rw http.ResponseWriter, req *http.Request) bool {
|
||||
return false
|
||||
})
|
||||
m.Before(func(rw http.ResponseWriter, req *http.Request) bool {
|
||||
return true
|
||||
})
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "/", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Macaron_ServeHTTP(t *testing.T) {
|
||||
Convey("Serve HTTP requests", t, func() {
|
||||
result := ""
|
||||
m := New()
|
||||
m.Use(func(c *Context) {
|
||||
result += "foo"
|
||||
c.Next()
|
||||
result += "ban"
|
||||
})
|
||||
m.Use(func(c *Context) {
|
||||
result += "bar"
|
||||
c.Next()
|
||||
result += "baz"
|
||||
})
|
||||
m.Get("/", func() {})
|
||||
m.Action(func(res http.ResponseWriter, req *http.Request) {
|
||||
result += "bat"
|
||||
res.WriteHeader(http.StatusBadRequest)
|
||||
})
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "/", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
So(result, ShouldEqual, "foobarbatbazban")
|
||||
So(resp.Code, ShouldEqual, http.StatusBadRequest)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Macaron_Handlers(t *testing.T) {
|
||||
Convey("Add custom handlers", t, func() {
|
||||
result := ""
|
||||
batman := func(c *Context) {
|
||||
result += "batman!"
|
||||
}
|
||||
|
||||
m := New()
|
||||
m.Use(func(c *Context) {
|
||||
result += "foo"
|
||||
c.Next()
|
||||
result += "ban"
|
||||
})
|
||||
m.Handlers(
|
||||
batman,
|
||||
batman,
|
||||
batman,
|
||||
)
|
||||
|
||||
Convey("Add not callable function", func() {
|
||||
defer func() {
|
||||
So(recover(), ShouldNotBeNil)
|
||||
}()
|
||||
m.Use("shit")
|
||||
})
|
||||
|
||||
m.Get("/", func() {})
|
||||
m.Action(func(res http.ResponseWriter, req *http.Request) {
|
||||
result += "bat"
|
||||
res.WriteHeader(http.StatusBadRequest)
|
||||
})
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "/", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
So(result, ShouldEqual, "batman!batman!batman!bat")
|
||||
So(resp.Code, ShouldEqual, http.StatusBadRequest)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Macaron_EarlyWrite(t *testing.T) {
|
||||
Convey("Write early content to response", t, func() {
|
||||
result := ""
|
||||
m := New()
|
||||
m.Use(func(res http.ResponseWriter) {
|
||||
result += "foobar"
|
||||
_, _ = res.Write([]byte("Hello world"))
|
||||
})
|
||||
m.Use(func() {
|
||||
result += "bat"
|
||||
})
|
||||
m.Get("/", func() {})
|
||||
m.Action(func(res http.ResponseWriter) {
|
||||
result += "baz"
|
||||
res.WriteHeader(http.StatusBadRequest)
|
||||
})
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "/", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
So(result, ShouldEqual, "foobar")
|
||||
So(resp.Code, ShouldEqual, http.StatusOK)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Macaron_Written(t *testing.T) {
|
||||
Convey("Written sign", t, func() {
|
||||
resp := httptest.NewRecorder()
|
||||
m := New()
|
||||
m.Handlers(func(res http.ResponseWriter) {
|
||||
res.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
ctx := m.createContext(resp, &http.Request{Method: "GET"})
|
||||
So(ctx.Written(), ShouldBeFalse)
|
||||
|
||||
ctx.run()
|
||||
So(ctx.Written(), ShouldBeTrue)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Macaron_Basic_NoRace(t *testing.T) {
|
||||
Convey("Make sure no race between requests", t, func() {
|
||||
m := New()
|
||||
handlers := []Handler{func() {}, func() {}}
|
||||
// Ensure append will not realloc to trigger the race condition
|
||||
m.handlers = handlers[:1]
|
||||
m.Get("/", func() {})
|
||||
for i := 0; i < 2; i++ {
|
||||
go func() {
|
||||
req, _ := http.NewRequest("GET", "/", nil)
|
||||
resp := httptest.NewRecorder()
|
||||
m.ServeHTTP(resp, req)
|
||||
}()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func Test_SetENV(t *testing.T) {
|
||||
Convey("Get and save environment variable", t, func() {
|
||||
tests := []struct {
|
||||
in string
|
||||
out string
|
||||
}{
|
||||
{"", "development"},
|
||||
{"not_development", "not_development"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
setENV(test.in)
|
||||
So(Env, ShouldEqual, test.out)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Config(t *testing.T) {
|
||||
Convey("Set and get configuration object", t, func() {
|
||||
So(Config(), ShouldNotBeNil)
|
||||
cfg, err := SetConfig([]byte(""))
|
||||
So(err, ShouldBeNil)
|
||||
So(cfg, ShouldNotBeNil)
|
||||
})
|
||||
}
|
||||
BIN
pkg/macaron/macaronlogo.png
Executable file
BIN
pkg/macaron/macaronlogo.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 87 KiB |
163
pkg/macaron/recovery.go
Executable file
163
pkg/macaron/recovery.go
Executable file
@@ -0,0 +1,163 @@
|
||||
// Copyright 2013 Martini Authors
|
||||
// Copyright 2014 The Macaron Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package macaron
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"runtime"
|
||||
|
||||
"github.com/go-macaron/inject"
|
||||
)
|
||||
|
||||
const (
|
||||
panicHtml = `<html>
|
||||
<head><title>PANIC: %s</title>
|
||||
<meta charset="utf-8" />
|
||||
<style type="text/css">
|
||||
html, body {
|
||||
font-family: "Roboto", sans-serif;
|
||||
color: #333333;
|
||||
background-color: #ea5343;
|
||||
margin: 0px;
|
||||
}
|
||||
h1 {
|
||||
color: #d04526;
|
||||
background-color: #ffffff;
|
||||
padding: 20px;
|
||||
border-bottom: 1px dashed #2b3848;
|
||||
}
|
||||
pre {
|
||||
margin: 20px;
|
||||
padding: 20px;
|
||||
border: 2px solid #2b3848;
|
||||
background-color: #ffffff;
|
||||
white-space: pre-wrap; /* css-3 */
|
||||
white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
|
||||
white-space: -pre-wrap; /* Opera 4-6 */
|
||||
white-space: -o-pre-wrap; /* Opera 7 */
|
||||
word-wrap: break-word; /* Internet Explorer 5.5+ */
|
||||
}
|
||||
</style>
|
||||
</head><body>
|
||||
<h1>PANIC</h1>
|
||||
<pre style="font-weight: bold;">%s</pre>
|
||||
<pre>%s</pre>
|
||||
</body>
|
||||
</html>`
|
||||
)
|
||||
|
||||
var (
|
||||
dunno = []byte("???")
|
||||
centerDot = []byte("·")
|
||||
dot = []byte(".")
|
||||
slash = []byte("/")
|
||||
)
|
||||
|
||||
// stack returns a nicely formated stack frame, skipping skip frames
|
||||
func stack(skip int) []byte {
|
||||
buf := new(bytes.Buffer) // the returned data
|
||||
// As we loop, we open files and read them. These variables record the currently
|
||||
// loaded file.
|
||||
var lines [][]byte
|
||||
var lastFile string
|
||||
for i := skip; ; i++ { // Skip the expected number of frames
|
||||
pc, file, line, ok := runtime.Caller(i)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
// Print this much at least. If we can't find the source, it won't show.
|
||||
fmt.Fprintf(buf, "%s:%d (0x%x)\n", file, line, pc)
|
||||
if file != lastFile {
|
||||
data, err := ioutil.ReadFile(file)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
lines = bytes.Split(data, []byte{'\n'})
|
||||
lastFile = file
|
||||
}
|
||||
fmt.Fprintf(buf, "\t%s: %s\n", function(pc), source(lines, line))
|
||||
}
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// source returns a space-trimmed slice of the n'th line.
|
||||
func source(lines [][]byte, n int) []byte {
|
||||
n-- // in stack trace, lines are 1-indexed but our array is 0-indexed
|
||||
if n < 0 || n >= len(lines) {
|
||||
return dunno
|
||||
}
|
||||
return bytes.TrimSpace(lines[n])
|
||||
}
|
||||
|
||||
// function returns, if possible, the name of the function containing the PC.
|
||||
func function(pc uintptr) []byte {
|
||||
fn := runtime.FuncForPC(pc)
|
||||
if fn == nil {
|
||||
return dunno
|
||||
}
|
||||
name := []byte(fn.Name())
|
||||
// The name includes the path name to the package, which is unnecessary
|
||||
// since the file name is already included. Plus, it has center dots.
|
||||
// That is, we see
|
||||
// runtime/debug.*T·ptrmethod
|
||||
// and want
|
||||
// *T.ptrmethod
|
||||
// Also the package path might contains dot (e.g. code.google.com/...),
|
||||
// so first eliminate the path prefix
|
||||
if lastslash := bytes.LastIndex(name, slash); lastslash >= 0 {
|
||||
name = name[lastslash+1:]
|
||||
}
|
||||
if period := bytes.Index(name, dot); period >= 0 {
|
||||
name = name[period+1:]
|
||||
}
|
||||
name = bytes.Replace(name, centerDot, dot, -1)
|
||||
return name
|
||||
}
|
||||
|
||||
// Recovery returns a middleware that recovers from any panics and writes a 500 if there was one.
|
||||
// While Martini is in development mode, Recovery will also output the panic as HTML.
|
||||
func Recovery() Handler {
|
||||
return func(c *Context, log *log.Logger) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
stack := stack(3)
|
||||
log.Printf("PANIC: %s\n%s", err, stack)
|
||||
|
||||
// Lookup the current responsewriter
|
||||
val := c.GetVal(inject.InterfaceOf((*http.ResponseWriter)(nil)))
|
||||
res := val.Interface().(http.ResponseWriter)
|
||||
|
||||
// respond with panic message while in development mode
|
||||
var body []byte
|
||||
if Env == DEV {
|
||||
res.Header().Set("Content-Type", "text/html")
|
||||
body = []byte(fmt.Sprintf(panicHtml, err, err, stack))
|
||||
}
|
||||
|
||||
res.WriteHeader(http.StatusInternalServerError)
|
||||
if nil != body {
|
||||
_, _ = res.Write(body)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
74
pkg/macaron/recovery_test.go
Executable file
74
pkg/macaron/recovery_test.go
Executable file
@@ -0,0 +1,74 @@
|
||||
// Copyright 2013 Martini Authors
|
||||
// Copyright 2014 The Macaron Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package macaron
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func Test_Recovery(t *testing.T) {
|
||||
Convey("Recovery from panic", t, func() {
|
||||
buf := bytes.NewBufferString("")
|
||||
setENV(DEV)
|
||||
|
||||
m := New()
|
||||
m.Map(log.New(buf, "[Macaron] ", 0))
|
||||
m.Use(func(res http.ResponseWriter, req *http.Request) {
|
||||
res.Header().Set("Content-Type", "unpredictable")
|
||||
})
|
||||
m.Use(Recovery())
|
||||
m.Use(func(res http.ResponseWriter, req *http.Request) {
|
||||
panic("here is a panic!")
|
||||
})
|
||||
m.Get("/", func() {})
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "/", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
So(resp.Code, ShouldEqual, http.StatusInternalServerError)
|
||||
So(resp.Header().Get("Content-Type"), ShouldEqual, "text/html")
|
||||
So(buf.String(), ShouldNotBeEmpty)
|
||||
})
|
||||
|
||||
Convey("Revocery panic to another response writer", t, func() {
|
||||
resp := httptest.NewRecorder()
|
||||
resp2 := httptest.NewRecorder()
|
||||
setENV(DEV)
|
||||
|
||||
m := New()
|
||||
m.Use(Recovery())
|
||||
m.Use(func(c *Context) {
|
||||
c.MapTo(resp2, (*http.ResponseWriter)(nil))
|
||||
panic("here is a panic!")
|
||||
})
|
||||
m.Get("/", func() {})
|
||||
|
||||
req, err := http.NewRequest("GET", "/", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
|
||||
So(resp2.Code, ShouldEqual, http.StatusInternalServerError)
|
||||
So(resp2.Header().Get("Content-Type"), ShouldEqual, "text/html")
|
||||
So(resp2.Body.Len(), ShouldBeGreaterThan, 0)
|
||||
})
|
||||
}
|
||||
724
pkg/macaron/render.go
Executable file
724
pkg/macaron/render.go
Executable file
@@ -0,0 +1,724 @@
|
||||
// Copyright 2013 Martini Authors
|
||||
// Copyright 2014 The Macaron Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package macaron
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/unknwon/com"
|
||||
)
|
||||
|
||||
const (
|
||||
_CONTENT_TYPE = "Content-Type"
|
||||
_CONTENT_BINARY = "application/octet-stream"
|
||||
_CONTENT_JSON = "application/json"
|
||||
_CONTENT_HTML = "text/html"
|
||||
_CONTENT_PLAIN = "text/plain"
|
||||
_CONTENT_XHTML = "application/xhtml+xml"
|
||||
_CONTENT_XML = "text/xml"
|
||||
_DEFAULT_CHARSET = "UTF-8"
|
||||
)
|
||||
|
||||
var (
|
||||
// Provides a temporary buffer to execute templates into and catch errors.
|
||||
bufpool = sync.Pool{
|
||||
New: func() interface{} { return new(bytes.Buffer) },
|
||||
}
|
||||
|
||||
// Included helper functions for use when rendering html
|
||||
helperFuncs = template.FuncMap{
|
||||
"yield": func() (string, error) {
|
||||
return "", fmt.Errorf("yield called with no layout defined")
|
||||
},
|
||||
"current": func() (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
type (
|
||||
// TemplateFile represents a interface of template file that has name and can be read.
|
||||
TemplateFile interface {
|
||||
Name() string
|
||||
Data() []byte
|
||||
Ext() string
|
||||
}
|
||||
// TemplateFileSystem represents a interface of template file system that able to list all files.
|
||||
TemplateFileSystem interface {
|
||||
ListFiles() []TemplateFile
|
||||
Get(string) (io.Reader, error)
|
||||
}
|
||||
|
||||
// Delims represents a set of Left and Right delimiters for HTML template rendering
|
||||
Delims struct {
|
||||
// Left delimiter, defaults to {{
|
||||
Left string
|
||||
// Right delimiter, defaults to }}
|
||||
Right string
|
||||
}
|
||||
|
||||
// RenderOptions represents a struct for specifying configuration options for the Render middleware.
|
||||
RenderOptions struct {
|
||||
// Directory to load templates. Default is "templates".
|
||||
Directory string
|
||||
// Addtional directories to overwite templates.
|
||||
AppendDirectories []string
|
||||
// Layout template name. Will not render a layout if "". Default is to "".
|
||||
Layout string
|
||||
// Extensions to parse template files from. Defaults are [".tmpl", ".html"].
|
||||
Extensions []string
|
||||
// Funcs is a slice of FuncMaps to apply to the template upon compilation. This is useful for helper functions. Default is [].
|
||||
Funcs []template.FuncMap
|
||||
// Delims sets the action delimiters to the specified strings in the Delims struct.
|
||||
Delims Delims
|
||||
// Appends the given charset to the Content-Type header. Default is "UTF-8".
|
||||
Charset string
|
||||
// Outputs human readable JSON.
|
||||
IndentJSON bool
|
||||
// Outputs human readable XML.
|
||||
IndentXML bool
|
||||
// Prefixes the JSON output with the given bytes.
|
||||
PrefixJSON []byte
|
||||
// Prefixes the XML output with the given bytes.
|
||||
PrefixXML []byte
|
||||
// Allows changing of output to XHTML instead of HTML. Default is "text/html"
|
||||
HTMLContentType string
|
||||
// TemplateFileSystem is the interface for supporting any implmentation of template file system.
|
||||
TemplateFileSystem
|
||||
}
|
||||
|
||||
// HTMLOptions is a struct for overriding some rendering Options for specific HTML call
|
||||
HTMLOptions struct {
|
||||
// Layout template name. Overrides Options.Layout.
|
||||
Layout string
|
||||
}
|
||||
|
||||
Render interface {
|
||||
http.ResponseWriter
|
||||
SetResponseWriter(http.ResponseWriter)
|
||||
|
||||
JSON(int, interface{})
|
||||
JSONString(interface{}) (string, error)
|
||||
RawData(int, []byte) // Serve content as binary
|
||||
PlainText(int, []byte) // Serve content as plain text
|
||||
HTML(int, string, interface{}, ...HTMLOptions)
|
||||
HTMLSet(int, string, string, interface{}, ...HTMLOptions)
|
||||
HTMLSetString(string, string, interface{}, ...HTMLOptions) (string, error)
|
||||
HTMLString(string, interface{}, ...HTMLOptions) (string, error)
|
||||
HTMLSetBytes(string, string, interface{}, ...HTMLOptions) ([]byte, error)
|
||||
HTMLBytes(string, interface{}, ...HTMLOptions) ([]byte, error)
|
||||
XML(int, interface{})
|
||||
Error(int, ...string)
|
||||
Status(int)
|
||||
SetTemplatePath(string, string)
|
||||
HasTemplateSet(string) bool
|
||||
}
|
||||
)
|
||||
|
||||
// TplFile implements TemplateFile interface.
|
||||
type TplFile struct {
|
||||
name string
|
||||
data []byte
|
||||
ext string
|
||||
}
|
||||
|
||||
// NewTplFile cerates new template file with given name and data.
|
||||
func NewTplFile(name string, data []byte, ext string) *TplFile {
|
||||
return &TplFile{name, data, ext}
|
||||
}
|
||||
|
||||
func (f *TplFile) Name() string {
|
||||
return f.name
|
||||
}
|
||||
|
||||
func (f *TplFile) Data() []byte {
|
||||
return f.data
|
||||
}
|
||||
|
||||
func (f *TplFile) Ext() string {
|
||||
return f.ext
|
||||
}
|
||||
|
||||
// TplFileSystem implements TemplateFileSystem interface.
|
||||
type TplFileSystem struct {
|
||||
files []TemplateFile
|
||||
}
|
||||
|
||||
// NewTemplateFileSystem creates new template file system with given options.
|
||||
func NewTemplateFileSystem(opt RenderOptions, omitData bool) TplFileSystem {
|
||||
fs := TplFileSystem{}
|
||||
fs.files = make([]TemplateFile, 0, 10)
|
||||
|
||||
// Directories are composed in reverse order because later one overwrites previous ones,
|
||||
// so once found, we can directly jump out of the loop.
|
||||
dirs := make([]string, 0, len(opt.AppendDirectories)+1)
|
||||
for i := len(opt.AppendDirectories) - 1; i >= 0; i-- {
|
||||
dirs = append(dirs, opt.AppendDirectories[i])
|
||||
}
|
||||
dirs = append(dirs, opt.Directory)
|
||||
|
||||
var err error
|
||||
for i := range dirs {
|
||||
// Skip ones that does not exists for symlink test,
|
||||
// but allow non-symlink ones added after start.
|
||||
if !com.IsExist(dirs[i]) {
|
||||
continue
|
||||
}
|
||||
|
||||
dirs[i], err = filepath.EvalSymlinks(dirs[i])
|
||||
if err != nil {
|
||||
panic("EvalSymlinks(" + dirs[i] + "): " + err.Error())
|
||||
}
|
||||
}
|
||||
lastDir := dirs[len(dirs)-1]
|
||||
|
||||
// We still walk the last (original) directory because it's non-sense we load templates not exist in original directory.
|
||||
if err = filepath.Walk(lastDir, func(path string, info os.FileInfo, _ error) error {
|
||||
r, err := filepath.Rel(lastDir, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ext := GetExt(r)
|
||||
|
||||
for _, extension := range opt.Extensions {
|
||||
if ext != extension {
|
||||
continue
|
||||
}
|
||||
|
||||
var data []byte
|
||||
if !omitData {
|
||||
// Loop over candidates of directory, break out once found.
|
||||
// The file always exists because it's inside the walk function,
|
||||
// and read original file is the worst case.
|
||||
for i := range dirs {
|
||||
path = filepath.Join(dirs[i], r)
|
||||
if !com.IsFile(path) {
|
||||
continue
|
||||
}
|
||||
|
||||
data, err = ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
name := filepath.ToSlash((r[0 : len(r)-len(ext)]))
|
||||
fs.files = append(fs.files, NewTplFile(name, data, ext))
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
panic("NewTemplateFileSystem: " + err.Error())
|
||||
}
|
||||
|
||||
return fs
|
||||
}
|
||||
|
||||
func (fs TplFileSystem) ListFiles() []TemplateFile {
|
||||
return fs.files
|
||||
}
|
||||
|
||||
func (fs TplFileSystem) Get(name string) (io.Reader, error) {
|
||||
for i := range fs.files {
|
||||
if fs.files[i].Name()+fs.files[i].Ext() == name {
|
||||
return bytes.NewReader(fs.files[i].Data()), nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("file '%s' not found", name)
|
||||
}
|
||||
|
||||
func PrepareCharset(charset string) string {
|
||||
if len(charset) != 0 {
|
||||
return "; charset=" + charset
|
||||
}
|
||||
|
||||
return "; charset=" + _DEFAULT_CHARSET
|
||||
}
|
||||
|
||||
func GetExt(s string) string {
|
||||
index := strings.Index(s, ".")
|
||||
if index == -1 {
|
||||
return ""
|
||||
}
|
||||
return s[index:]
|
||||
}
|
||||
|
||||
func compile(opt RenderOptions) *template.Template {
|
||||
t := template.New(opt.Directory)
|
||||
t.Delims(opt.Delims.Left, opt.Delims.Right)
|
||||
// Parse an initial template in case we don't have any.
|
||||
template.Must(t.Parse("Macaron"))
|
||||
|
||||
if opt.TemplateFileSystem == nil {
|
||||
opt.TemplateFileSystem = NewTemplateFileSystem(opt, false)
|
||||
}
|
||||
|
||||
for _, f := range opt.TemplateFileSystem.ListFiles() {
|
||||
tmpl := t.New(f.Name())
|
||||
for _, funcs := range opt.Funcs {
|
||||
tmpl.Funcs(funcs)
|
||||
}
|
||||
// Bomb out if parse fails. We don't want any silent server starts.
|
||||
template.Must(tmpl.Funcs(helperFuncs).Parse(string(f.Data())))
|
||||
}
|
||||
|
||||
return t
|
||||
}
|
||||
|
||||
const (
|
||||
DEFAULT_TPL_SET_NAME = "DEFAULT"
|
||||
)
|
||||
|
||||
// TemplateSet represents a template set of type *template.Template.
|
||||
type TemplateSet struct {
|
||||
lock sync.RWMutex
|
||||
sets map[string]*template.Template
|
||||
dirs map[string]string
|
||||
}
|
||||
|
||||
// NewTemplateSet initializes a new empty template set.
|
||||
func NewTemplateSet() *TemplateSet {
|
||||
return &TemplateSet{
|
||||
sets: make(map[string]*template.Template),
|
||||
dirs: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
func (ts *TemplateSet) Set(name string, opt *RenderOptions) *template.Template {
|
||||
t := compile(*opt)
|
||||
|
||||
ts.lock.Lock()
|
||||
defer ts.lock.Unlock()
|
||||
|
||||
ts.sets[name] = t
|
||||
ts.dirs[name] = opt.Directory
|
||||
return t
|
||||
}
|
||||
|
||||
func (ts *TemplateSet) Get(name string) *template.Template {
|
||||
ts.lock.RLock()
|
||||
defer ts.lock.RUnlock()
|
||||
|
||||
return ts.sets[name]
|
||||
}
|
||||
|
||||
func (ts *TemplateSet) GetDir(name string) string {
|
||||
ts.lock.RLock()
|
||||
defer ts.lock.RUnlock()
|
||||
|
||||
return ts.dirs[name]
|
||||
}
|
||||
|
||||
func prepareRenderOptions(options []RenderOptions) RenderOptions {
|
||||
var opt RenderOptions
|
||||
if len(options) > 0 {
|
||||
opt = options[0]
|
||||
}
|
||||
|
||||
// Defaults.
|
||||
if len(opt.Directory) == 0 {
|
||||
opt.Directory = "templates"
|
||||
}
|
||||
if len(opt.Extensions) == 0 {
|
||||
opt.Extensions = []string{".tmpl", ".html"}
|
||||
}
|
||||
if len(opt.HTMLContentType) == 0 {
|
||||
opt.HTMLContentType = _CONTENT_HTML
|
||||
}
|
||||
|
||||
return opt
|
||||
}
|
||||
|
||||
func ParseTplSet(tplSet string) (tplName string, tplDir string) {
|
||||
tplSet = strings.TrimSpace(tplSet)
|
||||
if len(tplSet) == 0 {
|
||||
panic("empty template set argument")
|
||||
}
|
||||
infos := strings.Split(tplSet, ":")
|
||||
if len(infos) == 1 {
|
||||
tplDir = infos[0]
|
||||
tplName = path.Base(tplDir)
|
||||
} else {
|
||||
tplName = infos[0]
|
||||
tplDir = infos[1]
|
||||
}
|
||||
|
||||
if !com.IsDir(tplDir) {
|
||||
panic("template set path does not exist or is not a directory")
|
||||
}
|
||||
return tplName, tplDir
|
||||
}
|
||||
|
||||
func renderHandler(opt RenderOptions, tplSets []string) Handler {
|
||||
cs := PrepareCharset(opt.Charset)
|
||||
ts := NewTemplateSet()
|
||||
ts.Set(DEFAULT_TPL_SET_NAME, &opt)
|
||||
|
||||
var tmpOpt RenderOptions
|
||||
for _, tplSet := range tplSets {
|
||||
tplName, tplDir := ParseTplSet(tplSet)
|
||||
tmpOpt = opt
|
||||
tmpOpt.Directory = tplDir
|
||||
ts.Set(tplName, &tmpOpt)
|
||||
}
|
||||
|
||||
return func(ctx *Context) {
|
||||
r := &TplRender{
|
||||
ResponseWriter: ctx.Resp,
|
||||
TemplateSet: ts,
|
||||
Opt: &opt,
|
||||
CompiledCharset: cs,
|
||||
}
|
||||
ctx.Data["TmplLoadTimes"] = func() string {
|
||||
if r.startTime.IsZero() {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprint(time.Since(r.startTime).Nanoseconds()/1e6) + "ms"
|
||||
}
|
||||
|
||||
ctx.Render = r
|
||||
ctx.MapTo(r, (*Render)(nil))
|
||||
}
|
||||
}
|
||||
|
||||
// Renderer is a Middleware that maps a macaron.Render service into the Macaron handler chain.
|
||||
// An single variadic macaron.RenderOptions struct can be optionally provided to configure
|
||||
// HTML rendering. The default directory for templates is "templates" and the default
|
||||
// file extension is ".tmpl" and ".html".
|
||||
//
|
||||
// If MACARON_ENV is set to "" or "development" then templates will be recompiled on every request. For more performance, set the
|
||||
// MACARON_ENV environment variable to "production".
|
||||
func Renderer(options ...RenderOptions) Handler {
|
||||
return renderHandler(prepareRenderOptions(options), []string{})
|
||||
}
|
||||
|
||||
func Renderers(options RenderOptions, tplSets ...string) Handler {
|
||||
return renderHandler(prepareRenderOptions([]RenderOptions{options}), tplSets)
|
||||
}
|
||||
|
||||
type TplRender struct {
|
||||
http.ResponseWriter
|
||||
*TemplateSet
|
||||
Opt *RenderOptions
|
||||
CompiledCharset string
|
||||
|
||||
startTime time.Time
|
||||
}
|
||||
|
||||
func (r *TplRender) SetResponseWriter(rw http.ResponseWriter) {
|
||||
r.ResponseWriter = rw
|
||||
}
|
||||
|
||||
func (r *TplRender) JSON(status int, v interface{}) {
|
||||
var (
|
||||
result []byte
|
||||
err error
|
||||
)
|
||||
if r.Opt.IndentJSON {
|
||||
result, err = json.MarshalIndent(v, "", " ")
|
||||
} else {
|
||||
result, err = json.Marshal(v)
|
||||
}
|
||||
if err != nil {
|
||||
http.Error(r, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
// json rendered fine, write out the result
|
||||
r.Header().Set(_CONTENT_TYPE, _CONTENT_JSON+r.CompiledCharset)
|
||||
r.WriteHeader(status)
|
||||
if len(r.Opt.PrefixJSON) > 0 {
|
||||
_, _ = r.Write(r.Opt.PrefixJSON)
|
||||
}
|
||||
_, _ = r.Write(result)
|
||||
}
|
||||
|
||||
func (r *TplRender) JSONString(v interface{}) (string, error) {
|
||||
var result []byte
|
||||
var err error
|
||||
if r.Opt.IndentJSON {
|
||||
result, err = json.MarshalIndent(v, "", " ")
|
||||
} else {
|
||||
result, err = json.Marshal(v)
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(result), nil
|
||||
}
|
||||
|
||||
func (r *TplRender) XML(status int, v interface{}) {
|
||||
var result []byte
|
||||
var err error
|
||||
if r.Opt.IndentXML {
|
||||
result, err = xml.MarshalIndent(v, "", " ")
|
||||
} else {
|
||||
result, err = xml.Marshal(v)
|
||||
}
|
||||
if err != nil {
|
||||
http.Error(r, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
// XML rendered fine, write out the result
|
||||
r.Header().Set(_CONTENT_TYPE, _CONTENT_XML+r.CompiledCharset)
|
||||
r.WriteHeader(status)
|
||||
if len(r.Opt.PrefixXML) > 0 {
|
||||
_, _ = r.Write(r.Opt.PrefixXML)
|
||||
}
|
||||
_, _ = r.Write(result)
|
||||
}
|
||||
|
||||
func (r *TplRender) data(status int, contentType string, v []byte) {
|
||||
if r.Header().Get(_CONTENT_TYPE) == "" {
|
||||
r.Header().Set(_CONTENT_TYPE, contentType)
|
||||
}
|
||||
r.WriteHeader(status)
|
||||
_, _ = r.Write(v)
|
||||
}
|
||||
|
||||
func (r *TplRender) RawData(status int, v []byte) {
|
||||
r.data(status, _CONTENT_BINARY, v)
|
||||
}
|
||||
|
||||
func (r *TplRender) PlainText(status int, v []byte) {
|
||||
r.data(status, _CONTENT_PLAIN, v)
|
||||
}
|
||||
|
||||
func (r *TplRender) execute(t *template.Template, name string, data interface{}) (*bytes.Buffer, error) {
|
||||
buf := bufpool.Get().(*bytes.Buffer)
|
||||
return buf, t.ExecuteTemplate(buf, name, data)
|
||||
}
|
||||
|
||||
func (r *TplRender) addYield(t *template.Template, tplName string, data interface{}) {
|
||||
funcs := template.FuncMap{
|
||||
"yield": func() (template.HTML, error) {
|
||||
buf, err := r.execute(t, tplName, data)
|
||||
// return safe html here since we are rendering our own template
|
||||
return template.HTML(buf.String()), err
|
||||
},
|
||||
"current": func() (string, error) {
|
||||
return tplName, nil
|
||||
},
|
||||
}
|
||||
t.Funcs(funcs)
|
||||
}
|
||||
|
||||
func (r *TplRender) renderBytes(setName, tplName string, data interface{}, htmlOpt ...HTMLOptions) (*bytes.Buffer, error) {
|
||||
t := r.TemplateSet.Get(setName)
|
||||
if Env == DEV {
|
||||
opt := *r.Opt
|
||||
opt.Directory = r.TemplateSet.GetDir(setName)
|
||||
t = r.TemplateSet.Set(setName, &opt)
|
||||
}
|
||||
if t == nil {
|
||||
return nil, fmt.Errorf("html/template: template \"%s\" is undefined", tplName)
|
||||
}
|
||||
|
||||
opt := r.prepareHTMLOptions(htmlOpt)
|
||||
|
||||
if len(opt.Layout) > 0 {
|
||||
r.addYield(t, tplName, data)
|
||||
tplName = opt.Layout
|
||||
}
|
||||
|
||||
out, err := r.execute(t, tplName, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (r *TplRender) renderHTML(status int, setName, tplName string, data interface{}, htmlOpt ...HTMLOptions) {
|
||||
r.startTime = time.Now()
|
||||
|
||||
out, err := r.renderBytes(setName, tplName, data, htmlOpt...)
|
||||
if err != nil {
|
||||
http.Error(r, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
r.Header().Set(_CONTENT_TYPE, r.Opt.HTMLContentType+r.CompiledCharset)
|
||||
r.WriteHeader(status)
|
||||
|
||||
if _, err := out.WriteTo(r); err != nil {
|
||||
out.Reset()
|
||||
}
|
||||
bufpool.Put(out)
|
||||
}
|
||||
|
||||
func (r *TplRender) HTML(status int, name string, data interface{}, htmlOpt ...HTMLOptions) {
|
||||
r.renderHTML(status, DEFAULT_TPL_SET_NAME, name, data, htmlOpt...)
|
||||
}
|
||||
|
||||
func (r *TplRender) HTMLSet(status int, setName, tplName string, data interface{}, htmlOpt ...HTMLOptions) {
|
||||
r.renderHTML(status, setName, tplName, data, htmlOpt...)
|
||||
}
|
||||
|
||||
func (r *TplRender) HTMLSetBytes(setName, tplName string, data interface{}, htmlOpt ...HTMLOptions) ([]byte, error) {
|
||||
out, err := r.renderBytes(setName, tplName, data, htmlOpt...)
|
||||
if err != nil {
|
||||
return []byte(""), err
|
||||
}
|
||||
return out.Bytes(), nil
|
||||
}
|
||||
|
||||
func (r *TplRender) HTMLBytes(name string, data interface{}, htmlOpt ...HTMLOptions) ([]byte, error) {
|
||||
return r.HTMLSetBytes(DEFAULT_TPL_SET_NAME, name, data, htmlOpt...)
|
||||
}
|
||||
|
||||
func (r *TplRender) HTMLSetString(setName, tplName string, data interface{}, htmlOpt ...HTMLOptions) (string, error) {
|
||||
p, err := r.HTMLSetBytes(setName, tplName, data, htmlOpt...)
|
||||
return string(p), err
|
||||
}
|
||||
|
||||
func (r *TplRender) HTMLString(name string, data interface{}, htmlOpt ...HTMLOptions) (string, error) {
|
||||
p, err := r.HTMLBytes(name, data, htmlOpt...)
|
||||
return string(p), err
|
||||
}
|
||||
|
||||
// Error writes the given HTTP status to the current ResponseWriter
|
||||
func (r *TplRender) Error(status int, message ...string) {
|
||||
r.WriteHeader(status)
|
||||
if len(message) > 0 {
|
||||
_, _ = r.Write([]byte(message[0]))
|
||||
}
|
||||
}
|
||||
|
||||
func (r *TplRender) Status(status int) {
|
||||
r.WriteHeader(status)
|
||||
}
|
||||
|
||||
func (r *TplRender) prepareHTMLOptions(htmlOpt []HTMLOptions) HTMLOptions {
|
||||
if len(htmlOpt) > 0 {
|
||||
return htmlOpt[0]
|
||||
}
|
||||
|
||||
return HTMLOptions{
|
||||
Layout: r.Opt.Layout,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *TplRender) SetTemplatePath(setName, dir string) {
|
||||
if len(setName) == 0 {
|
||||
setName = DEFAULT_TPL_SET_NAME
|
||||
}
|
||||
opt := *r.Opt
|
||||
opt.Directory = dir
|
||||
r.TemplateSet.Set(setName, &opt)
|
||||
}
|
||||
|
||||
func (r *TplRender) HasTemplateSet(name string) bool {
|
||||
return r.TemplateSet.Get(name) != nil
|
||||
}
|
||||
|
||||
// DummyRender is used when user does not choose any real render to use.
|
||||
// This way, we can print out friendly message which asks them to register one,
|
||||
// instead of ugly and confusing 'nil pointer' panic.
|
||||
type DummyRender struct {
|
||||
http.ResponseWriter
|
||||
}
|
||||
|
||||
func renderNotRegistered() {
|
||||
panic("middleware render hasn't been registered")
|
||||
}
|
||||
|
||||
func (r *DummyRender) SetResponseWriter(http.ResponseWriter) {
|
||||
renderNotRegistered()
|
||||
}
|
||||
|
||||
func (r *DummyRender) JSON(int, interface{}) {
|
||||
renderNotRegistered()
|
||||
}
|
||||
|
||||
func (r *DummyRender) JSONString(interface{}) (string, error) {
|
||||
renderNotRegistered()
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (r *DummyRender) RawData(int, []byte) {
|
||||
renderNotRegistered()
|
||||
}
|
||||
|
||||
func (r *DummyRender) PlainText(int, []byte) {
|
||||
renderNotRegistered()
|
||||
}
|
||||
|
||||
func (r *DummyRender) HTML(int, string, interface{}, ...HTMLOptions) {
|
||||
renderNotRegistered()
|
||||
}
|
||||
|
||||
func (r *DummyRender) HTMLSet(int, string, string, interface{}, ...HTMLOptions) {
|
||||
renderNotRegistered()
|
||||
}
|
||||
|
||||
func (r *DummyRender) HTMLSetString(string, string, interface{}, ...HTMLOptions) (string, error) {
|
||||
renderNotRegistered()
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (r *DummyRender) HTMLString(string, interface{}, ...HTMLOptions) (string, error) {
|
||||
renderNotRegistered()
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (r *DummyRender) HTMLSetBytes(string, string, interface{}, ...HTMLOptions) ([]byte, error) {
|
||||
renderNotRegistered()
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *DummyRender) HTMLBytes(string, interface{}, ...HTMLOptions) ([]byte, error) {
|
||||
renderNotRegistered()
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *DummyRender) XML(int, interface{}) {
|
||||
renderNotRegistered()
|
||||
}
|
||||
|
||||
func (r *DummyRender) Error(int, ...string) {
|
||||
renderNotRegistered()
|
||||
}
|
||||
|
||||
func (r *DummyRender) Status(int) {
|
||||
renderNotRegistered()
|
||||
}
|
||||
|
||||
func (r *DummyRender) SetTemplatePath(string, string) {
|
||||
renderNotRegistered()
|
||||
}
|
||||
|
||||
func (r *DummyRender) HasTemplateSet(string) bool {
|
||||
renderNotRegistered()
|
||||
return false
|
||||
}
|
||||
742
pkg/macaron/render_test.go
Executable file
742
pkg/macaron/render_test.go
Executable file
@@ -0,0 +1,742 @@
|
||||
// Copyright 2013 Martini Authors
|
||||
// Copyright 2014 The Macaron Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package macaron
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
type Greeting struct {
|
||||
One string `json:"one"`
|
||||
Two string `json:"two"`
|
||||
}
|
||||
|
||||
type GreetingXML struct {
|
||||
XMLName xml.Name `xml:"greeting"`
|
||||
One string `xml:"one,attr"`
|
||||
Two string `xml:"two,attr"`
|
||||
}
|
||||
|
||||
func Test_Render_JSON(t *testing.T) {
|
||||
Convey("Render JSON", t, func() {
|
||||
m := Classic()
|
||||
m.Use(Renderer())
|
||||
m.Get("/foobar", func(r Render) {
|
||||
r.JSON(300, Greeting{"hello", "world"})
|
||||
})
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "/foobar", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
|
||||
So(resp.Code, ShouldEqual, http.StatusMultipleChoices)
|
||||
So(resp.Header().Get(_CONTENT_TYPE), ShouldEqual, _CONTENT_JSON+"; charset=UTF-8")
|
||||
So(resp.Body.String(), ShouldEqual, `{"one":"hello","two":"world"}`)
|
||||
})
|
||||
|
||||
Convey("Render JSON with prefix", t, func() {
|
||||
m := Classic()
|
||||
prefix := ")]}',\n"
|
||||
m.Use(Renderer(RenderOptions{
|
||||
PrefixJSON: []byte(prefix),
|
||||
}))
|
||||
m.Get("/foobar", func(r Render) {
|
||||
r.JSON(300, Greeting{"hello", "world"})
|
||||
})
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "/foobar", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
|
||||
So(resp.Code, ShouldEqual, http.StatusMultipleChoices)
|
||||
So(resp.Header().Get(_CONTENT_TYPE), ShouldEqual, _CONTENT_JSON+"; charset=UTF-8")
|
||||
So(resp.Body.String(), ShouldEqual, prefix+`{"one":"hello","two":"world"}`)
|
||||
})
|
||||
|
||||
Convey("Render Indented JSON", t, func() {
|
||||
m := Classic()
|
||||
m.Use(Renderer(RenderOptions{
|
||||
IndentJSON: true,
|
||||
}))
|
||||
m.Get("/foobar", func(r Render) {
|
||||
r.JSON(300, Greeting{"hello", "world"})
|
||||
})
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "/foobar", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
|
||||
So(resp.Code, ShouldEqual, http.StatusMultipleChoices)
|
||||
So(resp.Header().Get(_CONTENT_TYPE), ShouldEqual, _CONTENT_JSON+"; charset=UTF-8")
|
||||
So(resp.Body.String(), ShouldEqual, `{
|
||||
"one": "hello",
|
||||
"two": "world"
|
||||
}`)
|
||||
})
|
||||
|
||||
Convey("Render JSON and return string", t, func() {
|
||||
m := Classic()
|
||||
m.Use(Renderer())
|
||||
m.Get("/foobar", func(r Render) {
|
||||
result, err := r.JSONString(Greeting{"hello", "world"})
|
||||
So(err, ShouldBeNil)
|
||||
So(result, ShouldEqual, `{"one":"hello","two":"world"}`)
|
||||
})
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "/foobar", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
})
|
||||
|
||||
Convey("Render with charset JSON", t, func() {
|
||||
m := Classic()
|
||||
m.Use(Renderer(RenderOptions{
|
||||
Charset: "foobar",
|
||||
}))
|
||||
m.Get("/foobar", func(r Render) {
|
||||
r.JSON(300, Greeting{"hello", "world"})
|
||||
})
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "/foobar", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
|
||||
So(resp.Code, ShouldEqual, http.StatusMultipleChoices)
|
||||
So(resp.Header().Get(_CONTENT_TYPE), ShouldEqual, _CONTENT_JSON+"; charset=foobar")
|
||||
So(resp.Body.String(), ShouldEqual, `{"one":"hello","two":"world"}`)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Render_XML(t *testing.T) {
|
||||
Convey("Render XML", t, func() {
|
||||
m := Classic()
|
||||
m.Use(Renderer())
|
||||
m.Get("/foobar", func(r Render) {
|
||||
r.XML(300, GreetingXML{One: "hello", Two: "world"})
|
||||
})
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "/foobar", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
|
||||
So(resp.Code, ShouldEqual, http.StatusMultipleChoices)
|
||||
So(resp.Header().Get(_CONTENT_TYPE), ShouldEqual, _CONTENT_XML+"; charset=UTF-8")
|
||||
So(resp.Body.String(), ShouldEqual, `<greeting one="hello" two="world"></greeting>`)
|
||||
})
|
||||
|
||||
Convey("Render XML with prefix", t, func() {
|
||||
m := Classic()
|
||||
prefix := ")]}',\n"
|
||||
m.Use(Renderer(RenderOptions{
|
||||
PrefixXML: []byte(prefix),
|
||||
}))
|
||||
m.Get("/foobar", func(r Render) {
|
||||
r.XML(300, GreetingXML{One: "hello", Two: "world"})
|
||||
})
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "/foobar", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
|
||||
So(resp.Code, ShouldEqual, http.StatusMultipleChoices)
|
||||
So(resp.Header().Get(_CONTENT_TYPE), ShouldEqual, _CONTENT_XML+"; charset=UTF-8")
|
||||
So(resp.Body.String(), ShouldEqual, prefix+`<greeting one="hello" two="world"></greeting>`)
|
||||
})
|
||||
|
||||
Convey("Render Indented XML", t, func() {
|
||||
m := Classic()
|
||||
m.Use(Renderer(RenderOptions{
|
||||
IndentXML: true,
|
||||
}))
|
||||
m.Get("/foobar", func(r Render) {
|
||||
r.XML(300, GreetingXML{One: "hello", Two: "world"})
|
||||
})
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "/foobar", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
|
||||
So(resp.Code, ShouldEqual, http.StatusMultipleChoices)
|
||||
So(resp.Header().Get(_CONTENT_TYPE), ShouldEqual, _CONTENT_XML+"; charset=UTF-8")
|
||||
So(resp.Body.String(), ShouldEqual, `<greeting one="hello" two="world"></greeting>`)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Render_HTML(t *testing.T) {
|
||||
Convey("Render HTML", t, func() {
|
||||
m := Classic()
|
||||
m.Use(Renderers(RenderOptions{
|
||||
Directory: "fixtures/basic",
|
||||
}, "fixtures/basic2"))
|
||||
m.Get("/foobar", func(r Render) {
|
||||
r.SetResponseWriter(r.(*TplRender).ResponseWriter)
|
||||
r.HTML(200, "hello", "jeremy")
|
||||
r.SetTemplatePath("", "fixtures/basic2")
|
||||
})
|
||||
m.Get("/foobar2", func(r Render) {
|
||||
if r.HasTemplateSet("basic2") {
|
||||
r.HTMLSet(200, "basic2", "hello", "jeremy")
|
||||
}
|
||||
})
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "/foobar", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
|
||||
So(resp.Code, ShouldEqual, http.StatusOK)
|
||||
So(resp.Header().Get(_CONTENT_TYPE), ShouldEqual, _CONTENT_HTML+"; charset=UTF-8")
|
||||
So(resp.Body.String(), ShouldEqual, "<h1>Hello jeremy</h1>")
|
||||
|
||||
resp = httptest.NewRecorder()
|
||||
req, err = http.NewRequest("GET", "/foobar2", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
|
||||
So(resp.Code, ShouldEqual, http.StatusOK)
|
||||
So(resp.Header().Get(_CONTENT_TYPE), ShouldEqual, _CONTENT_HTML+"; charset=UTF-8")
|
||||
So(resp.Body.String(), ShouldEqual, "<h1>What's up, jeremy</h1>")
|
||||
|
||||
Convey("Change render templates path", func() {
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "/foobar", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
|
||||
So(resp.Code, ShouldEqual, http.StatusOK)
|
||||
So(resp.Header().Get(_CONTENT_TYPE), ShouldEqual, _CONTENT_HTML+"; charset=UTF-8")
|
||||
So(resp.Body.String(), ShouldEqual, "<h1>What's up, jeremy</h1>")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Render HTML and return string", t, func() {
|
||||
m := Classic()
|
||||
m.Use(Renderers(RenderOptions{
|
||||
Directory: "fixtures/basic",
|
||||
}, "basic2:fixtures/basic2"))
|
||||
m.Get("/foobar", func(r Render) {
|
||||
result, err := r.HTMLString("hello", "jeremy")
|
||||
So(err, ShouldBeNil)
|
||||
So(result, ShouldEqual, "<h1>Hello jeremy</h1>")
|
||||
})
|
||||
m.Get("/foobar2", func(r Render) {
|
||||
result, err := r.HTMLSetString("basic2", "hello", "jeremy")
|
||||
So(err, ShouldBeNil)
|
||||
So(result, ShouldEqual, "<h1>What's up, jeremy</h1>")
|
||||
})
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "/foobar", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
|
||||
resp = httptest.NewRecorder()
|
||||
req, err = http.NewRequest("GET", "/foobar2", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
})
|
||||
|
||||
Convey("Render with nested HTML", t, func() {
|
||||
m := Classic()
|
||||
m.Use(Renderer(RenderOptions{
|
||||
Directory: "fixtures/basic",
|
||||
}))
|
||||
m.Get("/foobar", func(r Render) {
|
||||
r.HTML(200, "admin/index", "jeremy")
|
||||
})
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "/foobar", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
|
||||
So(resp.Code, ShouldEqual, http.StatusOK)
|
||||
So(resp.Header().Get(_CONTENT_TYPE), ShouldEqual, _CONTENT_HTML+"; charset=UTF-8")
|
||||
So(resp.Body.String(), ShouldEqual, "<h1>Admin jeremy</h1>")
|
||||
})
|
||||
|
||||
Convey("Render bad HTML", t, func() {
|
||||
m := Classic()
|
||||
m.Use(Renderer(RenderOptions{
|
||||
Directory: "fixtures/basic",
|
||||
}))
|
||||
m.Get("/foobar", func(r Render) {
|
||||
r.HTML(200, "nope", nil)
|
||||
})
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "/foobar", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
|
||||
So(resp.Code, ShouldEqual, http.StatusInternalServerError)
|
||||
So(resp.Body.String(), ShouldEqual, "html/template: \"nope\" is undefined\n")
|
||||
})
|
||||
|
||||
Convey("Invalid template set", t, func() {
|
||||
Convey("Empty template set argument", func() {
|
||||
defer func() {
|
||||
So(recover(), ShouldNotBeNil)
|
||||
}()
|
||||
m := Classic()
|
||||
m.Use(Renderers(RenderOptions{
|
||||
Directory: "fixtures/basic",
|
||||
}, ""))
|
||||
})
|
||||
|
||||
Convey("Bad template set path", func() {
|
||||
defer func() {
|
||||
So(recover(), ShouldNotBeNil)
|
||||
}()
|
||||
m := Classic()
|
||||
m.Use(Renderers(RenderOptions{
|
||||
Directory: "fixtures/basic",
|
||||
}, "404"))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Render_XHTML(t *testing.T) {
|
||||
Convey("Render XHTML", t, func() {
|
||||
m := Classic()
|
||||
m.Use(Renderer(RenderOptions{
|
||||
Directory: "fixtures/basic",
|
||||
HTMLContentType: _CONTENT_XHTML,
|
||||
}))
|
||||
m.Get("/foobar", func(r Render) {
|
||||
r.HTML(200, "hello", "jeremy")
|
||||
})
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "/foobar", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
|
||||
So(resp.Code, ShouldEqual, http.StatusOK)
|
||||
So(resp.Header().Get(_CONTENT_TYPE), ShouldEqual, _CONTENT_XHTML+"; charset=UTF-8")
|
||||
So(resp.Body.String(), ShouldEqual, "<h1>Hello jeremy</h1>")
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Render_Extensions(t *testing.T) {
|
||||
Convey("Render with extensions", t, func() {
|
||||
m := Classic()
|
||||
m.Use(Renderer(RenderOptions{
|
||||
Directory: "fixtures/basic",
|
||||
Extensions: []string{".tmpl", ".html"},
|
||||
}))
|
||||
m.Get("/foobar", func(r Render) {
|
||||
r.HTML(200, "hypertext", nil)
|
||||
})
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "/foobar", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
|
||||
So(resp.Code, ShouldEqual, http.StatusOK)
|
||||
So(resp.Header().Get(_CONTENT_TYPE), ShouldEqual, _CONTENT_HTML+"; charset=UTF-8")
|
||||
So(resp.Body.String(), ShouldEqual, "Hypertext!")
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Render_Funcs(t *testing.T) {
|
||||
Convey("Render with functions", t, func() {
|
||||
m := Classic()
|
||||
m.Use(Renderer(RenderOptions{
|
||||
Directory: "fixtures/custom_funcs",
|
||||
Funcs: []template.FuncMap{
|
||||
{
|
||||
"myCustomFunc": func() string {
|
||||
return "My custom function"
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
m.Get("/foobar", func(r Render) {
|
||||
r.HTML(200, "index", "jeremy")
|
||||
})
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "/foobar", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
|
||||
So(resp.Body.String(), ShouldEqual, "My custom function")
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Render_Layout(t *testing.T) {
|
||||
Convey("Render with layout", t, func() {
|
||||
m := Classic()
|
||||
m.Use(Renderer(RenderOptions{
|
||||
Directory: "fixtures/basic",
|
||||
Layout: "layout",
|
||||
}))
|
||||
m.Get("/foobar", func(r Render) {
|
||||
r.HTML(200, "content", "jeremy")
|
||||
})
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "/foobar", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
|
||||
So(resp.Body.String(), ShouldEqual, "head<h1>jeremy</h1>foot")
|
||||
})
|
||||
|
||||
Convey("Render with current layout", t, func() {
|
||||
m := Classic()
|
||||
m.Use(Renderer(RenderOptions{
|
||||
Directory: "fixtures/basic",
|
||||
Layout: "current_layout",
|
||||
}))
|
||||
m.Get("/foobar", func(r Render) {
|
||||
r.HTML(200, "content", "jeremy")
|
||||
})
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "/foobar", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
|
||||
So(resp.Body.String(), ShouldEqual, "content head<h1>jeremy</h1>content foot")
|
||||
})
|
||||
|
||||
Convey("Render with override layout", t, func() {
|
||||
m := Classic()
|
||||
m.Use(Renderer(RenderOptions{
|
||||
Directory: "fixtures/basic",
|
||||
Layout: "layout",
|
||||
}))
|
||||
m.Get("/foobar", func(r Render) {
|
||||
r.HTML(200, "content", "jeremy", HTMLOptions{
|
||||
Layout: "another_layout",
|
||||
})
|
||||
})
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "/foobar", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
|
||||
So(resp.Code, ShouldEqual, http.StatusOK)
|
||||
So(resp.Header().Get(_CONTENT_TYPE), ShouldEqual, _CONTENT_HTML+"; charset=UTF-8")
|
||||
So(resp.Body.String(), ShouldEqual, "another head<h1>jeremy</h1>another foot")
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Render_Delimiters(t *testing.T) {
|
||||
Convey("Render with delimiters", t, func() {
|
||||
m := Classic()
|
||||
m.Use(Renderer(RenderOptions{
|
||||
Delims: Delims{"{[{", "}]}"},
|
||||
Directory: "fixtures/basic",
|
||||
}))
|
||||
m.Get("/foobar", func(r Render) {
|
||||
r.HTML(200, "delims", "jeremy")
|
||||
})
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "/foobar", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
|
||||
So(resp.Code, ShouldEqual, http.StatusOK)
|
||||
So(resp.Header().Get(_CONTENT_TYPE), ShouldEqual, _CONTENT_HTML+"; charset=UTF-8")
|
||||
So(resp.Body.String(), ShouldEqual, "<h1>Hello jeremy</h1>")
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Render_BinaryData(t *testing.T) {
|
||||
Convey("Render binary data", t, func() {
|
||||
m := Classic()
|
||||
m.Use(Renderer())
|
||||
m.Get("/foobar", func(r Render) {
|
||||
r.RawData(200, []byte("hello there"))
|
||||
})
|
||||
m.Get("/foobar2", func(r Render) {
|
||||
r.PlainText(200, []byte("hello there"))
|
||||
})
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "/foobar", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
|
||||
So(resp.Code, ShouldEqual, http.StatusOK)
|
||||
So(resp.Header().Get(_CONTENT_TYPE), ShouldEqual, _CONTENT_BINARY)
|
||||
So(resp.Body.String(), ShouldEqual, "hello there")
|
||||
|
||||
resp = httptest.NewRecorder()
|
||||
req, err = http.NewRequest("GET", "/foobar2", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
|
||||
So(resp.Code, ShouldEqual, http.StatusOK)
|
||||
So(resp.Header().Get(_CONTENT_TYPE), ShouldEqual, _CONTENT_PLAIN)
|
||||
So(resp.Body.String(), ShouldEqual, "hello there")
|
||||
})
|
||||
|
||||
Convey("Render binary data with mime type", t, func() {
|
||||
m := Classic()
|
||||
m.Use(Renderer())
|
||||
m.Get("/foobar", func(r Render) {
|
||||
r.(*TplRender).ResponseWriter.Header().Set(_CONTENT_TYPE, "image/jpeg")
|
||||
r.RawData(200, []byte("..jpeg data.."))
|
||||
})
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "/foobar", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
|
||||
So(resp.Code, ShouldEqual, http.StatusOK)
|
||||
So(resp.Header().Get(_CONTENT_TYPE), ShouldEqual, "image/jpeg")
|
||||
So(resp.Body.String(), ShouldEqual, "..jpeg data..")
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Render_Status(t *testing.T) {
|
||||
Convey("Render with status 204", t, func() {
|
||||
resp := httptest.NewRecorder()
|
||||
r := TplRender{resp, NewTemplateSet(), &RenderOptions{}, "", time.Now()}
|
||||
r.Status(204)
|
||||
So(resp.Code, ShouldEqual, http.StatusNoContent)
|
||||
})
|
||||
|
||||
Convey("Render with status 404", t, func() {
|
||||
resp := httptest.NewRecorder()
|
||||
r := TplRender{resp, NewTemplateSet(), &RenderOptions{}, "", time.Now()}
|
||||
r.Error(404)
|
||||
So(resp.Code, ShouldEqual, http.StatusNotFound)
|
||||
})
|
||||
|
||||
Convey("Render with status 500", t, func() {
|
||||
resp := httptest.NewRecorder()
|
||||
r := TplRender{resp, NewTemplateSet(), &RenderOptions{}, "", time.Now()}
|
||||
r.Error(500)
|
||||
So(resp.Code, ShouldEqual, http.StatusInternalServerError)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Render_NoRace(t *testing.T) {
|
||||
Convey("Make sure render has no race", t, func() {
|
||||
m := Classic()
|
||||
m.Use(Renderer(RenderOptions{
|
||||
Directory: "fixtures/basic",
|
||||
}))
|
||||
m.Get("/foobar", func(r Render) {
|
||||
r.HTML(200, "hello", "world")
|
||||
})
|
||||
|
||||
done := make(chan bool)
|
||||
doreq := func() {
|
||||
resp := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/foobar", nil)
|
||||
m.ServeHTTP(resp, req)
|
||||
done <- true
|
||||
}
|
||||
// Run two requests to check there is no race condition
|
||||
go doreq()
|
||||
go doreq()
|
||||
<-done
|
||||
<-done
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Render_Symlink(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("Skipping testing on Windows")
|
||||
}
|
||||
|
||||
Convey("Render can follow symlinks", t, func() {
|
||||
m := Classic()
|
||||
m.Use(Renderer(RenderOptions{
|
||||
Directory: "fixtures/symlink",
|
||||
}))
|
||||
m.Get("/foobar", func(r Render) {
|
||||
r.HTML(200, "hello", "world")
|
||||
})
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "/foobar", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
So(resp.Code, ShouldEqual, http.StatusOK)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Render_AppendDirectories(t *testing.T) {
|
||||
Convey("Render with additional templates", t, func() {
|
||||
m := Classic()
|
||||
m.Use(Renderer(RenderOptions{
|
||||
Directory: "fixtures/basic",
|
||||
AppendDirectories: []string{"fixtures/basic/custom"},
|
||||
}))
|
||||
|
||||
Convey("Request normal template", func() {
|
||||
m.Get("/normal", func(r Render) {
|
||||
r.HTML(200, "content", "Macaron")
|
||||
})
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "/normal", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
|
||||
So(resp.Body.String(), ShouldEqual, "<h1>Macaron</h1>")
|
||||
So(resp.Code, ShouldEqual, http.StatusOK)
|
||||
})
|
||||
|
||||
Convey("Request overwritten template", func() {
|
||||
m.Get("/custom", func(r Render) {
|
||||
r.HTML(200, "hello", "world")
|
||||
})
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "/custom", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
|
||||
So(resp.Body.String(), ShouldEqual, "<h1>This is custom version of: Hello world</h1>")
|
||||
So(resp.Code, ShouldEqual, http.StatusOK)
|
||||
})
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
func Test_GetExt(t *testing.T) {
|
||||
Convey("Get extension", t, func() {
|
||||
So(GetExt("test"), ShouldBeBlank)
|
||||
So(GetExt("test.tmpl"), ShouldEqual, ".tmpl")
|
||||
So(GetExt("test.go.tmpl"), ShouldEqual, ".go.tmpl")
|
||||
})
|
||||
}
|
||||
|
||||
func Test_dummyRender(t *testing.T) {
|
||||
shouldPanic := func() { So(recover(), ShouldNotBeNil) }
|
||||
|
||||
Convey("Use dummy render to gracefully handle panic", t, func() {
|
||||
m := New()
|
||||
|
||||
performRequest := func(method, path string) {
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest(method, path, nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
}
|
||||
|
||||
m.Get("/set_response_writer", func(ctx *Context) {
|
||||
defer shouldPanic()
|
||||
ctx.SetResponseWriter(nil)
|
||||
})
|
||||
m.Get("/json", func(ctx *Context) {
|
||||
defer shouldPanic()
|
||||
ctx.JSON(0, nil)
|
||||
})
|
||||
m.Get("/jsonstring", func(ctx *Context) {
|
||||
defer shouldPanic()
|
||||
_, _ = ctx.JSONString(nil)
|
||||
})
|
||||
m.Get("/rawdata", func(ctx *Context) {
|
||||
defer shouldPanic()
|
||||
ctx.RawData(0, nil)
|
||||
})
|
||||
m.Get("/plaintext", func(ctx *Context) {
|
||||
defer shouldPanic()
|
||||
ctx.PlainText(0, nil)
|
||||
})
|
||||
m.Get("/html", func(ctx *Context) {
|
||||
defer shouldPanic()
|
||||
ctx.Render.HTML(0, "", nil)
|
||||
})
|
||||
m.Get("/htmlset", func(ctx *Context) {
|
||||
defer shouldPanic()
|
||||
ctx.Render.HTMLSet(0, "", "", nil)
|
||||
})
|
||||
m.Get("/htmlsetstring", func(ctx *Context) {
|
||||
defer shouldPanic()
|
||||
_, _ = ctx.Render.HTMLSetString("", "", nil)
|
||||
})
|
||||
m.Get("/htmlstring", func(ctx *Context) {
|
||||
defer shouldPanic()
|
||||
_, _ = ctx.Render.HTMLString("", nil)
|
||||
})
|
||||
m.Get("/htmlsetbytes", func(ctx *Context) {
|
||||
defer shouldPanic()
|
||||
_, _ = ctx.Render.HTMLSetBytes("", "", nil)
|
||||
})
|
||||
m.Get("/htmlbytes", func(ctx *Context) {
|
||||
defer shouldPanic()
|
||||
_, _ = ctx.Render.HTMLBytes("", nil)
|
||||
})
|
||||
m.Get("/xml", func(ctx *Context) {
|
||||
defer shouldPanic()
|
||||
ctx.XML(0, nil)
|
||||
})
|
||||
m.Get("/error", func(ctx *Context) {
|
||||
defer shouldPanic()
|
||||
ctx.Error(0)
|
||||
})
|
||||
m.Get("/status", func(ctx *Context) {
|
||||
defer shouldPanic()
|
||||
ctx.Status(0)
|
||||
})
|
||||
m.Get("/settemplatepath", func(ctx *Context) {
|
||||
defer shouldPanic()
|
||||
ctx.SetTemplatePath("", "")
|
||||
})
|
||||
m.Get("/hastemplateset", func(ctx *Context) {
|
||||
defer shouldPanic()
|
||||
ctx.HasTemplateSet("")
|
||||
})
|
||||
|
||||
performRequest("GET", "/set_response_writer")
|
||||
performRequest("GET", "/json")
|
||||
performRequest("GET", "/jsonstring")
|
||||
performRequest("GET", "/rawdata")
|
||||
performRequest("GET", "/jsonstring")
|
||||
performRequest("GET", "/plaintext")
|
||||
performRequest("GET", "/html")
|
||||
performRequest("GET", "/htmlset")
|
||||
performRequest("GET", "/htmlsetstring")
|
||||
performRequest("GET", "/htmlstring")
|
||||
performRequest("GET", "/htmlsetbytes")
|
||||
performRequest("GET", "/htmlbytes")
|
||||
performRequest("GET", "/xml")
|
||||
performRequest("GET", "/error")
|
||||
performRequest("GET", "/status")
|
||||
performRequest("GET", "/settemplatepath")
|
||||
performRequest("GET", "/hastemplateset")
|
||||
})
|
||||
}
|
||||
124
pkg/macaron/response_writer.go
Executable file
124
pkg/macaron/response_writer.go
Executable file
@@ -0,0 +1,124 @@
|
||||
// Copyright 2013 Martini Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package macaron
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// ResponseWriter is a wrapper around http.ResponseWriter that provides extra information about
|
||||
// the response. It is recommended that middleware handlers use this construct to wrap a responsewriter
|
||||
// if the functionality calls for it.
|
||||
type ResponseWriter interface {
|
||||
http.ResponseWriter
|
||||
http.Flusher
|
||||
http.Pusher
|
||||
// Status returns the status code of the response or 0 if the response has not been written.
|
||||
Status() int
|
||||
// Written returns whether or not the ResponseWriter has been written.
|
||||
Written() bool
|
||||
// Size returns the size of the response body.
|
||||
Size() int
|
||||
// Before allows for a function to be called before the ResponseWriter has been written to. This is
|
||||
// useful for setting headers or any other operations that must happen before a response has been written.
|
||||
Before(BeforeFunc)
|
||||
}
|
||||
|
||||
// BeforeFunc is a function that is called before the ResponseWriter has been written to.
|
||||
type BeforeFunc func(ResponseWriter)
|
||||
|
||||
// NewResponseWriter creates a ResponseWriter that wraps an http.ResponseWriter
|
||||
func NewResponseWriter(method string, rw http.ResponseWriter) ResponseWriter {
|
||||
return &responseWriter{method, rw, 0, 0, nil}
|
||||
}
|
||||
|
||||
type responseWriter struct {
|
||||
method string
|
||||
http.ResponseWriter
|
||||
status int
|
||||
size int
|
||||
beforeFuncs []BeforeFunc
|
||||
}
|
||||
|
||||
func (rw *responseWriter) WriteHeader(s int) {
|
||||
rw.callBefore()
|
||||
rw.ResponseWriter.WriteHeader(s)
|
||||
rw.status = s
|
||||
}
|
||||
|
||||
func (rw *responseWriter) Write(b []byte) (size int, err error) {
|
||||
if !rw.Written() {
|
||||
// The status will be StatusOK if WriteHeader has not been called yet
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
}
|
||||
if rw.method != "HEAD" {
|
||||
size, err = rw.ResponseWriter.Write(b)
|
||||
rw.size += size
|
||||
}
|
||||
return size, err
|
||||
}
|
||||
|
||||
func (rw *responseWriter) Status() int {
|
||||
return rw.status
|
||||
}
|
||||
|
||||
func (rw *responseWriter) Size() int {
|
||||
return rw.size
|
||||
}
|
||||
|
||||
func (rw *responseWriter) Written() bool {
|
||||
return rw.status != 0
|
||||
}
|
||||
|
||||
func (rw *responseWriter) Before(before BeforeFunc) {
|
||||
rw.beforeFuncs = append(rw.beforeFuncs, before)
|
||||
}
|
||||
|
||||
func (rw *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||
hijacker, ok := rw.ResponseWriter.(http.Hijacker)
|
||||
if !ok {
|
||||
return nil, nil, errors.New("the ResponseWriter doesn't support the Hijacker interface")
|
||||
}
|
||||
return hijacker.Hijack()
|
||||
}
|
||||
|
||||
//nolint
|
||||
func (rw *responseWriter) CloseNotify() <-chan bool {
|
||||
return rw.ResponseWriter.(http.CloseNotifier).CloseNotify()
|
||||
}
|
||||
|
||||
func (rw *responseWriter) callBefore() {
|
||||
for i := len(rw.beforeFuncs) - 1; i >= 0; i-- {
|
||||
rw.beforeFuncs[i](rw)
|
||||
}
|
||||
}
|
||||
|
||||
func (rw *responseWriter) Flush() {
|
||||
flusher, ok := rw.ResponseWriter.(http.Flusher)
|
||||
if ok {
|
||||
flusher.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
func (rw *responseWriter) Push(target string, opts *http.PushOptions) error {
|
||||
pusher, ok := rw.ResponseWriter.(http.Pusher)
|
||||
if !ok {
|
||||
return errors.New("the ResponseWriter doesn't support the Pusher interface")
|
||||
}
|
||||
return pusher.Push(target, opts)
|
||||
}
|
||||
195
pkg/macaron/response_writer_test.go
Executable file
195
pkg/macaron/response_writer_test.go
Executable file
@@ -0,0 +1,195 @@
|
||||
// Copyright 2013 Martini Authors
|
||||
// Copyright 2014 The Macaron Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package macaron
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
type closeNotifyingRecorder struct {
|
||||
*httptest.ResponseRecorder
|
||||
closed chan bool
|
||||
}
|
||||
|
||||
func newCloseNotifyingRecorder() *closeNotifyingRecorder {
|
||||
return &closeNotifyingRecorder{
|
||||
httptest.NewRecorder(),
|
||||
make(chan bool, 1),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *closeNotifyingRecorder) close() {
|
||||
c.closed <- true
|
||||
}
|
||||
|
||||
func (c *closeNotifyingRecorder) CloseNotify() <-chan bool {
|
||||
return c.closed
|
||||
}
|
||||
|
||||
type hijackableResponse struct {
|
||||
Hijacked bool
|
||||
}
|
||||
|
||||
func newHijackableResponse() *hijackableResponse {
|
||||
return &hijackableResponse{}
|
||||
}
|
||||
|
||||
func (h *hijackableResponse) Header() http.Header { return nil }
|
||||
func (h *hijackableResponse) Write(buf []byte) (int, error) { return 0, nil }
|
||||
func (h *hijackableResponse) WriteHeader(code int) {}
|
||||
func (h *hijackableResponse) Flush() {}
|
||||
func (h *hijackableResponse) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||
h.Hijacked = true
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
func Test_ResponseWriter(t *testing.T) {
|
||||
Convey("Write string to response writer", t, func() {
|
||||
resp := httptest.NewRecorder()
|
||||
rw := NewResponseWriter("GET", resp)
|
||||
_, _ = rw.Write([]byte("Hello world"))
|
||||
|
||||
So(resp.Code, ShouldEqual, rw.Status())
|
||||
So(resp.Body.String(), ShouldEqual, "Hello world")
|
||||
So(rw.Status(), ShouldEqual, http.StatusOK)
|
||||
So(rw.Size(), ShouldEqual, 11)
|
||||
So(rw.Written(), ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("Write strings to response writer", t, func() {
|
||||
resp := httptest.NewRecorder()
|
||||
rw := NewResponseWriter("GET", resp)
|
||||
_, _ = rw.Write([]byte("Hello world"))
|
||||
_, _ = rw.Write([]byte("foo bar bat baz"))
|
||||
|
||||
So(resp.Code, ShouldEqual, rw.Status())
|
||||
So(resp.Body.String(), ShouldEqual, "Hello worldfoo bar bat baz")
|
||||
So(rw.Status(), ShouldEqual, http.StatusOK)
|
||||
So(rw.Size(), ShouldEqual, 26)
|
||||
So(rw.Written(), ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("Write header to response writer", t, func() {
|
||||
resp := httptest.NewRecorder()
|
||||
rw := NewResponseWriter("GET", resp)
|
||||
rw.WriteHeader(http.StatusNotFound)
|
||||
|
||||
So(resp.Code, ShouldEqual, rw.Status())
|
||||
So(resp.Body.String(), ShouldBeBlank)
|
||||
So(rw.Status(), ShouldEqual, http.StatusNotFound)
|
||||
So(rw.Size(), ShouldEqual, 0)
|
||||
})
|
||||
|
||||
Convey("Write before response write", t, func() {
|
||||
result := ""
|
||||
resp := httptest.NewRecorder()
|
||||
rw := NewResponseWriter("GET", resp)
|
||||
rw.Before(func(ResponseWriter) {
|
||||
result += "foo"
|
||||
})
|
||||
rw.Before(func(ResponseWriter) {
|
||||
result += "bar"
|
||||
})
|
||||
rw.WriteHeader(http.StatusNotFound)
|
||||
|
||||
So(resp.Code, ShouldEqual, rw.Status())
|
||||
So(resp.Body.String(), ShouldBeBlank)
|
||||
So(rw.Status(), ShouldEqual, http.StatusNotFound)
|
||||
So(rw.Size(), ShouldEqual, 0)
|
||||
So(result, ShouldEqual, "barfoo")
|
||||
})
|
||||
|
||||
Convey("Response writer with Hijack", t, func() {
|
||||
hijackable := newHijackableResponse()
|
||||
rw := NewResponseWriter("GET", hijackable)
|
||||
hijacker, ok := rw.(http.Hijacker)
|
||||
So(ok, ShouldBeTrue)
|
||||
_, _, err := hijacker.Hijack()
|
||||
So(err, ShouldBeNil)
|
||||
So(hijackable.Hijacked, ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("Response writer with bad Hijack", t, func() {
|
||||
hijackable := new(http.ResponseWriter)
|
||||
rw := NewResponseWriter("GET", *hijackable)
|
||||
hijacker, ok := rw.(http.Hijacker)
|
||||
So(ok, ShouldBeTrue)
|
||||
_, _, err := hijacker.Hijack()
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("Response writer with close notify", t, func() {
|
||||
resp := newCloseNotifyingRecorder()
|
||||
rw := NewResponseWriter("GET", resp)
|
||||
closed := false
|
||||
notifier := rw.(http.CloseNotifier).CloseNotify() //nolint
|
||||
resp.close()
|
||||
select {
|
||||
case <-notifier:
|
||||
closed = true
|
||||
case <-time.After(time.Second):
|
||||
}
|
||||
So(closed, ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("Response writer with flusher", t, func() {
|
||||
resp := httptest.NewRecorder()
|
||||
rw := NewResponseWriter("GET", resp)
|
||||
_, ok := rw.(http.Flusher)
|
||||
So(ok, ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("Response writer with flusher handler", t, func() {
|
||||
m := Classic()
|
||||
m.Get("/events", func(w http.ResponseWriter, r *http.Request) {
|
||||
f, ok := w.(http.Flusher)
|
||||
So(ok, ShouldBeTrue)
|
||||
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
w.Header().Set("Connection", "keep-alive")
|
||||
|
||||
for i := 0; i < 2; i++ {
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
_, _ = io.WriteString(w, "data: Hello\n\n")
|
||||
f.Flush()
|
||||
}
|
||||
})
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "/events", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
|
||||
So(resp.Code, ShouldEqual, http.StatusOK)
|
||||
So(resp.Body.String(), ShouldEqual, "data: Hello\n\ndata: Hello\n\n")
|
||||
})
|
||||
|
||||
Convey("Response writer with http/2 push", t, func() {
|
||||
resp := httptest.NewRecorder()
|
||||
rw := NewResponseWriter("GET", resp)
|
||||
_, ok := rw.(http.Pusher)
|
||||
So(ok, ShouldBeTrue)
|
||||
})
|
||||
}
|
||||
76
pkg/macaron/return_handler.go
Executable file
76
pkg/macaron/return_handler.go
Executable file
@@ -0,0 +1,76 @@
|
||||
// Copyright 2013 Martini Authors
|
||||
// Copyright 2014 The Macaron Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package macaron
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"reflect"
|
||||
|
||||
"github.com/go-macaron/inject"
|
||||
)
|
||||
|
||||
// ReturnHandler is a service that Martini provides that is called
|
||||
// when a route handler returns something. The ReturnHandler is
|
||||
// responsible for writing to the ResponseWriter based on the values
|
||||
// that are passed into this function.
|
||||
type ReturnHandler func(*Context, []reflect.Value)
|
||||
|
||||
func canDeref(val reflect.Value) bool {
|
||||
return val.Kind() == reflect.Interface || val.Kind() == reflect.Ptr
|
||||
}
|
||||
|
||||
func isError(val reflect.Value) bool {
|
||||
_, ok := val.Interface().(error)
|
||||
return ok
|
||||
}
|
||||
|
||||
func isByteSlice(val reflect.Value) bool {
|
||||
return val.Kind() == reflect.Slice && val.Type().Elem().Kind() == reflect.Uint8
|
||||
}
|
||||
|
||||
func defaultReturnHandler() ReturnHandler {
|
||||
return func(ctx *Context, vals []reflect.Value) {
|
||||
rv := ctx.GetVal(inject.InterfaceOf((*http.ResponseWriter)(nil)))
|
||||
resp := rv.Interface().(http.ResponseWriter)
|
||||
var respVal reflect.Value
|
||||
if len(vals) > 1 && vals[0].Kind() == reflect.Int {
|
||||
resp.WriteHeader(int(vals[0].Int()))
|
||||
respVal = vals[1]
|
||||
} else if len(vals) > 0 {
|
||||
respVal = vals[0]
|
||||
|
||||
if isError(respVal) {
|
||||
err := respVal.Interface().(error)
|
||||
if err != nil {
|
||||
ctx.internalServerError(ctx, err)
|
||||
}
|
||||
return
|
||||
} else if canDeref(respVal) {
|
||||
if respVal.IsNil() {
|
||||
return // Ignore nil error
|
||||
}
|
||||
}
|
||||
}
|
||||
if canDeref(respVal) {
|
||||
respVal = respVal.Elem()
|
||||
}
|
||||
if isByteSlice(respVal) {
|
||||
_, _ = resp.Write(respVal.Bytes())
|
||||
} else {
|
||||
_, _ = resp.Write([]byte(respVal.String()))
|
||||
}
|
||||
}
|
||||
}
|
||||
127
pkg/macaron/return_handler_test.go
Executable file
127
pkg/macaron/return_handler_test.go
Executable file
@@ -0,0 +1,127 @@
|
||||
// Copyright 2014 The Macaron Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package macaron
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
type r1Invoker func() (int, string)
|
||||
|
||||
func (l r1Invoker) Invoke(p []interface{}) ([]reflect.Value, error) {
|
||||
ret, str := l()
|
||||
return []reflect.Value{reflect.ValueOf(ret), reflect.ValueOf(str)}, nil
|
||||
}
|
||||
|
||||
func Test_Return_Handler(t *testing.T) {
|
||||
Convey("Return with status and body", t, func() {
|
||||
m := New()
|
||||
m.Get("/", func() (int, string) {
|
||||
return 418, "i'm a teapot"
|
||||
})
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "/", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
|
||||
So(resp.Code, ShouldEqual, http.StatusTeapot)
|
||||
So(resp.Body.String(), ShouldEqual, "i'm a teapot")
|
||||
})
|
||||
|
||||
Convey("Return with status and body-FastInvoke", t, func() {
|
||||
m := New()
|
||||
m.Get("/", r1Invoker(func() (int, string) {
|
||||
return 418, "i'm a teapot"
|
||||
}))
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "/", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
|
||||
So(resp.Code, ShouldEqual, http.StatusTeapot)
|
||||
So(resp.Body.String(), ShouldEqual, "i'm a teapot")
|
||||
})
|
||||
|
||||
Convey("Return with error", t, func() {
|
||||
m := New()
|
||||
//revive:disable
|
||||
m.Get("/", func() error {
|
||||
return errors.New("what the hell!!!")
|
||||
})
|
||||
//revive:enable
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "/", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
|
||||
So(resp.Code, ShouldEqual, http.StatusInternalServerError)
|
||||
So(resp.Body.String(), ShouldEqual, "what the hell!!!\n")
|
||||
|
||||
Convey("Return with nil error", func() {
|
||||
m := New()
|
||||
m.Get("/", func() error {
|
||||
return nil
|
||||
}, func() (int, string) {
|
||||
return 200, "Awesome"
|
||||
})
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "/", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
|
||||
So(resp.Code, ShouldEqual, http.StatusOK)
|
||||
So(resp.Body.String(), ShouldEqual, "Awesome")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Return with pointer", t, func() {
|
||||
m := New()
|
||||
m.Get("/", func() *string {
|
||||
str := "hello world"
|
||||
return &str
|
||||
})
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "/", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
|
||||
So(resp.Body.String(), ShouldEqual, "hello world")
|
||||
})
|
||||
|
||||
Convey("Return with byte slice", t, func() {
|
||||
m := New()
|
||||
m.Get("/", func() []byte {
|
||||
return []byte("hello world")
|
||||
})
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "/", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
|
||||
So(resp.Body.String(), ShouldEqual, "hello world")
|
||||
})
|
||||
}
|
||||
382
pkg/macaron/router.go
Executable file
382
pkg/macaron/router.go
Executable file
@@ -0,0 +1,382 @@
|
||||
// Copyright 2014 The Macaron Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package macaron
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
// Known HTTP methods.
|
||||
_HTTP_METHODS = map[string]bool{
|
||||
"GET": true,
|
||||
"POST": true,
|
||||
"PUT": true,
|
||||
"DELETE": true,
|
||||
"PATCH": true,
|
||||
"OPTIONS": true,
|
||||
"HEAD": true,
|
||||
}
|
||||
)
|
||||
|
||||
// routeMap represents a thread-safe map for route tree.
|
||||
type routeMap struct {
|
||||
lock sync.RWMutex
|
||||
routes map[string]map[string]*Leaf
|
||||
}
|
||||
|
||||
// NewRouteMap initializes and returns a new routeMap.
|
||||
func NewRouteMap() *routeMap {
|
||||
rm := &routeMap{
|
||||
routes: make(map[string]map[string]*Leaf),
|
||||
}
|
||||
for m := range _HTTP_METHODS {
|
||||
rm.routes[m] = make(map[string]*Leaf)
|
||||
}
|
||||
return rm
|
||||
}
|
||||
|
||||
// getLeaf returns Leaf object if a route has been registered.
|
||||
func (rm *routeMap) getLeaf(method, pattern string) *Leaf {
|
||||
rm.lock.RLock()
|
||||
defer rm.lock.RUnlock()
|
||||
|
||||
return rm.routes[method][pattern]
|
||||
}
|
||||
|
||||
// add adds new route to route tree map.
|
||||
func (rm *routeMap) add(method, pattern string, leaf *Leaf) {
|
||||
rm.lock.Lock()
|
||||
defer rm.lock.Unlock()
|
||||
|
||||
rm.routes[method][pattern] = leaf
|
||||
}
|
||||
|
||||
type group struct {
|
||||
pattern string
|
||||
handlers []Handler
|
||||
}
|
||||
|
||||
// Router represents a Macaron router layer.
|
||||
type Router struct {
|
||||
m *Macaron
|
||||
autoHead bool
|
||||
routers map[string]*Tree
|
||||
*routeMap
|
||||
namedRoutes map[string]*Leaf
|
||||
|
||||
groups []group
|
||||
notFound http.HandlerFunc
|
||||
internalServerError func(*Context, error)
|
||||
|
||||
// handlerWrapper is used to wrap arbitrary function from Handler to inject.FastInvoker.
|
||||
handlerWrapper func(Handler) Handler
|
||||
}
|
||||
|
||||
func NewRouter() *Router {
|
||||
return &Router{
|
||||
routers: make(map[string]*Tree),
|
||||
routeMap: NewRouteMap(),
|
||||
namedRoutes: make(map[string]*Leaf),
|
||||
}
|
||||
}
|
||||
|
||||
// SetAutoHead sets the value who determines whether add HEAD method automatically
|
||||
// when GET method is added.
|
||||
func (r *Router) SetAutoHead(v bool) {
|
||||
r.autoHead = v
|
||||
}
|
||||
|
||||
type Params map[string]string
|
||||
|
||||
// Handle is a function that can be registered to a route to handle HTTP requests.
|
||||
// Like http.HandlerFunc, but has a third parameter for the values of wildcards (variables).
|
||||
type Handle func(http.ResponseWriter, *http.Request, Params)
|
||||
|
||||
// Route represents a wrapper of leaf route and upper level router.
|
||||
type Route struct {
|
||||
router *Router
|
||||
leaf *Leaf
|
||||
}
|
||||
|
||||
// Name sets name of route.
|
||||
func (r *Route) Name(name string) {
|
||||
if len(name) == 0 {
|
||||
panic("route name cannot be empty")
|
||||
} else if r.router.namedRoutes[name] != nil {
|
||||
panic("route with given name already exists: " + name)
|
||||
}
|
||||
r.router.namedRoutes[name] = r.leaf
|
||||
}
|
||||
|
||||
// handle adds new route to the router tree.
|
||||
func (r *Router) handle(method, pattern string, handle Handle) *Route {
|
||||
method = strings.ToUpper(method)
|
||||
|
||||
var leaf *Leaf
|
||||
// Prevent duplicate routes.
|
||||
if leaf = r.getLeaf(method, pattern); leaf != nil {
|
||||
return &Route{r, leaf}
|
||||
}
|
||||
|
||||
// Validate HTTP methods.
|
||||
if !_HTTP_METHODS[method] && method != "*" {
|
||||
panic("unknown HTTP method: " + method)
|
||||
}
|
||||
|
||||
// Generate methods need register.
|
||||
methods := make(map[string]bool)
|
||||
if method == "*" {
|
||||
for m := range _HTTP_METHODS {
|
||||
methods[m] = true
|
||||
}
|
||||
} else {
|
||||
methods[method] = true
|
||||
}
|
||||
|
||||
// Add to router tree.
|
||||
for m := range methods {
|
||||
if t, ok := r.routers[m]; ok {
|
||||
leaf = t.Add(pattern, handle)
|
||||
} else {
|
||||
t := NewTree()
|
||||
leaf = t.Add(pattern, handle)
|
||||
r.routers[m] = t
|
||||
}
|
||||
r.add(m, pattern, leaf)
|
||||
}
|
||||
return &Route{r, leaf}
|
||||
}
|
||||
|
||||
// Handle registers a new request handle with the given pattern, method and handlers.
|
||||
func (r *Router) Handle(method string, pattern string, handlers []Handler) *Route {
|
||||
if len(r.groups) > 0 {
|
||||
groupPattern := ""
|
||||
h := make([]Handler, 0)
|
||||
for _, g := range r.groups {
|
||||
groupPattern += g.pattern
|
||||
h = append(h, g.handlers...)
|
||||
}
|
||||
|
||||
pattern = groupPattern + pattern
|
||||
h = append(h, handlers...)
|
||||
handlers = h
|
||||
}
|
||||
handlers = validateAndWrapHandlers(handlers, r.handlerWrapper)
|
||||
|
||||
return r.handle(method, pattern, func(resp http.ResponseWriter, req *http.Request, params Params) {
|
||||
c := r.m.createContext(resp, req)
|
||||
c.params = params
|
||||
c.handlers = make([]Handler, 0, len(r.m.handlers)+len(handlers))
|
||||
c.handlers = append(c.handlers, r.m.handlers...)
|
||||
c.handlers = append(c.handlers, handlers...)
|
||||
c.run()
|
||||
})
|
||||
}
|
||||
|
||||
func (r *Router) Group(pattern string, fn func(), h ...Handler) {
|
||||
r.groups = append(r.groups, group{pattern, h})
|
||||
fn()
|
||||
r.groups = r.groups[:len(r.groups)-1]
|
||||
}
|
||||
|
||||
// Get is a shortcut for r.Handle("GET", pattern, handlers)
|
||||
func (r *Router) Get(pattern string, h ...Handler) (leaf *Route) {
|
||||
leaf = r.Handle("GET", pattern, h)
|
||||
if r.autoHead {
|
||||
r.Head(pattern, h...)
|
||||
}
|
||||
return leaf
|
||||
}
|
||||
|
||||
// Patch is a shortcut for r.Handle("PATCH", pattern, handlers)
|
||||
func (r *Router) Patch(pattern string, h ...Handler) *Route {
|
||||
return r.Handle("PATCH", pattern, h)
|
||||
}
|
||||
|
||||
// Post is a shortcut for r.Handle("POST", pattern, handlers)
|
||||
func (r *Router) Post(pattern string, h ...Handler) *Route {
|
||||
return r.Handle("POST", pattern, h)
|
||||
}
|
||||
|
||||
// Put is a shortcut for r.Handle("PUT", pattern, handlers)
|
||||
func (r *Router) Put(pattern string, h ...Handler) *Route {
|
||||
return r.Handle("PUT", pattern, h)
|
||||
}
|
||||
|
||||
// Delete is a shortcut for r.Handle("DELETE", pattern, handlers)
|
||||
func (r *Router) Delete(pattern string, h ...Handler) *Route {
|
||||
return r.Handle("DELETE", pattern, h)
|
||||
}
|
||||
|
||||
// Options is a shortcut for r.Handle("OPTIONS", pattern, handlers)
|
||||
func (r *Router) Options(pattern string, h ...Handler) *Route {
|
||||
return r.Handle("OPTIONS", pattern, h)
|
||||
}
|
||||
|
||||
// Head is a shortcut for r.Handle("HEAD", pattern, handlers)
|
||||
func (r *Router) Head(pattern string, h ...Handler) *Route {
|
||||
return r.Handle("HEAD", pattern, h)
|
||||
}
|
||||
|
||||
// Any is a shortcut for r.Handle("*", pattern, handlers)
|
||||
func (r *Router) Any(pattern string, h ...Handler) *Route {
|
||||
return r.Handle("*", pattern, h)
|
||||
}
|
||||
|
||||
// Route is a shortcut for same handlers but different HTTP methods.
|
||||
//
|
||||
// Example:
|
||||
// m.Route("/", "GET,POST", h)
|
||||
func (r *Router) Route(pattern, methods string, h ...Handler) (route *Route) {
|
||||
for _, m := range strings.Split(methods, ",") {
|
||||
route = r.Handle(strings.TrimSpace(m), pattern, h)
|
||||
}
|
||||
return route
|
||||
}
|
||||
|
||||
// Combo returns a combo router.
|
||||
func (r *Router) Combo(pattern string, h ...Handler) *ComboRouter {
|
||||
return &ComboRouter{r, pattern, h, map[string]bool{}, nil}
|
||||
}
|
||||
|
||||
// NotFound configurates http.HandlerFunc which is called when no matching route is
|
||||
// found. If it is not set, http.NotFound is used.
|
||||
// Be sure to set 404 response code in your handler.
|
||||
func (r *Router) NotFound(handlers ...Handler) {
|
||||
handlers = validateAndWrapHandlers(handlers)
|
||||
r.notFound = func(rw http.ResponseWriter, req *http.Request) {
|
||||
c := r.m.createContext(rw, req)
|
||||
c.handlers = make([]Handler, 0, len(r.m.handlers)+len(handlers))
|
||||
c.handlers = append(c.handlers, r.m.handlers...)
|
||||
c.handlers = append(c.handlers, handlers...)
|
||||
c.run()
|
||||
}
|
||||
}
|
||||
|
||||
// InternalServerError configurates handler which is called when route handler returns
|
||||
// error. If it is not set, default handler is used.
|
||||
// Be sure to set 500 response code in your handler.
|
||||
func (r *Router) InternalServerError(handlers ...Handler) {
|
||||
handlers = validateAndWrapHandlers(handlers)
|
||||
r.internalServerError = func(c *Context, err error) {
|
||||
c.index = 0
|
||||
c.handlers = handlers
|
||||
c.Map(err)
|
||||
c.run()
|
||||
}
|
||||
}
|
||||
|
||||
// SetHandlerWrapper sets handlerWrapper for the router.
|
||||
func (r *Router) SetHandlerWrapper(f func(Handler) Handler) {
|
||||
r.handlerWrapper = f
|
||||
}
|
||||
|
||||
func (r *Router) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
if t, ok := r.routers[req.Method]; ok {
|
||||
if !strings.ContainsAny(req.URL.Path, ":*") {
|
||||
// Fast match for static routes
|
||||
leaf := r.getLeaf(req.Method, req.URL.Path)
|
||||
if leaf != nil {
|
||||
leaf.handle(rw, req, nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
h, p, ok := t.Match(req.URL.EscapedPath())
|
||||
if ok {
|
||||
if splat, ok := p["*0"]; ok {
|
||||
p["*"] = splat // Easy name.
|
||||
}
|
||||
h(rw, req, p)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
r.notFound(rw, req)
|
||||
}
|
||||
|
||||
// URLFor builds path part of URL by given pair values.
|
||||
func (r *Router) URLFor(name string, pairs ...string) string {
|
||||
leaf, ok := r.namedRoutes[name]
|
||||
if !ok {
|
||||
panic("route with given name does not exists: " + name)
|
||||
}
|
||||
return leaf.URLPath(pairs...)
|
||||
}
|
||||
|
||||
// ComboRouter represents a combo router.
|
||||
type ComboRouter struct {
|
||||
router *Router
|
||||
pattern string
|
||||
handlers []Handler
|
||||
methods map[string]bool // Registered methods.
|
||||
|
||||
lastRoute *Route
|
||||
}
|
||||
|
||||
func (cr *ComboRouter) checkMethod(name string) {
|
||||
if cr.methods[name] {
|
||||
panic("method '" + name + "' has already been registered")
|
||||
}
|
||||
cr.methods[name] = true
|
||||
}
|
||||
|
||||
func (cr *ComboRouter) route(fn func(string, ...Handler) *Route, method string, h ...Handler) *ComboRouter {
|
||||
cr.checkMethod(method)
|
||||
cr.lastRoute = fn(cr.pattern, append(cr.handlers, h...)...)
|
||||
return cr
|
||||
}
|
||||
|
||||
func (cr *ComboRouter) Get(h ...Handler) *ComboRouter {
|
||||
if cr.router.autoHead {
|
||||
cr.Head(h...)
|
||||
}
|
||||
return cr.route(cr.router.Get, "GET", h...)
|
||||
}
|
||||
|
||||
func (cr *ComboRouter) Patch(h ...Handler) *ComboRouter {
|
||||
return cr.route(cr.router.Patch, "PATCH", h...)
|
||||
}
|
||||
|
||||
func (cr *ComboRouter) Post(h ...Handler) *ComboRouter {
|
||||
return cr.route(cr.router.Post, "POST", h...)
|
||||
}
|
||||
|
||||
func (cr *ComboRouter) Put(h ...Handler) *ComboRouter {
|
||||
return cr.route(cr.router.Put, "PUT", h...)
|
||||
}
|
||||
|
||||
func (cr *ComboRouter) Delete(h ...Handler) *ComboRouter {
|
||||
return cr.route(cr.router.Delete, "DELETE", h...)
|
||||
}
|
||||
|
||||
func (cr *ComboRouter) Options(h ...Handler) *ComboRouter {
|
||||
return cr.route(cr.router.Options, "OPTIONS", h...)
|
||||
}
|
||||
|
||||
func (cr *ComboRouter) Head(h ...Handler) *ComboRouter {
|
||||
return cr.route(cr.router.Head, "HEAD", h...)
|
||||
}
|
||||
|
||||
// Name sets name of ComboRouter route.
|
||||
func (cr *ComboRouter) Name(name string) {
|
||||
if cr.lastRoute == nil {
|
||||
panic("no corresponding route to be named")
|
||||
}
|
||||
cr.lastRoute.Name(name)
|
||||
}
|
||||
347
pkg/macaron/router_test.go
Executable file
347
pkg/macaron/router_test.go
Executable file
@@ -0,0 +1,347 @@
|
||||
// Copyright 2014 The Macaron Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package macaron
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func Test_Router_Handle(t *testing.T) {
|
||||
test_Router_Handle(t, false)
|
||||
}
|
||||
func Test_Router_FastInvoker_Handle(t *testing.T) {
|
||||
test_Router_Handle(t, true)
|
||||
}
|
||||
|
||||
// handlerFunc0Invoker func()string Invoker Handler
|
||||
type handlerFunc0Invoker func() string
|
||||
|
||||
// Invoke handlerFunc0Invoker
|
||||
func (l handlerFunc0Invoker) Invoke(p []interface{}) ([]reflect.Value, error) {
|
||||
ret := l()
|
||||
return []reflect.Value{reflect.ValueOf(ret)}, nil
|
||||
}
|
||||
|
||||
func test_Router_Handle(t *testing.T, isFast bool) {
|
||||
Convey("Register all HTTP methods routes", t, func() {
|
||||
m := New()
|
||||
|
||||
if isFast {
|
||||
// FastInvoker Handler Wrap Action
|
||||
m.Router.SetHandlerWrapper(func(h Handler) Handler {
|
||||
switch v := h.(type) {
|
||||
case func() string:
|
||||
return handlerFunc0Invoker(v)
|
||||
}
|
||||
return h
|
||||
})
|
||||
}
|
||||
|
||||
m.Get("/get", func() string {
|
||||
return "GET"
|
||||
})
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "/get", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
So(resp.Body.String(), ShouldEqual, "GET")
|
||||
|
||||
m.Patch("/patch", func() string {
|
||||
return "PATCH"
|
||||
})
|
||||
resp = httptest.NewRecorder()
|
||||
req, err = http.NewRequest("PATCH", "/patch", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
So(resp.Body.String(), ShouldEqual, "PATCH")
|
||||
|
||||
m.Post("/post", func() string {
|
||||
return "POST"
|
||||
})
|
||||
resp = httptest.NewRecorder()
|
||||
req, err = http.NewRequest("POST", "/post", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
So(resp.Body.String(), ShouldEqual, "POST")
|
||||
|
||||
m.Put("/put", func() string {
|
||||
return "PUT"
|
||||
})
|
||||
resp = httptest.NewRecorder()
|
||||
req, err = http.NewRequest("PUT", "/put", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
So(resp.Body.String(), ShouldEqual, "PUT")
|
||||
|
||||
m.Delete("/delete", func() string {
|
||||
return "DELETE"
|
||||
})
|
||||
resp = httptest.NewRecorder()
|
||||
req, err = http.NewRequest("DELETE", "/delete", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
So(resp.Body.String(), ShouldEqual, "DELETE")
|
||||
|
||||
m.Options("/options", func() string {
|
||||
return "OPTIONS"
|
||||
})
|
||||
resp = httptest.NewRecorder()
|
||||
req, err = http.NewRequest("OPTIONS", "/options", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
So(resp.Body.String(), ShouldEqual, "OPTIONS")
|
||||
|
||||
m.Head("/head", func() string {
|
||||
return "HEAD"
|
||||
})
|
||||
resp = httptest.NewRecorder()
|
||||
req, err = http.NewRequest("HEAD", "/head", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
So(resp.Body.String(), ShouldHaveLength, 0)
|
||||
|
||||
m.Any("/any", func() string {
|
||||
return "ANY"
|
||||
})
|
||||
resp = httptest.NewRecorder()
|
||||
req, err = http.NewRequest("GET", "/any", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
So(resp.Body.String(), ShouldEqual, "ANY")
|
||||
|
||||
m.Route("/route", "GET,POST", func() string {
|
||||
return "ROUTE"
|
||||
})
|
||||
resp = httptest.NewRecorder()
|
||||
req, err = http.NewRequest("POST", "/route", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
So(resp.Body.String(), ShouldEqual, "ROUTE")
|
||||
|
||||
if isFast {
|
||||
//remove Handler Wrap Action
|
||||
m.Router.SetHandlerWrapper(nil)
|
||||
}
|
||||
})
|
||||
|
||||
Convey("Register with or without auto head", t, func() {
|
||||
Convey("Without auto head", func() {
|
||||
m := New()
|
||||
m.Get("/", func() string {
|
||||
return "GET"
|
||||
})
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("HEAD", "/", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
So(resp.Code, ShouldEqual, 404)
|
||||
})
|
||||
|
||||
Convey("With auto head", func() {
|
||||
m := New()
|
||||
m.SetAutoHead(true)
|
||||
m.Get("/", func() string {
|
||||
return "GET"
|
||||
})
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("HEAD", "/", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
So(resp.Code, ShouldEqual, 200)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Register all HTTP methods routes with combo", t, func() {
|
||||
m := New()
|
||||
m.SetURLPrefix("/prefix")
|
||||
m.Use(Renderer())
|
||||
m.Combo("/", func(ctx *Context) {
|
||||
ctx.Data["prefix"] = "Prefix_"
|
||||
}).
|
||||
Get(func(ctx *Context) string { return ctx.Data["prefix"].(string) + "GET" }).
|
||||
Patch(func(ctx *Context) string { return ctx.Data["prefix"].(string) + "PATCH" }).
|
||||
Post(func(ctx *Context) string { return ctx.Data["prefix"].(string) + "POST" }).
|
||||
Put(func(ctx *Context) string { return ctx.Data["prefix"].(string) + "PUT" }).
|
||||
Delete(func(ctx *Context) string { return ctx.Data["prefix"].(string) + "DELETE" }).
|
||||
Options(func(ctx *Context) string { return ctx.Data["prefix"].(string) + "OPTIONS" }).
|
||||
Head(func(ctx *Context) string { return ctx.Data["prefix"].(string) + "HEAD" })
|
||||
|
||||
for name := range _HTTP_METHODS {
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest(name, "/", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
if name == "HEAD" {
|
||||
So(resp.Body.String(), ShouldHaveLength, 0)
|
||||
} else {
|
||||
So(resp.Body.String(), ShouldEqual, "Prefix_"+name)
|
||||
}
|
||||
}
|
||||
|
||||
defer func() {
|
||||
So(recover(), ShouldNotBeNil)
|
||||
}()
|
||||
m.Combo("/").Get(func() {}).Get(nil)
|
||||
})
|
||||
|
||||
Convey("Register duplicated routes", t, func() {
|
||||
r := NewRouter()
|
||||
r.Get("/")
|
||||
r.Get("/")
|
||||
})
|
||||
|
||||
Convey("Register invalid HTTP method", t, func() {
|
||||
defer func() {
|
||||
So(recover(), ShouldNotBeNil)
|
||||
}()
|
||||
r := NewRouter()
|
||||
r.Handle("404", "/", nil)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Route_Name(t *testing.T) {
|
||||
Convey("Set route name", t, func() {
|
||||
m := New()
|
||||
m.Get("/", func() {}).Name("home")
|
||||
|
||||
defer func() {
|
||||
So(recover(), ShouldNotBeNil)
|
||||
}()
|
||||
m.Get("/", func() {}).Name("home")
|
||||
})
|
||||
|
||||
Convey("Set combo router name", t, func() {
|
||||
m := New()
|
||||
m.Combo("/").Get(func() {}).Name("home")
|
||||
|
||||
defer func() {
|
||||
So(recover(), ShouldNotBeNil)
|
||||
}()
|
||||
m.Combo("/").Name("home")
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Router_URLFor(t *testing.T) {
|
||||
Convey("Build URL path", t, func() {
|
||||
m := New()
|
||||
m.Get("/user/:id", func() {}).Name("user_id")
|
||||
m.Get("/user/:id/:name", func() {}).Name("user_id_name")
|
||||
m.Get("cms_:id_:page.html", func() {}).Name("id_page")
|
||||
|
||||
So(m.URLFor("user_id", "id", "12"), ShouldEqual, "/user/12")
|
||||
So(m.URLFor("user_id_name", "id", "12", "name", "unknwon"), ShouldEqual, "/user/12/unknwon")
|
||||
So(m.URLFor("id_page", "id", "12", "page", "profile"), ShouldEqual, "/cms_12_profile.html")
|
||||
|
||||
Convey("Number of pair values does not match", func() {
|
||||
defer func() {
|
||||
So(recover(), ShouldNotBeNil)
|
||||
}()
|
||||
m.URLFor("user_id", "id")
|
||||
})
|
||||
|
||||
Convey("Empty pair value", func() {
|
||||
defer func() {
|
||||
So(recover(), ShouldNotBeNil)
|
||||
}()
|
||||
m.URLFor("user_id", "", "")
|
||||
})
|
||||
|
||||
Convey("Empty route name", func() {
|
||||
defer func() {
|
||||
So(recover(), ShouldNotBeNil)
|
||||
}()
|
||||
m.Get("/user/:id", func() {}).Name("")
|
||||
})
|
||||
|
||||
Convey("Invalid route name", func() {
|
||||
defer func() {
|
||||
So(recover(), ShouldNotBeNil)
|
||||
}()
|
||||
m.URLFor("404")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Router_Group(t *testing.T) {
|
||||
Convey("Register route group", t, func() {
|
||||
m := New()
|
||||
m.Group("/api", func() {
|
||||
m.Group("/v1", func() {
|
||||
m.Get("/list", func() string {
|
||||
return "Well done!"
|
||||
})
|
||||
})
|
||||
})
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "/api/v1/list", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
So(resp.Body.String(), ShouldEqual, "Well done!")
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Router_NotFound(t *testing.T) {
|
||||
Convey("Custom not found handler", t, func() {
|
||||
m := New()
|
||||
m.Get("/", func() {})
|
||||
m.NotFound(func() string {
|
||||
return "Custom not found"
|
||||
})
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "/404", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
So(resp.Body.String(), ShouldEqual, "Custom not found")
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Router_InternalServerError(t *testing.T) {
|
||||
Convey("Custom internal server error handler", t, func() {
|
||||
m := New()
|
||||
m.Get("/", func() error {
|
||||
return errors.New("Custom internal server error")
|
||||
})
|
||||
m.InternalServerError(func(rw http.ResponseWriter, err error) {
|
||||
rw.WriteHeader(500)
|
||||
_, _ = rw.Write([]byte(err.Error()))
|
||||
})
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "/", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
So(resp.Code, ShouldEqual, 500)
|
||||
So(resp.Body.String(), ShouldEqual, "Custom internal server error")
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Router_splat(t *testing.T) {
|
||||
Convey("Register router with glob", t, func() {
|
||||
m := New()
|
||||
m.Get("/*", func(ctx *Context) string {
|
||||
return ctx.Params("*")
|
||||
})
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "/hahaha", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
So(resp.Body.String(), ShouldEqual, "hahaha")
|
||||
})
|
||||
}
|
||||
231
pkg/macaron/static.go
Executable file
231
pkg/macaron/static.go
Executable file
@@ -0,0 +1,231 @@
|
||||
// Copyright 2013 Martini Authors
|
||||
// Copyright 2014 The Macaron Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package macaron
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// StaticOptions is a struct for specifying configuration options for the macaron.Static middleware.
|
||||
type StaticOptions struct {
|
||||
// Prefix is the optional prefix used to serve the static directory content
|
||||
Prefix string
|
||||
// SkipLogging will disable [Static] log messages when a static file is served.
|
||||
SkipLogging bool
|
||||
// IndexFile defines which file to serve as index if it exists.
|
||||
IndexFile string
|
||||
// Expires defines which user-defined function to use for producing a HTTP Expires Header
|
||||
// https://developers.google.com/speed/docs/insights/LeverageBrowserCaching
|
||||
Expires func() string
|
||||
// ETag defines if we should add an ETag header
|
||||
// https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching#validating-cached-responses-with-etags
|
||||
ETag bool
|
||||
// FileSystem is the interface for supporting any implmentation of file system.
|
||||
FileSystem http.FileSystem
|
||||
}
|
||||
|
||||
// FIXME: to be deleted.
|
||||
type staticMap struct {
|
||||
lock sync.RWMutex
|
||||
data map[string]*http.Dir
|
||||
}
|
||||
|
||||
func (sm *staticMap) Set(dir *http.Dir) {
|
||||
sm.lock.Lock()
|
||||
defer sm.lock.Unlock()
|
||||
|
||||
sm.data[string(*dir)] = dir
|
||||
}
|
||||
|
||||
func (sm *staticMap) Get(name string) *http.Dir {
|
||||
sm.lock.RLock()
|
||||
defer sm.lock.RUnlock()
|
||||
|
||||
return sm.data[name]
|
||||
}
|
||||
|
||||
func (sm *staticMap) Delete(name string) {
|
||||
sm.lock.Lock()
|
||||
defer sm.lock.Unlock()
|
||||
|
||||
delete(sm.data, name)
|
||||
}
|
||||
|
||||
var statics = staticMap{sync.RWMutex{}, map[string]*http.Dir{}}
|
||||
|
||||
// staticFileSystem implements http.FileSystem interface.
|
||||
type staticFileSystem struct {
|
||||
dir *http.Dir
|
||||
}
|
||||
|
||||
func newStaticFileSystem(directory string) staticFileSystem {
|
||||
if !filepath.IsAbs(directory) {
|
||||
directory = filepath.Join(Root, directory)
|
||||
}
|
||||
dir := http.Dir(directory)
|
||||
statics.Set(&dir)
|
||||
return staticFileSystem{&dir}
|
||||
}
|
||||
|
||||
func (fs staticFileSystem) Open(name string) (http.File, error) {
|
||||
return fs.dir.Open(name)
|
||||
}
|
||||
|
||||
func prepareStaticOption(dir string, opt StaticOptions) StaticOptions {
|
||||
// Defaults
|
||||
if len(opt.IndexFile) == 0 {
|
||||
opt.IndexFile = "index.html"
|
||||
}
|
||||
// Normalize the prefix if provided
|
||||
if opt.Prefix != "" {
|
||||
// Ensure we have a leading '/'
|
||||
if opt.Prefix[0] != '/' {
|
||||
opt.Prefix = "/" + opt.Prefix
|
||||
}
|
||||
// Remove any trailing '/'
|
||||
opt.Prefix = strings.TrimRight(opt.Prefix, "/")
|
||||
}
|
||||
if opt.FileSystem == nil {
|
||||
opt.FileSystem = newStaticFileSystem(dir)
|
||||
}
|
||||
return opt
|
||||
}
|
||||
|
||||
func prepareStaticOptions(dir string, options []StaticOptions) StaticOptions {
|
||||
var opt StaticOptions
|
||||
if len(options) > 0 {
|
||||
opt = options[0]
|
||||
}
|
||||
return prepareStaticOption(dir, opt)
|
||||
}
|
||||
|
||||
func staticHandler(ctx *Context, log *log.Logger, opt StaticOptions) bool {
|
||||
if ctx.Req.Method != "GET" && ctx.Req.Method != "HEAD" {
|
||||
return false
|
||||
}
|
||||
|
||||
file := ctx.Req.URL.Path
|
||||
// if we have a prefix, filter requests by stripping the prefix
|
||||
if opt.Prefix != "" {
|
||||
if !strings.HasPrefix(file, opt.Prefix) {
|
||||
return false
|
||||
}
|
||||
file = file[len(opt.Prefix):]
|
||||
if file != "" && file[0] != '/' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
f, err := opt.FileSystem.Open(file)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
fi, err := f.Stat()
|
||||
if err != nil {
|
||||
return true // File exists but fail to open.
|
||||
}
|
||||
|
||||
// Try to serve index file
|
||||
if fi.IsDir() {
|
||||
redirPath := path.Clean(ctx.Req.URL.Path)
|
||||
// path.Clean removes the trailing slash, so we need to add it back when
|
||||
// the original path has it.
|
||||
if strings.HasSuffix(ctx.Req.URL.Path, "/") {
|
||||
redirPath = redirPath + "/"
|
||||
}
|
||||
// Redirect if missing trailing slash.
|
||||
if !strings.HasSuffix(redirPath, "/") {
|
||||
http.Redirect(ctx.Resp, ctx.Req.Request, redirPath+"/", http.StatusFound)
|
||||
return true
|
||||
}
|
||||
|
||||
file = path.Join(file, opt.IndexFile)
|
||||
f, err = opt.FileSystem.Open(file)
|
||||
if err != nil {
|
||||
return false // Discard error.
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
fi, err = f.Stat()
|
||||
if err != nil || fi.IsDir() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if !opt.SkipLogging {
|
||||
log.Println("[Static] Serving " + file)
|
||||
}
|
||||
|
||||
// Add an Expires header to the static content
|
||||
if opt.Expires != nil {
|
||||
ctx.Resp.Header().Set("Expires", opt.Expires())
|
||||
}
|
||||
|
||||
if opt.ETag {
|
||||
tag := `"` + GenerateETag(fmt.Sprintf("%d", fi.Size()), fi.Name(), fi.ModTime().UTC().Format(http.TimeFormat)) + `"`
|
||||
ctx.Resp.Header().Set("ETag", tag)
|
||||
if ctx.Req.Header.Get("If-None-Match") == tag {
|
||||
ctx.Resp.WriteHeader(http.StatusNotModified)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
http.ServeContent(ctx.Resp, ctx.Req.Request, file, fi.ModTime(), f)
|
||||
return true
|
||||
}
|
||||
|
||||
// GenerateETag generates an ETag based on size, filename and file modification time
|
||||
func GenerateETag(fileSize, fileName, modTime string) string {
|
||||
etag := fileSize + fileName + modTime
|
||||
return base64.StdEncoding.EncodeToString([]byte(etag))
|
||||
}
|
||||
|
||||
// Static returns a middleware handler that serves static files in the given directory.
|
||||
func Static(directory string, staticOpt ...StaticOptions) Handler {
|
||||
opt := prepareStaticOptions(directory, staticOpt)
|
||||
|
||||
return func(ctx *Context, log *log.Logger) {
|
||||
staticHandler(ctx, log, opt)
|
||||
}
|
||||
}
|
||||
|
||||
// Statics registers multiple static middleware handlers all at once.
|
||||
func Statics(opt StaticOptions, dirs ...string) Handler {
|
||||
if len(dirs) == 0 {
|
||||
panic("no static directory is given")
|
||||
}
|
||||
opts := make([]StaticOptions, len(dirs))
|
||||
for i := range dirs {
|
||||
opts[i] = prepareStaticOption(dirs[i], opt)
|
||||
}
|
||||
|
||||
return func(ctx *Context, log *log.Logger) {
|
||||
for i := range opts {
|
||||
if staticHandler(ctx, log, opts[i]) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
305
pkg/macaron/static_test.go
Executable file
305
pkg/macaron/static_test.go
Executable file
@@ -0,0 +1,305 @@
|
||||
// Copyright 2013 Martini Authors
|
||||
// Copyright 2014 The Macaron Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package macaron
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
var currentRoot, _ = os.Getwd()
|
||||
|
||||
func Test_Static(t *testing.T) {
|
||||
Convey("Serve static files", t, func() {
|
||||
m := New()
|
||||
m.Use(Static("./"))
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
resp.Body = new(bytes.Buffer)
|
||||
req, err := http.NewRequest("GET", "http://localhost:4000/macaron.go", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
So(resp.Code, ShouldEqual, http.StatusOK)
|
||||
So(resp.Header().Get("Expires"), ShouldBeBlank)
|
||||
So(resp.Body.Len(), ShouldBeGreaterThan, 0)
|
||||
|
||||
Convey("Change static path", func() {
|
||||
m.Get("/", func(ctx *Context) {
|
||||
ctx.ChangeStaticPath("./", "fixtures/basic2")
|
||||
})
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "/", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
|
||||
resp = httptest.NewRecorder()
|
||||
resp.Body = new(bytes.Buffer)
|
||||
req, err = http.NewRequest("GET", "http://localhost:4000/hello.tmpl", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
So(resp.Code, ShouldEqual, http.StatusOK)
|
||||
So(resp.Header().Get("Expires"), ShouldBeBlank)
|
||||
So(resp.Body.Len(), ShouldBeGreaterThan, 0)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Serve static files with local path", t, func() {
|
||||
Root = os.TempDir()
|
||||
f, err := ioutil.TempFile(Root, "static_content")
|
||||
So(err, ShouldBeNil)
|
||||
_, _ = f.WriteString("Expected Content")
|
||||
f.Close()
|
||||
|
||||
m := New()
|
||||
m.Use(Static("."))
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
resp.Body = new(bytes.Buffer)
|
||||
req, err := http.NewRequest("GET", "http://localhost:4000/"+path.Base(strings.Replace(f.Name(), "\\", "/", -1)), nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
So(resp.Code, ShouldEqual, http.StatusOK)
|
||||
So(resp.Header().Get("Expires"), ShouldBeBlank)
|
||||
So(resp.Body.String(), ShouldEqual, "Expected Content")
|
||||
})
|
||||
|
||||
Convey("Serve static files with head", t, func() {
|
||||
m := New()
|
||||
m.Use(Static(currentRoot))
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
resp.Body = new(bytes.Buffer)
|
||||
req, err := http.NewRequest("HEAD", "http://localhost:4000/macaron.go", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
So(resp.Code, ShouldEqual, http.StatusOK)
|
||||
So(resp.Body.Len(), ShouldEqual, 0)
|
||||
})
|
||||
|
||||
Convey("Serve static files as post", t, func() {
|
||||
m := New()
|
||||
m.Use(Static(currentRoot))
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("POST", "http://localhost:4000/macaron.go", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
So(resp.Code, ShouldEqual, http.StatusNotFound)
|
||||
})
|
||||
|
||||
Convey("Serve static files with bad directory", t, func() {
|
||||
m := Classic()
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "http://localhost:4000/macaron.go", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
So(resp.Code, ShouldNotEqual, http.StatusOK)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Static_Options(t *testing.T) {
|
||||
Convey("Serve static files with options logging", t, func() {
|
||||
var buf bytes.Buffer
|
||||
m := NewWithLogger(&buf)
|
||||
opt := StaticOptions{}
|
||||
m.Use(Static(currentRoot, opt))
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "http://localhost:4000/macaron.go", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
|
||||
So(resp.Code, ShouldEqual, http.StatusOK)
|
||||
So(buf.String(), ShouldEqual, "[Macaron] [Static] Serving /macaron.go\n")
|
||||
|
||||
// Not disable logging.
|
||||
m.Handlers()
|
||||
buf.Reset()
|
||||
opt.SkipLogging = true
|
||||
m.Use(Static(currentRoot, opt))
|
||||
m.ServeHTTP(resp, req)
|
||||
|
||||
So(resp.Code, ShouldEqual, http.StatusOK)
|
||||
So(buf.Len(), ShouldEqual, 0)
|
||||
})
|
||||
|
||||
Convey("Serve static files with options serve index", t, func() {
|
||||
var buf bytes.Buffer
|
||||
m := NewWithLogger(&buf)
|
||||
opt := StaticOptions{IndexFile: "macaron.go"}
|
||||
m.Use(Static(currentRoot, opt))
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "http://localhost:4000/", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
|
||||
So(resp.Code, ShouldEqual, http.StatusOK)
|
||||
So(buf.String(), ShouldEqual, "[Macaron] [Static] Serving /macaron.go\n")
|
||||
})
|
||||
|
||||
Convey("Serve static files with options prefix", t, func() {
|
||||
var buf bytes.Buffer
|
||||
m := NewWithLogger(&buf)
|
||||
opt := StaticOptions{Prefix: "public"}
|
||||
m.Use(Static(currentRoot, opt))
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "http://localhost:4000/public/macaron.go", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
|
||||
So(resp.Code, ShouldEqual, http.StatusOK)
|
||||
So(buf.String(), ShouldEqual, "[Macaron] [Static] Serving /macaron.go\n")
|
||||
})
|
||||
|
||||
Convey("Serve static files with options expires", t, func() {
|
||||
m := New()
|
||||
opt := StaticOptions{Expires: func() string { return "46" }}
|
||||
m.Use(Static(currentRoot, opt))
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "http://localhost:4000/macaron.go", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
|
||||
So(resp.Header().Get("Expires"), ShouldEqual, "46")
|
||||
})
|
||||
|
||||
Convey("Serve static files with options ETag", t, func() {
|
||||
m := New()
|
||||
opt := StaticOptions{ETag: true}
|
||||
m.Use(Static(currentRoot, opt))
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "http://localhost:4000/macaron.go", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
tag := GenerateETag(fmt.Sprintf("%d", resp.Body.Len()), "macaron.go", resp.Header().Get("last-modified"))
|
||||
|
||||
So(resp.Header().Get("ETag"), ShouldEqual, `"`+tag+`"`)
|
||||
})
|
||||
|
||||
Convey("Serve static files with ETag in If-None-Match", t, func() {
|
||||
m := New()
|
||||
opt := StaticOptions{ETag: true}
|
||||
m.Use(Static(currentRoot, opt))
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "http://localhost:4000/macaron.go", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
tag := GenerateETag(fmt.Sprintf("%d", resp.Body.Len()), "macaron.go", resp.Header().Get("last-modified"))
|
||||
|
||||
// Second request with ETag in If-None-Match
|
||||
resp = httptest.NewRecorder()
|
||||
req.Header.Add("If-None-Match", `"`+tag+`"`)
|
||||
m.ServeHTTP(resp, req)
|
||||
|
||||
So(resp.Code, ShouldEqual, http.StatusNotModified)
|
||||
So(len(resp.Body.Bytes()), ShouldEqual, 0)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Static_Redirect(t *testing.T) {
|
||||
Convey("Serve static files with prefix without redirect", t, func() {
|
||||
m := New()
|
||||
opt := StaticOptions{Prefix: "/public"}
|
||||
m.Use(Static(currentRoot, opt))
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "http://localhost:4000/public/", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
|
||||
So(resp.Code, ShouldEqual, http.StatusNotFound)
|
||||
})
|
||||
|
||||
Convey("Serve static files with redirect", t, func() {
|
||||
m := New()
|
||||
m.Use(Static(currentRoot, StaticOptions{Prefix: "/public"}))
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "http://localhost:4000/public", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
|
||||
So(resp.Code, ShouldEqual, http.StatusFound)
|
||||
So(resp.Header().Get("Location"), ShouldEqual, "/public/")
|
||||
})
|
||||
|
||||
Convey("Serve static files with improper request", t, func() {
|
||||
m := New()
|
||||
m.Use(Static(currentRoot))
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", `http://localhost:4000//example.com%2f..`, nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
|
||||
So(resp.Code, ShouldEqual, http.StatusNotFound)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Statics(t *testing.T) {
|
||||
Convey("Serve multiple static routers", t, func() {
|
||||
Convey("Register empty directory", func() {
|
||||
defer func() {
|
||||
So(recover(), ShouldNotBeNil)
|
||||
}()
|
||||
|
||||
m := New()
|
||||
m.Use(Statics(StaticOptions{}))
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "http://localhost:4000/", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
})
|
||||
|
||||
Convey("Serve normally", func() {
|
||||
var buf bytes.Buffer
|
||||
m := NewWithLogger(&buf)
|
||||
m.Use(Statics(StaticOptions{}, currentRoot, currentRoot+"/fixtures/basic"))
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "http://localhost:4000/macaron.go", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
|
||||
So(resp.Code, ShouldEqual, http.StatusOK)
|
||||
So(buf.String(), ShouldEqual, "[Macaron] [Static] Serving /macaron.go\n")
|
||||
|
||||
resp = httptest.NewRecorder()
|
||||
req, err = http.NewRequest("GET", "http://localhost:4000/admin/index.tmpl", nil)
|
||||
So(err, ShouldBeNil)
|
||||
m.ServeHTTP(resp, req)
|
||||
|
||||
So(resp.Code, ShouldEqual, http.StatusOK)
|
||||
So(buf.String(), ShouldEndWith, "[Macaron] [Static] Serving /admin/index.tmpl\n")
|
||||
})
|
||||
})
|
||||
}
|
||||
390
pkg/macaron/tree.go
Executable file
390
pkg/macaron/tree.go
Executable file
@@ -0,0 +1,390 @@
|
||||
// Copyright 2015 The Macaron Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package macaron
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/unknwon/com"
|
||||
)
|
||||
|
||||
type patternType int8
|
||||
|
||||
const (
|
||||
_PATTERN_STATIC patternType = iota // /home
|
||||
_PATTERN_REGEXP // /:id([0-9]+)
|
||||
_PATTERN_PATH_EXT // /*.*
|
||||
_PATTERN_HOLDER // /:user
|
||||
_PATTERN_MATCH_ALL // /*
|
||||
)
|
||||
|
||||
// Leaf represents a leaf route information.
|
||||
type Leaf struct {
|
||||
parent *Tree
|
||||
|
||||
typ patternType
|
||||
pattern string
|
||||
rawPattern string // Contains wildcard instead of regexp
|
||||
wildcards []string
|
||||
reg *regexp.Regexp
|
||||
optional bool
|
||||
|
||||
handle Handle
|
||||
}
|
||||
|
||||
var wildcardPattern = regexp.MustCompile(`:[a-zA-Z0-9]+`)
|
||||
|
||||
func isSpecialRegexp(pattern, regStr string, pos []int) bool {
|
||||
return len(pattern) >= pos[1]+len(regStr) && pattern[pos[1]:pos[1]+len(regStr)] == regStr
|
||||
}
|
||||
|
||||
// getNextWildcard tries to find next wildcard and update pattern with corresponding regexp.
|
||||
func getNextWildcard(pattern string) (wildcard, _ string) {
|
||||
pos := wildcardPattern.FindStringIndex(pattern)
|
||||
if pos == nil {
|
||||
return "", pattern
|
||||
}
|
||||
wildcard = pattern[pos[0]:pos[1]]
|
||||
|
||||
// Reach last character or no regexp is given.
|
||||
if len(pattern) == pos[1] {
|
||||
return wildcard, strings.Replace(pattern, wildcard, `(.+)`, 1)
|
||||
} else if pattern[pos[1]] != '(' {
|
||||
switch {
|
||||
case isSpecialRegexp(pattern, ":int", pos):
|
||||
pattern = strings.Replace(pattern, ":int", "([0-9]+)", 1)
|
||||
case isSpecialRegexp(pattern, ":string", pos):
|
||||
pattern = strings.Replace(pattern, ":string", "([\\w]+)", 1)
|
||||
default:
|
||||
return wildcard, strings.Replace(pattern, wildcard, `(.+)`, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// Cut out placeholder directly.
|
||||
return wildcard, pattern[:pos[0]] + pattern[pos[1]:]
|
||||
}
|
||||
|
||||
func getWildcards(pattern string) (string, []string) {
|
||||
wildcards := make([]string, 0, 2)
|
||||
|
||||
// Keep getting next wildcard until nothing is left.
|
||||
var wildcard string
|
||||
for {
|
||||
wildcard, pattern = getNextWildcard(pattern)
|
||||
if len(wildcard) > 0 {
|
||||
wildcards = append(wildcards, wildcard)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return pattern, wildcards
|
||||
}
|
||||
|
||||
// getRawPattern removes all regexp but keeps wildcards for building URL path.
|
||||
func getRawPattern(rawPattern string) string {
|
||||
rawPattern = strings.Replace(rawPattern, ":int", "", -1)
|
||||
rawPattern = strings.Replace(rawPattern, ":string", "", -1)
|
||||
|
||||
for {
|
||||
startIdx := strings.Index(rawPattern, "(")
|
||||
if startIdx == -1 {
|
||||
break
|
||||
}
|
||||
|
||||
closeIdx := strings.Index(rawPattern, ")")
|
||||
if closeIdx > -1 {
|
||||
rawPattern = rawPattern[:startIdx] + rawPattern[closeIdx+1:]
|
||||
}
|
||||
}
|
||||
return rawPattern
|
||||
}
|
||||
|
||||
func checkPattern(pattern string) (typ patternType, rawPattern string, wildcards []string, reg *regexp.Regexp) {
|
||||
pattern = strings.TrimLeft(pattern, "?")
|
||||
rawPattern = getRawPattern(pattern)
|
||||
|
||||
if pattern == "*" {
|
||||
typ = _PATTERN_MATCH_ALL
|
||||
} else if pattern == "*.*" {
|
||||
typ = _PATTERN_PATH_EXT
|
||||
} else if strings.Contains(pattern, ":") {
|
||||
typ = _PATTERN_REGEXP
|
||||
pattern, wildcards = getWildcards(pattern)
|
||||
if pattern == "(.+)" {
|
||||
typ = _PATTERN_HOLDER
|
||||
} else {
|
||||
reg = regexp.MustCompile(pattern)
|
||||
}
|
||||
}
|
||||
return typ, rawPattern, wildcards, reg
|
||||
}
|
||||
|
||||
func NewLeaf(parent *Tree, pattern string, handle Handle) *Leaf {
|
||||
typ, rawPattern, wildcards, reg := checkPattern(pattern)
|
||||
optional := false
|
||||
if len(pattern) > 0 && pattern[0] == '?' {
|
||||
optional = true
|
||||
}
|
||||
return &Leaf{parent, typ, pattern, rawPattern, wildcards, reg, optional, handle}
|
||||
}
|
||||
|
||||
// URLPath build path part of URL by given pair values.
|
||||
func (l *Leaf) URLPath(pairs ...string) string {
|
||||
if len(pairs)%2 != 0 {
|
||||
panic("number of pairs does not match")
|
||||
}
|
||||
|
||||
urlPath := l.rawPattern
|
||||
parent := l.parent
|
||||
for parent != nil {
|
||||
urlPath = parent.rawPattern + "/" + urlPath
|
||||
parent = parent.parent
|
||||
}
|
||||
for i := 0; i < len(pairs); i += 2 {
|
||||
if len(pairs[i]) == 0 {
|
||||
panic("pair value cannot be empty: " + com.ToStr(i))
|
||||
} else if pairs[i][0] != ':' && pairs[i] != "*" && pairs[i] != "*.*" {
|
||||
pairs[i] = ":" + pairs[i]
|
||||
}
|
||||
urlPath = strings.Replace(urlPath, pairs[i], pairs[i+1], 1)
|
||||
}
|
||||
return urlPath
|
||||
}
|
||||
|
||||
// Tree represents a router tree in Macaron.
|
||||
type Tree struct {
|
||||
parent *Tree
|
||||
|
||||
typ patternType
|
||||
pattern string
|
||||
rawPattern string
|
||||
wildcards []string
|
||||
reg *regexp.Regexp
|
||||
|
||||
subtrees []*Tree
|
||||
leaves []*Leaf
|
||||
}
|
||||
|
||||
func NewSubtree(parent *Tree, pattern string) *Tree {
|
||||
typ, rawPattern, wildcards, reg := checkPattern(pattern)
|
||||
return &Tree{parent, typ, pattern, rawPattern, wildcards, reg, make([]*Tree, 0, 5), make([]*Leaf, 0, 5)}
|
||||
}
|
||||
|
||||
func NewTree() *Tree {
|
||||
return NewSubtree(nil, "")
|
||||
}
|
||||
|
||||
func (t *Tree) addLeaf(pattern string, handle Handle) *Leaf {
|
||||
for i := 0; i < len(t.leaves); i++ {
|
||||
if t.leaves[i].pattern == pattern {
|
||||
return t.leaves[i]
|
||||
}
|
||||
}
|
||||
|
||||
leaf := NewLeaf(t, pattern, handle)
|
||||
|
||||
// Add exact same leaf to grandparent/parent level without optional.
|
||||
if leaf.optional {
|
||||
parent := leaf.parent
|
||||
if parent.parent != nil {
|
||||
parent.parent.addLeaf(parent.pattern, handle)
|
||||
} else {
|
||||
parent.addLeaf("", handle) // Root tree can add as empty pattern.
|
||||
}
|
||||
}
|
||||
|
||||
i := 0
|
||||
for ; i < len(t.leaves); i++ {
|
||||
if leaf.typ < t.leaves[i].typ {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if i == len(t.leaves) {
|
||||
t.leaves = append(t.leaves, leaf)
|
||||
} else {
|
||||
t.leaves = append(t.leaves[:i], append([]*Leaf{leaf}, t.leaves[i:]...)...)
|
||||
}
|
||||
return leaf
|
||||
}
|
||||
|
||||
func (t *Tree) addSubtree(segment, pattern string, handle Handle) *Leaf {
|
||||
for i := 0; i < len(t.subtrees); i++ {
|
||||
if t.subtrees[i].pattern == segment {
|
||||
return t.subtrees[i].addNextSegment(pattern, handle)
|
||||
}
|
||||
}
|
||||
|
||||
subtree := NewSubtree(t, segment)
|
||||
i := 0
|
||||
for ; i < len(t.subtrees); i++ {
|
||||
if subtree.typ < t.subtrees[i].typ {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if i == len(t.subtrees) {
|
||||
t.subtrees = append(t.subtrees, subtree)
|
||||
} else {
|
||||
t.subtrees = append(t.subtrees[:i], append([]*Tree{subtree}, t.subtrees[i:]...)...)
|
||||
}
|
||||
return subtree.addNextSegment(pattern, handle)
|
||||
}
|
||||
|
||||
func (t *Tree) addNextSegment(pattern string, handle Handle) *Leaf {
|
||||
pattern = strings.TrimPrefix(pattern, "/")
|
||||
|
||||
i := strings.Index(pattern, "/")
|
||||
if i == -1 {
|
||||
return t.addLeaf(pattern, handle)
|
||||
}
|
||||
return t.addSubtree(pattern[:i], pattern[i+1:], handle)
|
||||
}
|
||||
|
||||
func (t *Tree) Add(pattern string, handle Handle) *Leaf {
|
||||
pattern = strings.TrimSuffix(pattern, "/")
|
||||
return t.addNextSegment(pattern, handle)
|
||||
}
|
||||
|
||||
func (t *Tree) matchLeaf(globLevel int, url string, params Params) (Handle, bool) {
|
||||
url, err := PathUnescape(url)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
for i := 0; i < len(t.leaves); i++ {
|
||||
switch t.leaves[i].typ {
|
||||
case _PATTERN_STATIC:
|
||||
if t.leaves[i].pattern == url {
|
||||
return t.leaves[i].handle, true
|
||||
}
|
||||
case _PATTERN_REGEXP:
|
||||
results := t.leaves[i].reg.FindStringSubmatch(url)
|
||||
// Number of results and wildcasrd should be exact same.
|
||||
if len(results)-1 != len(t.leaves[i].wildcards) {
|
||||
break
|
||||
}
|
||||
|
||||
for j := 0; j < len(t.leaves[i].wildcards); j++ {
|
||||
params[t.leaves[i].wildcards[j]] = results[j+1]
|
||||
}
|
||||
return t.leaves[i].handle, true
|
||||
case _PATTERN_PATH_EXT:
|
||||
j := strings.LastIndex(url, ".")
|
||||
if j > -1 {
|
||||
params[":path"] = url[:j]
|
||||
params[":ext"] = url[j+1:]
|
||||
} else {
|
||||
params[":path"] = url
|
||||
}
|
||||
return t.leaves[i].handle, true
|
||||
case _PATTERN_HOLDER:
|
||||
params[t.leaves[i].wildcards[0]] = url
|
||||
return t.leaves[i].handle, true
|
||||
case _PATTERN_MATCH_ALL:
|
||||
params["*"] = url
|
||||
params["*"+com.ToStr(globLevel)] = url
|
||||
return t.leaves[i].handle, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (t *Tree) matchSubtree(globLevel int, segment, url string, params Params) (Handle, bool) {
|
||||
unescapedSegment, err := PathUnescape(segment)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
for i := 0; i < len(t.subtrees); i++ {
|
||||
switch t.subtrees[i].typ {
|
||||
case _PATTERN_STATIC:
|
||||
if t.subtrees[i].pattern == unescapedSegment {
|
||||
if handle, ok := t.subtrees[i].matchNextSegment(globLevel, url, params); ok {
|
||||
return handle, true
|
||||
}
|
||||
}
|
||||
case _PATTERN_REGEXP:
|
||||
results := t.subtrees[i].reg.FindStringSubmatch(unescapedSegment)
|
||||
if len(results)-1 != len(t.subtrees[i].wildcards) {
|
||||
break
|
||||
}
|
||||
|
||||
for j := 0; j < len(t.subtrees[i].wildcards); j++ {
|
||||
params[t.subtrees[i].wildcards[j]] = results[j+1]
|
||||
}
|
||||
if handle, ok := t.subtrees[i].matchNextSegment(globLevel, url, params); ok {
|
||||
return handle, true
|
||||
}
|
||||
case _PATTERN_HOLDER:
|
||||
if handle, ok := t.subtrees[i].matchNextSegment(globLevel+1, url, params); ok {
|
||||
params[t.subtrees[i].wildcards[0]] = unescapedSegment
|
||||
return handle, true
|
||||
}
|
||||
case _PATTERN_MATCH_ALL:
|
||||
if handle, ok := t.subtrees[i].matchNextSegment(globLevel+1, url, params); ok {
|
||||
params["*"+com.ToStr(globLevel)] = unescapedSegment
|
||||
return handle, true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(t.leaves) > 0 {
|
||||
leaf := t.leaves[len(t.leaves)-1]
|
||||
unescapedURL, err := PathUnescape(segment + "/" + url)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
if leaf.typ == _PATTERN_PATH_EXT {
|
||||
j := strings.LastIndex(unescapedURL, ".")
|
||||
if j > -1 {
|
||||
params[":path"] = unescapedURL[:j]
|
||||
params[":ext"] = unescapedURL[j+1:]
|
||||
} else {
|
||||
params[":path"] = unescapedURL
|
||||
}
|
||||
return leaf.handle, true
|
||||
} else if leaf.typ == _PATTERN_MATCH_ALL {
|
||||
params["*"] = unescapedURL
|
||||
params["*"+com.ToStr(globLevel)] = unescapedURL
|
||||
return leaf.handle, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (t *Tree) matchNextSegment(globLevel int, url string, params Params) (Handle, bool) {
|
||||
i := strings.Index(url, "/")
|
||||
if i == -1 {
|
||||
return t.matchLeaf(globLevel, url, params)
|
||||
}
|
||||
return t.matchSubtree(globLevel, url[:i], url[i+1:], params)
|
||||
}
|
||||
|
||||
func (t *Tree) Match(url string) (Handle, Params, bool) {
|
||||
url = strings.TrimPrefix(url, "/")
|
||||
url = strings.TrimSuffix(url, "/")
|
||||
params := make(Params)
|
||||
handle, ok := t.matchNextSegment(0, url, params)
|
||||
return handle, params, ok
|
||||
}
|
||||
|
||||
// MatchTest returns true if given URL is matched by given pattern.
|
||||
func MatchTest(pattern, url string) bool {
|
||||
t := NewTree()
|
||||
t.Add(pattern, nil)
|
||||
_, _, ok := t.Match(url)
|
||||
return ok
|
||||
}
|
||||
243
pkg/macaron/tree_test.go
Executable file
243
pkg/macaron/tree_test.go
Executable file
@@ -0,0 +1,243 @@
|
||||
// Copyright 2015 The Macaron Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package macaron
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func Test_getWildcards(t *testing.T) {
|
||||
type result struct {
|
||||
pattern string
|
||||
wildcards string
|
||||
}
|
||||
cases := map[string]result{
|
||||
"admin": {"admin", ""},
|
||||
":id": {"(.+)", ":id"},
|
||||
":id:int": {"([0-9]+)", ":id"},
|
||||
":id([0-9]+)": {"([0-9]+)", ":id"},
|
||||
":id([0-9]+)_:name": {"([0-9]+)_(.+)", ":id :name"},
|
||||
"article_:id_:page.html": {"article_(.+)_(.+).html", ":id :page"},
|
||||
"article_:id:int_:page:string.html": {"article_([0-9]+)_([\\w]+).html", ":id :page"},
|
||||
"*": {"*", ""},
|
||||
"*.*": {"*.*", ""},
|
||||
}
|
||||
Convey("Get wildcards", t, func() {
|
||||
for key, result := range cases {
|
||||
pattern, wildcards := getWildcards(key)
|
||||
So(pattern, ShouldEqual, result.pattern)
|
||||
So(strings.Join(wildcards, " "), ShouldEqual, result.wildcards)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func Test_getRawPattern(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
"admin": "admin",
|
||||
":id": ":id",
|
||||
":id:int": ":id",
|
||||
":id([0-9]+)": ":id",
|
||||
":id([0-9]+)_:name": ":id_:name",
|
||||
"article_:id_:page.html": "article_:id_:page.html",
|
||||
"article_:id:int_:page:string.html": "article_:id_:page.html",
|
||||
"article_:id([0-9]+)_:page([\\w]+).html": "article_:id_:page.html",
|
||||
"*": "*",
|
||||
"*.*": "*.*",
|
||||
}
|
||||
Convey("Get raw pattern", t, func() {
|
||||
for k, v := range cases {
|
||||
So(getRawPattern(k), ShouldEqual, v)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Tree_Match(t *testing.T) {
|
||||
Convey("Match route in tree", t, func() {
|
||||
Convey("Match static routes", func() {
|
||||
t := NewTree()
|
||||
So(t.Add("/", nil), ShouldNotBeNil)
|
||||
So(t.Add("/user", nil), ShouldNotBeNil)
|
||||
So(t.Add("/user/unknwon", nil), ShouldNotBeNil)
|
||||
So(t.Add("/user/unknwon/profile", nil), ShouldNotBeNil)
|
||||
|
||||
So(t.Add("/", nil), ShouldNotBeNil)
|
||||
|
||||
_, _, ok := t.Match("/")
|
||||
So(ok, ShouldBeTrue)
|
||||
_, _, ok = t.Match("/user")
|
||||
So(ok, ShouldBeTrue)
|
||||
_, _, ok = t.Match("/user/unknwon")
|
||||
So(ok, ShouldBeTrue)
|
||||
_, _, ok = t.Match("/user/unknwon/profile")
|
||||
So(ok, ShouldBeTrue)
|
||||
|
||||
_, _, ok = t.Match("/404")
|
||||
So(ok, ShouldBeFalse)
|
||||
})
|
||||
|
||||
Convey("Match optional routes", func() {
|
||||
t := NewTree()
|
||||
So(t.Add("/?:user", nil), ShouldNotBeNil)
|
||||
So(t.Add("/user/?:name", nil), ShouldNotBeNil)
|
||||
So(t.Add("/user/list/?:page:int", nil), ShouldNotBeNil)
|
||||
|
||||
_, params, ok := t.Match("/")
|
||||
So(ok, ShouldBeTrue)
|
||||
So(params[":user"], ShouldBeEmpty)
|
||||
_, params, ok = t.Match("/unknwon")
|
||||
So(ok, ShouldBeTrue)
|
||||
So(params[":user"], ShouldEqual, "unknwon")
|
||||
_, params, ok = t.Match("/hello%2Fworld")
|
||||
So(ok, ShouldBeTrue)
|
||||
So(params[":user"], ShouldEqual, "hello/world")
|
||||
|
||||
_, params, ok = t.Match("/user")
|
||||
So(ok, ShouldBeTrue)
|
||||
So(params[":name"], ShouldBeEmpty)
|
||||
_, params, ok = t.Match("/user/unknwon")
|
||||
So(ok, ShouldBeTrue)
|
||||
So(params[":name"], ShouldEqual, "unknwon")
|
||||
_, params, ok = t.Match("/hello%20world")
|
||||
So(ok, ShouldBeTrue)
|
||||
So(params[":user"], ShouldEqual, "hello world")
|
||||
|
||||
_, params, ok = t.Match("/user/list/")
|
||||
So(ok, ShouldBeTrue)
|
||||
So(params[":page"], ShouldBeEmpty)
|
||||
_, params, ok = t.Match("/user/list/123")
|
||||
So(ok, ShouldBeTrue)
|
||||
So(params[":page"], ShouldEqual, "123")
|
||||
})
|
||||
|
||||
Convey("Match with regexp", func() {
|
||||
t := NewTree()
|
||||
So(t.Add("/v1/:year:int/6/23", nil), ShouldNotBeNil)
|
||||
So(t.Add("/v2/2015/:month:int/23", nil), ShouldNotBeNil)
|
||||
So(t.Add("/v3/2015/6/:day:int", nil), ShouldNotBeNil)
|
||||
|
||||
_, params, ok := t.Match("/v1/2015/6/23")
|
||||
So(ok, ShouldBeTrue)
|
||||
So(MatchTest("/v1/:year:int/6/23", "/v1/2015/6/23"), ShouldBeTrue)
|
||||
So(params[":year"], ShouldEqual, "2015")
|
||||
_, _, ok = t.Match("/v1/year/6/23")
|
||||
So(ok, ShouldBeFalse)
|
||||
So(MatchTest("/v1/:year:int/6/23", "/v1/year/6/23"), ShouldBeFalse)
|
||||
|
||||
_, params, ok = t.Match("/v2/2015/6/23")
|
||||
So(ok, ShouldBeTrue)
|
||||
So(params[":month"], ShouldEqual, "6")
|
||||
_, _, ok = t.Match("/v2/2015/month/23")
|
||||
So(ok, ShouldBeFalse)
|
||||
|
||||
_, params, ok = t.Match("/v3/2015/6/23")
|
||||
So(ok, ShouldBeTrue)
|
||||
So(params[":day"], ShouldEqual, "23")
|
||||
_, _, ok = t.Match("/v2/2015/6/day")
|
||||
So(ok, ShouldBeFalse)
|
||||
|
||||
So(t.Add("/v1/shop/cms_:id(.+)_:page(.+).html", nil), ShouldNotBeNil)
|
||||
So(t.Add("/v1/:v/cms/aaa_:id(.+)_:page(.+).html", nil), ShouldNotBeNil)
|
||||
So(t.Add("/v1/:v/cms_:id(.+)_:page(.+).html", nil), ShouldNotBeNil)
|
||||
So(t.Add("/v1/:v(.+)_cms/ttt_:id(.+)_:page:string.html", nil), ShouldNotBeNil)
|
||||
|
||||
_, params, ok = t.Match("/v1/shop/cms_123_1.html")
|
||||
So(ok, ShouldBeTrue)
|
||||
So(params[":id"], ShouldEqual, "123")
|
||||
So(params[":page"], ShouldEqual, "1")
|
||||
|
||||
_, params, ok = t.Match("/v1/2/cms/aaa_124_2.html")
|
||||
So(ok, ShouldBeTrue)
|
||||
So(params[":v"], ShouldEqual, "2")
|
||||
So(params[":id"], ShouldEqual, "124")
|
||||
So(params[":page"], ShouldEqual, "2")
|
||||
|
||||
_, params, ok = t.Match("/v1/3/cms_125_3.html")
|
||||
So(ok, ShouldBeTrue)
|
||||
So(params[":v"], ShouldEqual, "3")
|
||||
So(params[":id"], ShouldEqual, "125")
|
||||
So(params[":page"], ShouldEqual, "3")
|
||||
|
||||
_, params, ok = t.Match("/v1/4_cms/ttt_126_4.html")
|
||||
So(ok, ShouldBeTrue)
|
||||
So(params[":v"], ShouldEqual, "4")
|
||||
So(params[":id"], ShouldEqual, "126")
|
||||
So(params[":page"], ShouldEqual, "4")
|
||||
})
|
||||
|
||||
Convey("Match with path and extension", func() {
|
||||
t := NewTree()
|
||||
So(t.Add("/*.*", nil), ShouldNotBeNil)
|
||||
So(t.Add("/docs/*.*", nil), ShouldNotBeNil)
|
||||
|
||||
_, params, ok := t.Match("/profile.html")
|
||||
So(ok, ShouldBeTrue)
|
||||
So(params[":path"], ShouldEqual, "profile")
|
||||
So(params[":ext"], ShouldEqual, "html")
|
||||
|
||||
_, params, ok = t.Match("/profile")
|
||||
So(ok, ShouldBeTrue)
|
||||
So(params[":path"], ShouldEqual, "profile")
|
||||
So(params[":ext"], ShouldBeEmpty)
|
||||
|
||||
_, params, ok = t.Match("/docs/framework/manual.html")
|
||||
So(ok, ShouldBeTrue)
|
||||
So(params[":path"], ShouldEqual, "framework/manual")
|
||||
So(params[":ext"], ShouldEqual, "html")
|
||||
|
||||
_, params, ok = t.Match("/docs/framework/manual")
|
||||
So(ok, ShouldBeTrue)
|
||||
So(params[":path"], ShouldEqual, "framework/manual")
|
||||
So(params[":ext"], ShouldBeEmpty)
|
||||
})
|
||||
|
||||
Convey("Match all", func() {
|
||||
t := NewTree()
|
||||
So(t.Add("/*", nil), ShouldNotBeNil)
|
||||
So(t.Add("/*/123", nil), ShouldNotBeNil)
|
||||
So(t.Add("/*/123/*", nil), ShouldNotBeNil)
|
||||
So(t.Add("/*/*/123", nil), ShouldNotBeNil)
|
||||
|
||||
_, params, ok := t.Match("/1/2/3")
|
||||
So(ok, ShouldBeTrue)
|
||||
So(params["*0"], ShouldEqual, "1/2/3")
|
||||
|
||||
_, params, ok = t.Match("/4/123")
|
||||
So(ok, ShouldBeTrue)
|
||||
So(params["*0"], ShouldEqual, "4")
|
||||
|
||||
_, params, ok = t.Match("/5/123/6")
|
||||
So(ok, ShouldBeTrue)
|
||||
So(params["*0"], ShouldEqual, "5")
|
||||
So(params["*1"], ShouldEqual, "6")
|
||||
|
||||
_, params, ok = t.Match("/7/8/123")
|
||||
So(ok, ShouldBeTrue)
|
||||
So(params["*0"], ShouldEqual, "7")
|
||||
So(params["*1"], ShouldEqual, "8")
|
||||
})
|
||||
|
||||
Convey("Complex tests", func() {
|
||||
t := NewTree()
|
||||
So(t.Add("/:username/:reponame/commit/*", nil), ShouldNotBeNil)
|
||||
|
||||
_, params, ok := t.Match("/unknwon/com/commit/d855b6c9dea98c619925b7b112f3c4e64b17bfa8")
|
||||
So(ok, ShouldBeTrue)
|
||||
So(params["*"], ShouldEqual, "d855b6c9dea98c619925b7b112f3c4e64b17bfa8")
|
||||
})
|
||||
})
|
||||
}
|
||||
25
pkg/macaron/util_go17.go
Executable file
25
pkg/macaron/util_go17.go
Executable file
@@ -0,0 +1,25 @@
|
||||
// +build !go1.8
|
||||
|
||||
// Copyright 2017 The Macaron Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package macaron
|
||||
|
||||
import "net/url"
|
||||
|
||||
// PathUnescape unescapes a path. Ideally, this function would use
|
||||
// url.PathUnescape(..), but the function was not introduced until go1.8.
|
||||
func PathUnescape(s string) (string, error) {
|
||||
return url.QueryUnescape(s)
|
||||
}
|
||||
24
pkg/macaron/util_go18.go
Executable file
24
pkg/macaron/util_go18.go
Executable file
@@ -0,0 +1,24 @@
|
||||
// +build go1.8
|
||||
|
||||
// Copyright 2017 The Macaron Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package macaron
|
||||
|
||||
import "net/url"
|
||||
|
||||
// PathUnescape unescapes a path.
|
||||
func PathUnescape(s string) (string, error) {
|
||||
return url.PathUnescape(s)
|
||||
}
|
||||
@@ -129,20 +129,22 @@ func Auth(options *AuthOptions) macaron.Handler {
|
||||
}
|
||||
}
|
||||
|
||||
// AdminOrFeatureEnabled creates a middleware that allows access
|
||||
// if the signed in user is either an Org Admin or if the
|
||||
// feature flag is enabled.
|
||||
// AdminOrEditorAndFeatureEnabled creates a middleware that allows
|
||||
// access if the signed in user is either an Org Admin or if they
|
||||
// are an Org Editor and the feature flag is enabled.
|
||||
// Intended for when feature flags open up access to APIs that
|
||||
// are otherwise only available to admins.
|
||||
func AdminOrFeatureEnabled(enabled bool) macaron.Handler {
|
||||
func AdminOrEditorAndFeatureEnabled(enabled bool) macaron.Handler {
|
||||
return func(c *models.ReqContext) {
|
||||
if c.OrgRole == models.ROLE_ADMIN {
|
||||
return
|
||||
}
|
||||
|
||||
if !enabled {
|
||||
accessForbidden(c)
|
||||
if c.OrgRole == models.ROLE_EDITOR && enabled {
|
||||
return
|
||||
}
|
||||
|
||||
accessForbidden(c)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
33
pkg/middleware/csrf.go
Normal file
33
pkg/middleware/csrf.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func CSRF(loginCookieName string) http.Handler {
|
||||
// As per RFC 7231/4.2.2 these methods are idempotent:
|
||||
// (GET is excluded because it may have side effects in some APIs)
|
||||
safeMethods := []string{"HEAD", "OPTIONS", "TRACE"}
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// If request has no login cookie - skip CSRF checks
|
||||
if _, err := r.Cookie(loginCookieName); errors.Is(err, http.ErrNoCookie) {
|
||||
return
|
||||
}
|
||||
// Skip CSRF checks for "safe" methods
|
||||
for _, method := range safeMethods {
|
||||
if r.Method == method {
|
||||
return
|
||||
}
|
||||
}
|
||||
// Otherwise - verify that Origin matches the server origin
|
||||
host := strings.Split(r.Host, ":")[0]
|
||||
origin, err := url.Parse(r.Header.Get("Origin"))
|
||||
if err != nil || (origin.String() != "" && origin.Hostname() != host) {
|
||||
http.Error(w, "origin not allowed", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -55,8 +55,12 @@ type GetTeamByIdQuery struct {
|
||||
SignedInUser *SignedInUser
|
||||
HiddenUsers map[string]struct{}
|
||||
Result *TeamDTO
|
||||
UserIdFilter int64
|
||||
}
|
||||
|
||||
// FilterIgnoreUser is used in a get / search teams query when the caller does not want to filter teams by user ID / membership
|
||||
const FilterIgnoreUser int64 = 0
|
||||
|
||||
type GetTeamsByUserQuery struct {
|
||||
OrgId int64
|
||||
UserId int64 `json:"userId"`
|
||||
|
||||
@@ -406,6 +406,7 @@ func flushStream(plugin Plugin, stream CallResourceClientResponseStream, w http.
|
||||
}
|
||||
}
|
||||
|
||||
proxyutil.SetProxyResponseHeaders(w.Header())
|
||||
w.WriteHeader(resp.Status)
|
||||
}
|
||||
|
||||
|
||||
@@ -177,7 +177,8 @@ func TestManager(t *testing.T) {
|
||||
t.Run("Call resource should return expected response", func(t *testing.T) {
|
||||
ctx.plugin.CallResourceHandlerFunc = backend.CallResourceHandlerFunc(func(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
|
||||
return sender.Send(&backend.CallResourceResponse{
|
||||
Status: http.StatusOK,
|
||||
Status: http.StatusOK,
|
||||
Headers: map[string][]string{},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -186,7 +187,13 @@ func TestManager(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
err = ctx.manager.callResourceInternal(w, req, backend.PluginContext{PluginID: testPluginID})
|
||||
require.NoError(t, err)
|
||||
for {
|
||||
if w.Flushed {
|
||||
break
|
||||
}
|
||||
}
|
||||
require.Equal(t, http.StatusOK, w.Code)
|
||||
require.Equal(t, "sandbox", w.Header().Get("Content-Security-Policy"))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -491,15 +491,15 @@ func GetPluginMarkdown(pluginId string, name string) ([]byte, error) {
|
||||
}
|
||||
|
||||
// nolint:gosec
|
||||
// We can ignore the gosec G304 warning on this one because `plug.PluginDir` is based
|
||||
// on plugin the folder structure on disk and not user input.
|
||||
path := filepath.Join(plug.PluginDir, fmt.Sprintf("%s.md", strings.ToUpper(name)))
|
||||
// We can ignore the gosec G304 warning since we have cleaned the requested file path and subsequently
|
||||
// use this with a prefix of the plugin's directory, which is set during plugin loading
|
||||
path := filepath.Join(plug.PluginDir, mdFilepath(strings.ToUpper(name)))
|
||||
exists, err := fs.Exists(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !exists {
|
||||
path = filepath.Join(plug.PluginDir, fmt.Sprintf("%s.md", strings.ToLower(name)))
|
||||
path = filepath.Join(plug.PluginDir, mdFilepath(strings.ToLower(name)))
|
||||
}
|
||||
|
||||
exists, err = fs.Exists(path)
|
||||
@@ -511,8 +511,8 @@ func GetPluginMarkdown(pluginId string, name string) ([]byte, error) {
|
||||
}
|
||||
|
||||
// nolint:gosec
|
||||
// We can ignore the gosec G304 warning on this one because `plug.PluginDir` is based
|
||||
// on plugin the folder structure on disk and not user input.
|
||||
// We can ignore the gosec G304 warning since we have cleaned the requested file path and subsequently
|
||||
// use this with a prefix of the plugin's directory, which is set during plugin loading
|
||||
data, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -520,6 +520,10 @@ func GetPluginMarkdown(pluginId string, name string) ([]byte, error) {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func mdFilepath(mdFilename string) string {
|
||||
return filepath.Clean(filepath.Join("/", fmt.Sprintf("%s.md", mdFilename)))
|
||||
}
|
||||
|
||||
// gets plugin filenames that require verification for plugin signing
|
||||
func collectPluginFilesWithin(rootDir string) ([]string, error) {
|
||||
var files []string
|
||||
|
||||
@@ -49,12 +49,15 @@ func (e *DefaultEvalHandler) Eval(context *EvalContext) {
|
||||
// calculating Firing based on operator
|
||||
if cr.Operator == "or" {
|
||||
firing = firing || cr.Firing
|
||||
noDataFound = noDataFound || cr.NoDataFound
|
||||
} else {
|
||||
firing = firing && cr.Firing
|
||||
noDataFound = noDataFound && cr.NoDataFound
|
||||
}
|
||||
|
||||
// We cannot evaluate the expression when one or more conditions are missing data
|
||||
// and so noDataFound should be true if at least one condition returns no data,
|
||||
// irrespective of the operator.
|
||||
noDataFound = noDataFound || cr.NoDataFound
|
||||
|
||||
if i > 0 {
|
||||
conditionEvals = "[" + conditionEvals + " " + strings.ToUpper(cr.Operator) + " " + strconv.FormatBool(cr.Firing) + "]"
|
||||
} else {
|
||||
|
||||
@@ -181,7 +181,7 @@ func TestAlertingEvaluationHandler(t *testing.T) {
|
||||
So(context.NoDataFound, ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("Should not return no data if at least one condition has no data and using AND", func() {
|
||||
Convey("Should return no data if at least one condition has no data and using AND", func() {
|
||||
context := NewEvalContext(context.TODO(), &Rule{
|
||||
Conditions: []Condition{
|
||||
&conditionStub{operator: "and", noData: true},
|
||||
@@ -190,7 +190,7 @@ func TestAlertingEvaluationHandler(t *testing.T) {
|
||||
}, &validations.OSSPluginRequestValidator{})
|
||||
|
||||
handler.Eval(context)
|
||||
So(context.NoDataFound, ShouldBeFalse)
|
||||
So(context.NoDataFound, ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("Should return no data if at least one condition has no data and using OR", func() {
|
||||
|
||||
@@ -53,18 +53,6 @@ func getTeamMemberCount(filteredUsers []string) string {
|
||||
return "(SELECT COUNT(*) FROM team_member WHERE team_member.team_id = team.id) AS member_count "
|
||||
}
|
||||
|
||||
func getTeamSearchSQLBase(filteredUsers []string) string {
|
||||
return `SELECT
|
||||
team.id AS id,
|
||||
team.org_id,
|
||||
team.name AS name,
|
||||
team.email AS email,
|
||||
team_member.permission, ` +
|
||||
getTeamMemberCount(filteredUsers) +
|
||||
` FROM team AS team
|
||||
INNER JOIN team_member ON team.id = team_member.team_id AND team_member.user_id = ? `
|
||||
}
|
||||
|
||||
func getTeamSelectSQLBase(filteredUsers []string) string {
|
||||
return `SELECT
|
||||
team.id as id,
|
||||
@@ -187,17 +175,15 @@ func SearchTeams(query *models.SearchTeamsQuery) error {
|
||||
params := make([]interface{}, 0)
|
||||
|
||||
filteredUsers := getFilteredUsers(query.SignedInUser, query.HiddenUsers)
|
||||
if query.UserIdFilter > 0 {
|
||||
sql.WriteString(getTeamSearchSQLBase(filteredUsers))
|
||||
for _, user := range filteredUsers {
|
||||
params = append(params, user)
|
||||
}
|
||||
sql.WriteString(getTeamSelectSQLBase(filteredUsers))
|
||||
|
||||
for _, user := range filteredUsers {
|
||||
params = append(params, user)
|
||||
}
|
||||
|
||||
if query.UserIdFilter != models.FilterIgnoreUser {
|
||||
sql.WriteString(` INNER JOIN team_member ON team.id = team_member.team_id AND team_member.user_id = ?`)
|
||||
params = append(params, query.UserIdFilter)
|
||||
} else {
|
||||
sql.WriteString(getTeamSelectSQLBase(filteredUsers))
|
||||
for _, user := range filteredUsers {
|
||||
params = append(params, user)
|
||||
}
|
||||
}
|
||||
|
||||
sql.WriteString(` WHERE team.org_id = ?`)
|
||||
@@ -226,6 +212,8 @@ func SearchTeams(query *models.SearchTeamsQuery) error {
|
||||
|
||||
team := models.Team{}
|
||||
countSess := x.Table("team")
|
||||
countSess.Where("team.org_id=?", query.OrgId)
|
||||
|
||||
if query.Query != "" {
|
||||
countSess.Where(`name `+dialect.LikeStr()+` ?`, queryWithWildcards)
|
||||
}
|
||||
@@ -234,6 +222,18 @@ func SearchTeams(query *models.SearchTeamsQuery) error {
|
||||
countSess.Where("name=?", query.Name)
|
||||
}
|
||||
|
||||
// If we're not retrieving all results, then only search for teams that this user has access to
|
||||
if query.UserIdFilter != models.FilterIgnoreUser {
|
||||
countSess.
|
||||
Where(`
|
||||
team.id IN (
|
||||
SELECT
|
||||
team_id
|
||||
FROM team_member
|
||||
WHERE team_member.user_id = ?
|
||||
)`, query.UserIdFilter)
|
||||
}
|
||||
|
||||
count, err := countSess.Count(&team)
|
||||
query.Result.TotalCount = count
|
||||
|
||||
@@ -250,6 +250,11 @@ func GetTeamById(query *models.GetTeamByIdQuery) error {
|
||||
params = append(params, user)
|
||||
}
|
||||
|
||||
if query.UserIdFilter != models.FilterIgnoreUser {
|
||||
sql.WriteString(` INNER JOIN team_member ON team.id = team_member.team_id AND team_member.user_id = ?`)
|
||||
params = append(params, query.UserIdFilter)
|
||||
}
|
||||
|
||||
sql.WriteString(` WHERE team.org_id = ? and team.id = ?`)
|
||||
params = append(params, query.OrgId, query.Id)
|
||||
|
||||
|
||||
@@ -142,6 +142,10 @@ func GetExternalUserInfoByLogin(query *models.GetExternalUserInfoByLoginQuery) e
|
||||
}
|
||||
|
||||
func GetAuthInfo(query *models.GetAuthInfoQuery) error {
|
||||
if query.UserId == 0 && query.AuthId == "" {
|
||||
return models.ErrUserNotFound
|
||||
}
|
||||
|
||||
userAuth := &models.UserAuth{
|
||||
UserId: query.UserId,
|
||||
AuthModule: query.AuthModule,
|
||||
|
||||
@@ -42,3 +42,9 @@ func ClearCookieHeader(req *http.Request, keepCookiesNames []string) {
|
||||
req.AddCookie(c)
|
||||
}
|
||||
}
|
||||
|
||||
// SetProxyResponseHeaders sets proxy response headers.
|
||||
// Sets Content-Security-Policy: sandbox
|
||||
func SetProxyResponseHeaders(header http.Header) {
|
||||
header.Set("Content-Security-Policy", "sandbox")
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@grafana-plugins/input-datasource",
|
||||
"version": "7.5.10",
|
||||
"version": "7.5.14",
|
||||
"description": "Input Datasource",
|
||||
"private": true,
|
||||
"repository": {
|
||||
@@ -16,9 +16,9 @@
|
||||
"author": "Grafana Labs",
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@grafana/data": "7.5.10",
|
||||
"@grafana/toolkit": "7.5.10",
|
||||
"@grafana/ui": "7.5.10"
|
||||
"@grafana/data": "7.5.14",
|
||||
"@grafana/toolkit": "7.5.14",
|
||||
"@grafana/ui": "7.5.14"
|
||||
},
|
||||
"volta": {
|
||||
"node": "12.16.2"
|
||||
|
||||
136
scripts/lib.star
136
scripts/lib.star
@@ -1,4 +1,4 @@
|
||||
grabpl_version = '0.5.58'
|
||||
grabpl_version = '0.5.60'
|
||||
build_image = 'grafana/build-container:1.4.1'
|
||||
publish_image = 'grafana/grafana-ci-deploy:1.3.1'
|
||||
grafana_docker_image = 'grafana/drone-grafana-docker:0.3.2'
|
||||
@@ -11,17 +11,26 @@ test_release_ver = 'v7.3.0-test'
|
||||
def pipeline(
|
||||
name, edition, trigger, steps, ver_mode, services=[], platform='linux', depends_on=[],
|
||||
is_downstream=False, install_deps=True,
|
||||
):
|
||||
):
|
||||
if platform != 'windows':
|
||||
platform_conf = {
|
||||
'os': 'linux',
|
||||
'arch': 'amd64',
|
||||
'platform': {
|
||||
'os': 'linux',
|
||||
'arch': 'amd64'
|
||||
},
|
||||
# A shared cache is used on the host
|
||||
# To avoid issues with parallel builds, we run this repo on single build agents
|
||||
'node': {
|
||||
'type': 'no-parallel'
|
||||
}
|
||||
}
|
||||
else:
|
||||
platform_conf = {
|
||||
'os': 'windows',
|
||||
'arch': 'amd64',
|
||||
'version': '1809',
|
||||
'platform': {
|
||||
'os': 'windows',
|
||||
'arch': 'amd64',
|
||||
'version': '1809',
|
||||
}
|
||||
}
|
||||
|
||||
pipeline = {
|
||||
@@ -36,6 +45,7 @@ def pipeline(
|
||||
) + steps,
|
||||
'depends_on': depends_on,
|
||||
}
|
||||
pipeline.update(platform_conf)
|
||||
|
||||
if edition in ('enterprise', 'enterprise2'):
|
||||
# We have a custom clone step for enterprise
|
||||
@@ -116,6 +126,8 @@ def init_steps(edition, platform, ver_mode, is_downstream=False, install_deps=Tr
|
||||
'curl -fLO https://github.com/jwilder/dockerize/releases/download/v$${DOCKERIZE_VERSION}/dockerize-linux-amd64-v$${DOCKERIZE_VERSION}.tar.gz',
|
||||
'tar -C bin -xzvf dockerize-linux-amd64-v$${DOCKERIZE_VERSION}.tar.gz',
|
||||
'rm dockerize-linux-amd64-v$${DOCKERIZE_VERSION}.tar.gz',
|
||||
'mv /etc/apt/sources.list.d/nodesource.list /etc/apt/sources.list.d/nodesource.list.disabled; apt-get update; apt-get -y upgrade; apt-get install -y ca-certificates libgnutls30',
|
||||
'mv /etc/apt/sources.list.d/nodesource.list.disabled /etc/apt/sources.list.d/nodesource.list',
|
||||
'yarn install --frozen-lockfile --no-progress',
|
||||
])
|
||||
if edition in ('enterprise', 'enterprise2'):
|
||||
@@ -157,13 +169,13 @@ def init_steps(edition, platform, ver_mode, is_downstream=False, install_deps=Tr
|
||||
'clone',
|
||||
],
|
||||
'commands': [
|
||||
'mv bin/grabpl /tmp/',
|
||||
'rmdir bin',
|
||||
'mv grafana-enterprise /tmp/',
|
||||
'/tmp/grabpl init-enterprise /tmp/grafana-enterprise{}'.format(source_commit),
|
||||
'mkdir bin',
|
||||
'mv /tmp/grabpl bin/'
|
||||
] + common_cmds,
|
||||
'mv bin/grabpl /tmp/',
|
||||
'rmdir bin',
|
||||
'mv grafana-enterprise /tmp/',
|
||||
'/tmp/grabpl init-enterprise /tmp/grafana-enterprise{}'.format(source_commit),
|
||||
'mkdir bin',
|
||||
'mv /tmp/grabpl bin/'
|
||||
] + common_cmds,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -230,7 +242,7 @@ def benchmark_ldap_step():
|
||||
'initialize',
|
||||
],
|
||||
'environment': {
|
||||
'LDAP_HOSTNAME': 'ldap',
|
||||
'LDAP_HOSTNAME': 'ldap',
|
||||
},
|
||||
'commands': [
|
||||
'./bin/dockerize -wait tcp://ldap:389 -timeout 120s',
|
||||
@@ -243,9 +255,9 @@ def ldap_service():
|
||||
'name': 'ldap',
|
||||
'image': 'osixia/openldap:1.4.0',
|
||||
'environment': {
|
||||
'LDAP_ADMIN_PASSWORD': 'grafana',
|
||||
'LDAP_DOMAIN': 'grafana.org',
|
||||
'SLAPD_ADDITIONAL_MODULES': 'memberof',
|
||||
'LDAP_ADMIN_PASSWORD': 'grafana',
|
||||
'LDAP_DOMAIN': 'grafana.org',
|
||||
'SLAPD_ADDITIONAL_MODULES': 'memberof',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -284,12 +296,12 @@ def publish_storybook_step(edition, ver_mode):
|
||||
else:
|
||||
channels = ['canary',]
|
||||
commands.extend([
|
||||
'printenv GCP_KEY | base64 -d > /tmp/gcpkey.json',
|
||||
'gcloud auth activate-service-account --key-file=/tmp/gcpkey.json',
|
||||
] + [
|
||||
'gsutil -m rsync -d -r ./packages/grafana-ui/dist/storybook gs://grafana-storybook/{}'.format(c)
|
||||
for c in channels
|
||||
])
|
||||
'printenv GCP_KEY | base64 -d > /tmp/gcpkey.json',
|
||||
'gcloud auth activate-service-account --key-file=/tmp/gcpkey.json',
|
||||
] + [
|
||||
'gsutil -m rsync -d -r ./packages/grafana-ui/dist/storybook gs://grafana-storybook/{}'.format(c)
|
||||
for c in channels
|
||||
])
|
||||
|
||||
return {
|
||||
'name': 'publish-storybook',
|
||||
@@ -312,14 +324,14 @@ def upload_cdn(edition):
|
||||
'image': publish_image,
|
||||
'depends_on': [
|
||||
'package' + enterprise2_sfx(edition),
|
||||
],
|
||||
],
|
||||
'environment': {
|
||||
'GCP_GRAFANA_UPLOAD_KEY': {
|
||||
'from_secret': 'gcp_key',
|
||||
},
|
||||
},
|
||||
'commands': [
|
||||
'./bin/grabpl upload-cdn --edition {} --bucket "grafana-static-assets"'.format(edition),
|
||||
'./bin/grabpl upload-cdn --edition {} --bucket "grafana-static-assets"'.format(edition),
|
||||
],
|
||||
}
|
||||
|
||||
@@ -370,7 +382,7 @@ def build_backend_step(edition, ver_mode, variants=None, is_downstream=False):
|
||||
'initialize',
|
||||
'lint-backend' + enterprise2_sfx(edition),
|
||||
'test-backend' + enterprise2_sfx(edition),
|
||||
],
|
||||
],
|
||||
'environment': env,
|
||||
'commands': cmds,
|
||||
}
|
||||
@@ -385,18 +397,18 @@ def build_frontend_step(edition, ver_mode, is_downstream=False):
|
||||
if ver_mode == 'release':
|
||||
cmds = [
|
||||
'./bin/grabpl build-frontend --jobs 8 --github-token $${GITHUB_TOKEN} --no-install-deps ' + \
|
||||
'--edition {} --no-pull-enterprise ${{DRONE_TAG}}'.format(edition),
|
||||
]
|
||||
'--edition {} --no-pull-enterprise ${{DRONE_TAG}}'.format(edition),
|
||||
]
|
||||
elif ver_mode == 'test-release':
|
||||
cmds = [
|
||||
'./bin/grabpl build-frontend --jobs 8 --github-token $${GITHUB_TOKEN} --no-install-deps ' + \
|
||||
'--edition {} --no-pull-enterprise {}'.format(edition, test_release_ver),
|
||||
'--edition {} --no-pull-enterprise {}'.format(edition, test_release_ver),
|
||||
]
|
||||
else:
|
||||
cmds = [
|
||||
'./bin/grabpl build-frontend --jobs 8 --no-install-deps --edition {} '.format(edition) + \
|
||||
'--build-id {} --no-pull-enterprise'.format(build_no),
|
||||
]
|
||||
'--build-id {} --no-pull-enterprise'.format(build_no),
|
||||
]
|
||||
|
||||
return {
|
||||
'name': 'build-frontend',
|
||||
@@ -452,7 +464,7 @@ def test_backend_step(edition):
|
||||
'depends_on': [
|
||||
'initialize',
|
||||
'lint-backend' + enterprise2_sfx(edition),
|
||||
],
|
||||
],
|
||||
'commands': [
|
||||
# First make sure that there are no tests with FocusConvey
|
||||
'[ $(grep FocusConvey -R pkg | wc -l) -eq "0" ] || exit 1',
|
||||
@@ -554,7 +566,7 @@ def gen_version_step(ver_mode, include_enterprise2=False, is_downstream=False):
|
||||
deps.extend([
|
||||
'build-backend' + sfx,
|
||||
'test-backend' + sfx,
|
||||
])
|
||||
])
|
||||
|
||||
if ver_mode == 'release':
|
||||
args = '${DRONE_TAG}'
|
||||
@@ -611,17 +623,17 @@ def package_step(edition, ver_mode, variants=None, is_downstream=False):
|
||||
if ver_mode == 'release':
|
||||
cmds = [
|
||||
'{}./bin/grabpl package --jobs 8 --edition {} '.format(test_args, edition) + \
|
||||
'--github-token $${{GITHUB_TOKEN}} --no-pull-enterprise{} ${{DRONE_TAG}}'.format(
|
||||
sign_args
|
||||
),
|
||||
]
|
||||
'--github-token $${{GITHUB_TOKEN}} --no-pull-enterprise{} ${{DRONE_TAG}}'.format(
|
||||
sign_args
|
||||
),
|
||||
]
|
||||
elif ver_mode == 'test-release':
|
||||
cmds = [
|
||||
'{}./bin/grabpl package --jobs 8 --edition {} '.format(test_args, edition) + \
|
||||
'--github-token $${{GITHUB_TOKEN}} --no-pull-enterprise{} {}'.format(
|
||||
sign_args, test_release_ver,
|
||||
),
|
||||
]
|
||||
'--github-token $${{GITHUB_TOKEN}} --no-pull-enterprise{} {}'.format(
|
||||
sign_args, test_release_ver,
|
||||
),
|
||||
]
|
||||
else:
|
||||
if not is_downstream:
|
||||
build_no = '${DRONE_BUILD_NUMBER}'
|
||||
@@ -629,8 +641,8 @@ def package_step(edition, ver_mode, variants=None, is_downstream=False):
|
||||
build_no = '$${SOURCE_BUILD_NUMBER}'
|
||||
cmds = [
|
||||
'{}./bin/grabpl package --jobs 8 --edition {} '.format(test_args, edition) + \
|
||||
'--build-id {} --no-pull-enterprise{}{}'.format(build_no, variants_str, sign_args),
|
||||
]
|
||||
'--build-id {} --no-pull-enterprise{}{}'.format(build_no, variants_str, sign_args),
|
||||
]
|
||||
|
||||
return {
|
||||
'name': 'package' + enterprise2_sfx(edition),
|
||||
@@ -664,7 +676,7 @@ def e2e_tests_server_step(edition, port=3001):
|
||||
'detach': True,
|
||||
'depends_on': [
|
||||
'package' + enterprise2_sfx(edition),
|
||||
],
|
||||
],
|
||||
'environment': environment,
|
||||
'commands': [
|
||||
'./e2e/start-server',
|
||||
@@ -680,7 +692,7 @@ def e2e_tests_step(edition, port=3001, tries=None):
|
||||
'image': 'grafana/ci-e2e:12.19.0-1',
|
||||
'depends_on': [
|
||||
'end-to-end-tests-server' + enterprise2_sfx(edition),
|
||||
],
|
||||
],
|
||||
'environment': {
|
||||
'HOST': 'end-to-end-tests-server' + enterprise2_sfx(edition),
|
||||
},
|
||||
@@ -765,15 +777,16 @@ def postgres_integration_tests_step():
|
||||
'POSTGRES_HOST': 'postgres',
|
||||
},
|
||||
'commands': [
|
||||
'apt-get update',
|
||||
'mv /etc/apt/sources.list.d/nodesource.list /etc/apt/sources.list.d/nodesource.list.disabled; apt-get update; apt-get -y upgrade; apt-get install -y ca-certificates libgnutls30',
|
||||
'mv /etc/apt/sources.list.d/nodesource.list.disabled /etc/apt/sources.list.d/nodesource.list',
|
||||
'apt-get install -yq postgresql-client',
|
||||
'./bin/dockerize -wait tcp://postgres:5432 -timeout 120s',
|
||||
'psql -p 5432 -h postgres -U grafanatest -d grafanatest -f ' +
|
||||
'devenv/docker/blocks/postgres_tests/setup.sql',
|
||||
'devenv/docker/blocks/postgres_tests/setup.sql',
|
||||
# Make sure that we don't use cached results for another database
|
||||
'go clean -testcache',
|
||||
'./bin/grabpl integration-tests --database postgres',
|
||||
],
|
||||
],
|
||||
}
|
||||
|
||||
def mysql_integration_tests_step():
|
||||
@@ -789,7 +802,8 @@ def mysql_integration_tests_step():
|
||||
'MYSQL_HOST': 'mysql',
|
||||
},
|
||||
'commands': [
|
||||
'apt-get update',
|
||||
'mv /etc/apt/sources.list.d/nodesource.list /etc/apt/sources.list.d/nodesource.list.disabled; apt-get update; apt-get -y upgrade; apt-get install -y ca-certificates libgnutls30',
|
||||
'mv /etc/apt/sources.list.d/nodesource.list.disabled /etc/apt/sources.list.d/nodesource.list',
|
||||
'apt-get install -yq default-mysql-client',
|
||||
'./bin/dockerize -wait tcp://mysql:3306 -timeout 120s',
|
||||
'cat devenv/docker/blocks/mysql_tests/setup.sql | mysql -h mysql -P 3306 -u root -prootpass',
|
||||
@@ -852,7 +866,7 @@ def upload_packages_step(edition, ver_mode, is_downstream=False):
|
||||
|
||||
if ver_mode == 'test-release':
|
||||
cmd = './bin/grabpl upload-packages --edition {} '.format(edition) + \
|
||||
'--packages-bucket grafana-downloads-test'
|
||||
'--packages-bucket grafana-downloads-test'
|
||||
else:
|
||||
cmd = './bin/grabpl upload-packages --edition {}{}'.format(edition, packages_bucket)
|
||||
|
||||
@@ -864,7 +878,7 @@ def upload_packages_step(edition, ver_mode, is_downstream=False):
|
||||
'end-to-end-tests' + enterprise2_sfx(edition),
|
||||
'mysql-integration-tests',
|
||||
'postgres-integration-tests',
|
||||
],
|
||||
],
|
||||
'environment': {
|
||||
'GCP_GRAFANA_UPLOAD_KEY': {
|
||||
'from_secret': 'gcp_key',
|
||||
@@ -876,10 +890,10 @@ def upload_packages_step(edition, ver_mode, is_downstream=False):
|
||||
def publish_packages_step(edition, ver_mode, is_downstream=False):
|
||||
if ver_mode == 'test-release':
|
||||
cmd = './bin/grabpl publish-packages --edition {} --gcp-key /tmp/gcpkey.json '.format(edition) + \
|
||||
'--deb-db-bucket grafana-testing-aptly-db --deb-repo-bucket grafana-testing-repo --packages-bucket ' + \
|
||||
'grafana-downloads-test --rpm-repo-bucket grafana-testing-repo --simulate-release {}'.format(
|
||||
test_release_ver,
|
||||
)
|
||||
'--deb-db-bucket grafana-testing-aptly-db --deb-repo-bucket grafana-testing-repo --packages-bucket ' + \
|
||||
'grafana-downloads-test --rpm-repo-bucket grafana-testing-repo --simulate-release {}'.format(
|
||||
test_release_ver,
|
||||
)
|
||||
elif ver_mode == 'release':
|
||||
cmd = './bin/grabpl publish-packages --edition {} --gcp-key /tmp/gcpkey.json ${{DRONE_TAG}}'.format(
|
||||
edition,
|
||||
@@ -890,7 +904,7 @@ def publish_packages_step(edition, ver_mode, is_downstream=False):
|
||||
else:
|
||||
build_no = '$${SOURCE_BUILD_NUMBER}'
|
||||
cmd = './bin/grabpl publish-packages --edition {} --gcp-key /tmp/gcpkey.json --build-id {}'.format(
|
||||
edition, build_no,
|
||||
edition, build_no,
|
||||
)
|
||||
else:
|
||||
fail('Unexpected version mode {}'.format(ver_mode))
|
||||
@@ -1046,14 +1060,14 @@ def get_windows_steps(edition, ver_mode, is_downstream=False):
|
||||
return steps
|
||||
|
||||
def integration_test_services():
|
||||
return [
|
||||
return [
|
||||
{
|
||||
'name': 'postgres',
|
||||
'image': 'postgres:12.3-alpine',
|
||||
'environment': {
|
||||
'POSTGRES_USER': 'grafanatest',
|
||||
'POSTGRES_PASSWORD': 'grafanatest',
|
||||
'POSTGRES_DB': 'grafanatest',
|
||||
'POSTGRES_USER': 'grafanatest',
|
||||
'POSTGRES_PASSWORD': 'grafanatest',
|
||||
'POSTGRES_DB': 'grafanatest',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
18
yarn.lock
18
yarn.lock
@@ -9567,19 +9567,19 @@ caniuse-db@1.0.30000772:
|
||||
integrity sha1-UarokXaChureSj2DGep21qAbUSs=
|
||||
|
||||
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001173:
|
||||
version "1.0.30001178"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001178.tgz#3ad813b2b2c7d585b0be0a2440e1e233c6eabdbc"
|
||||
integrity sha512-VtdZLC0vsXykKni8Uztx45xynytOi71Ufx9T8kHptSw9AL4dpqailUJJHavttuzUe1KYuBYtChiWv+BAb7mPmQ==
|
||||
version "1.0.30001299"
|
||||
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001299.tgz"
|
||||
integrity sha512-iujN4+x7QzqA2NCSrS5VUy+4gLmRd4xv6vbBBsmfVqTx8bLAD8097euLqQgKxSVLvxjSDcvF1T/i9ocgnUFexw==
|
||||
|
||||
caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001020, caniuse-lite@^1.0.30001035, caniuse-lite@^1.0.30001093:
|
||||
version "1.0.30001104"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001104.tgz#4e3d5b3b1dd3c3529f10cb7f519c62ba3e579f5d"
|
||||
integrity sha512-pkpCg7dmI/a7WcqM2yfdOiT4Xx5tzyoHAXWsX5/HxZ3TemwDZs0QXdqbE0UPLPVy/7BeK7693YfzfRYfu1YVpg==
|
||||
version "1.0.30001299"
|
||||
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001299.tgz"
|
||||
integrity sha512-iujN4+x7QzqA2NCSrS5VUy+4gLmRd4xv6vbBBsmfVqTx8bLAD8097euLqQgKxSVLvxjSDcvF1T/i9ocgnUFexw==
|
||||
|
||||
caniuse-lite@^1.0.30001109:
|
||||
version "1.0.30001179"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001179.tgz#b0803883b4471a6c62066fb1752756f8afc699c8"
|
||||
integrity sha512-blMmO0QQujuUWZKyVrD1msR4WNDAqb/UPO1Sw2WWsQ7deoM5bJiicKnWJ1Y0NS/aGINSnKPIWBMw5luX+NDUCA==
|
||||
version "1.0.30001299"
|
||||
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001299.tgz"
|
||||
integrity sha512-iujN4+x7QzqA2NCSrS5VUy+4gLmRd4xv6vbBBsmfVqTx8bLAD8097euLqQgKxSVLvxjSDcvF1T/i9ocgnUFexw==
|
||||
|
||||
capture-exit@^2.0.0:
|
||||
version "2.0.0"
|
||||
|
||||
Reference in New Issue
Block a user