Merge branch 'large-files' into 'master'

S3 storage for large files

See merge request acs/public/villas/web-backend-go!22
This commit is contained in:
Sonja Happ 2021-01-21 16:20:59 +01:00
commit d11c1c5f61
10 changed files with 541 additions and 126 deletions

View file

@ -1,22 +1,19 @@
services:
- postgres:latest
- rabbitmq:latest
variables:
DOCKER_TAG: ${CI_COMMIT_SHORT_SHA}
DOCKER_TAG: ${CI_COMMIT_BRANCH}
DOCKER_IMAGE: ${CI_REGISTRY_IMAGE}
POSTGRES_DB: testvillasdb
POSTGRES_USER: villas
POSTGRES_PASSWORD: villas
POSTGRES_HOST: postgres
RABBITMQ_DEFAULT_USER: villas
RABBITMQ_DEFAULT_PASS: villas
AMQP_HOST: rabbitmq:5672
AMQP_USER: villas
AMQP_PASS: villas
PORT: 4000
GO_IMAGE: golang:1.13-buster
# Template
.go:
image: ${GO_IMAGE}
variables:
GOPATH: $CI_PROJECT_DIR/.go
before_script:
- mkdir -p .go
cache:
paths:
- .go/pkg/mod/
stages:
- build
- test
@ -27,11 +24,11 @@ stages:
build:
stage: build
image: ${GO_IMAGE}
extends: .go
script:
- go mod tidy
- go install github.com/swaggo/swag/cmd/swag
- swag init -p pascalcase -g "start.go" -o "./doc/api/"
- ${GOPATH}/bin/swag init -p pascalcase -g "start.go" -o "./doc/api/"
- go build
artifacts:
paths:
@ -43,26 +40,83 @@ build:
test:
stage: test
image: ${GO_IMAGE}
extends: .go
variables:
POSTGRES_DB: testvillasdb
POSTGRES_USER: villas
POSTGRES_PASSWORD: villas
POSTGRES_HOST: postgres
RABBITMQ_DEFAULT_USER: villas
RABBITMQ_DEFAULT_PASS: villas
MINIO_ROOT_USER: minio-villas
MINIO_ROOT_PASSWORD: minio-villas
MINIO_REGION_NAME: default
AWS_ACCESS_KEY_ID: ${MINIO_ROOT_USER}
AWS_SECRET_ACCESS_KEY: ${MINIO_ROOT_PASSWORD}
S3_BUCKET: villas-web
S3_ENDPOINT: http://minio:9000
S3_PATHSTYLE: 'true'
S3_NOSSL: 'false'
S3_REGION: ${MINIO_REGION_NAME}
AMQP_HOST: rabbitmq:5672
AMQP_USER: villas
AMQP_PASS: villas
PORT: 4000
DB_NAME: ${POSTGRES_DB}
DB_HOST: ${POSTGRES_HOST}
DB_USER: ${POSTGRES_USER}
DB_PASS: ${POSTGRES_PASSWORD}
BASE_PATH: /api
MODE: test
MODE: release
ADMIN_USER: User_0
ADMIN_PASS: xyz789
services:
- postgres:latest
- rabbitmq:latest
- name: minio/minio:RELEASE.2021-01-16T02-19-44Z
command: ['server', '/minio']
alias: minio
before_script:
- wget -qO /usr/bin/mc https://dl.min.io/client/mc/release/linux-amd64/mc && chmod +x /usr/bin/mc
- mc alias set gitlab http://minio:9000 ${MINIO_ROOT_USER} ${MINIO_ROOT_PASSWORD}
- mc mb gitlab/${S3_BUCKET}
script:
- go mod tidy
- go test $(go list ./... )
-p 1
-v
-covermode=count
-coverprofile ./testcover.txt
- go tool cover -func=testcover.txt
dependencies:
- build
test:files_postgres:
stage: test
extends: .go
variables:
POSTGRES_DB: testvillasdb
POSTGRES_USER: villas
POSTGRES_PASSWORD: villas
POSTGRES_HOST: postgres
PORT: 4000
DB_NAME: ${POSTGRES_DB}
DB_HOST: ${POSTGRES_HOST}
DB_USER: ${POSTGRES_USER}
DB_PASS: ${POSTGRES_PASSWORD}
BASE_PATH: /api
MODE: release
ADMIN_USER: User_0
ADMIN_PASS: xyz789
services:
- postgres:latest
script:
- go mod tidy
- cd routes/file
- go test -v
dependencies:
- build
# Stage: deploy
##############################################################################
@ -80,7 +134,5 @@ deploy:
--dockerfile ${CI_PROJECT_DIR}/Dockerfile
--destination ${DOCKER_IMAGE}:${DOCKER_TAG}
--snapshotMode=redo
--cache=true
--cache-ttl=12h
dependencies:
- test

View file

@ -38,59 +38,84 @@ func InitConfig() error {
}
var (
dbHost = flag.String("db-host", "/var/run/postgresql", "Host of the PostgreSQL database (default is /var/run/postgresql for localhost DB on Ubuntu systems)")
dbName = flag.String("db-name", "villasdb", "Name of the database to use (default is villasdb)")
dbUser = flag.String("db-user", "", "Username of database connection (default is <empty>)")
dbPass = flag.String("db-pass", "", "Password of database connection (default is <empty>)")
dbSSLMode = flag.String("db-ssl-mode", "disable", "SSL mode of DB (default is disable)") // TODO: change default for production
amqpHost = flag.String("amqp-host", "", "If set, use this as host for AMQP broker (default is disabled)")
amqpUser = flag.String("amqp-user", "", "Username for AMQP broker")
amqpPass = flag.String("amqp-pass", "", "Password for AMQP broker")
configFile = flag.String("config", "", "Path to YAML configuration file")
mode = flag.String("mode", "release", "Select debug/release/test mode (default is release)")
port = flag.String("port", "4000", "Port of the backend (default is 4000)")
baseHost = flag.String("base-host", "localhost:4000", "The host at which the backend is hosted (default: localhost)")
basePath = flag.String("base-path", "/api/v2", "The path at which the API routes are located (default /api/v2)")
adminUser = flag.String("admin-user", "", "Initial admin username")
adminPass = flag.String("admin-pass", "", "Initial admin password")
adminMail = flag.String("admin-mail", "", "Initial admin mail address")
dbHost = flag.String("db-host", "/var/run/postgresql", "Host of the PostgreSQL database (default is /var/run/postgresql for localhost DB on Ubuntu systems)")
dbName = flag.String("db-name", "villasdb", "Name of the database to use (default is villasdb)")
dbUser = flag.String("db-user", "", "Username of database connection (default is <empty>)")
dbPass = flag.String("db-pass", "", "Password of database connection (default is <empty>)")
dbSSLMode = flag.String("db-ssl-mode", "disable", "SSL mode of DB (default is disable)") // TODO: change default for production
amqpHost = flag.String("amqp-host", "", "If set, use this as host for AMQP broker (default is disabled)")
amqpUser = flag.String("amqp-user", "", "Username for AMQP broker")
amqpPass = flag.String("amqp-pass", "", "Password for AMQP broker")
configFile = flag.String("config", "", "Path to YAML configuration file")
mode = flag.String("mode", "release", "Select debug/release/test mode (default is release)")
port = flag.String("port", "4000", "Port of the backend (default is 4000)")
baseHost = flag.String("base-host", "localhost:4000", "The host at which the backend is hosted (default: localhost)")
basePath = flag.String("base-path", "/api/v2", "The path at which the API routes are located (default /api/v2)")
adminUser = flag.String("admin-user", "", "Initial admin username")
adminPass = flag.String("admin-pass", "", "Initial admin password")
adminMail = flag.String("admin-mail", "", "Initial admin mail address")
s3Bucket = flag.String("s3-bucket", "", "S3 Bucket for uploading files")
s3Endpoint = flag.String("s3-endpoint", "", "Endpoint of S3 API for file uploads")
s3Region = flag.String("s3-region", "default", "S3 Region for file uploads")
s3NoSSL = flag.Bool("s3-nossl", false, "Use encrypted connections to the S3 API")
s3PathStyle = flag.Bool("s3-pathstyle", false, "Use path-style S3 API")
)
flag.Parse()
static := map[string]string{
"db.host": *dbHost,
"db.name": *dbName,
"db.user": *dbUser,
"db.pass": *dbPass,
"db.ssl": *dbSSLMode,
"amqp.host": *amqpHost,
"amqp.user": *amqpUser,
"amqp.pass": *amqpPass,
"mode": *mode,
"port": *port,
"base.host": *baseHost,
"base.path": *basePath,
"admin.user": *adminUser,
"admin.pass": *adminPass,
"admin.mail": *adminMail,
"db.host": *dbHost,
"db.name": *dbName,
"db.user": *dbUser,
"db.pass": *dbPass,
"db.ssl": *dbSSLMode,
"amqp.host": *amqpHost,
"amqp.user": *amqpUser,
"amqp.pass": *amqpPass,
"mode": *mode,
"port": *port,
"base.host": *baseHost,
"base.path": *basePath,
"admin.user": *adminUser,
"admin.pass": *adminPass,
"admin.mail": *adminMail,
"s3.bucket": *s3Bucket,
"s3.endpoint": *s3Endpoint,
"s3.region": *s3Region,
}
if *s3NoSSL == true {
static["s3.nossl"] = "true"
} else {
static["s3.nossl"] = "false"
}
if *s3PathStyle == true {
static["s3.pathstyle"] = "true"
} else {
static["s3.pathstyle"] = "false"
}
mappings := map[string]string{
"DB_HOST": "db.host",
"DB_NAME": "db.name",
"DB_USER": "db.user",
"DB_PASS": "db.pass",
"DB_SSLMODE": "db.ssl",
"AMQP_HOST": "amqp.host",
"AMQP_USER": "amqp.user",
"AMQP_PASS": "amqp.pass",
"BASE_HOST": "base.host",
"BASE_PATH": "base.path",
"MODE": "mode",
"PORT": "port",
"ADMIN_USER": "admin.user",
"ADMIN_PASS": "admin.pass",
"ADMIN_MAIL": "admin.mail",
"DB_HOST": "db.host",
"DB_NAME": "db.name",
"DB_USER": "db.user",
"DB_PASS": "db.pass",
"DB_SSLMODE": "db.ssl",
"AMQP_HOST": "amqp.host",
"AMQP_USER": "amqp.user",
"AMQP_PASS": "amqp.pass",
"BASE_HOST": "base.host",
"BASE_PATH": "base.path",
"MODE": "mode",
"PORT": "port",
"ADMIN_USER": "admin.user",
"ADMIN_PASS": "admin.pass",
"ADMIN_MAIL": "admin.mail",
"S3_BUCKET": "s3.bucket",
"S3_ENDPOINT": "s3.endpoint",
"S3_REGION": "s3.region",
"S3_NOSSL": "s3.nossl",
"S3_PATHSTYLE": "s3.pathstyle",
}
defaults := config.NewStatic(static)

View file

@ -22,9 +22,10 @@
package database
import (
"github.com/lib/pq"
"time"
"github.com/lib/pq"
"github.com/jinzhu/gorm/dialects/postgres"
)
@ -199,6 +200,8 @@ type File struct {
Model
// Name of file
Name string `json:"name" gorm:"not null"`
// Key of file in S3 bucket
Key string `json:"key"`
// Type of file (MIME type)
Type string `json:"type"`
// Size of file (in byte)

3
go.mod
View file

@ -2,6 +2,7 @@ module git.rwth-aachen.de/acs/public/villas/web-backend-go
require (
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751
github.com/aws/aws-sdk-go v1.35.35
github.com/chenjiandongx/ginprom v0.0.0-20191022035802-6f3da3c84986
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/gin-gonic/gin v1.4.0
@ -19,7 +20,7 @@ require (
github.com/swaggo/gin-swagger v1.2.0
github.com/swaggo/swag v1.6.3
github.com/zpatrick/go-config v0.0.0-20191104215613-50bc2709703f
golang.org/x/crypto v0.0.0-20191112222119-e1110fd1c708
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
gopkg.in/go-playground/validator.v9 v9.30.0
gopkg.in/ini.v1 v1.51.0 // indirect
)

19
go.sum
View file

@ -18,6 +18,8 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/aws/aws-sdk-go v1.35.35 h1:o/EbgEcIPWga7GWhJhb3tiaxqk4/goTdo5YEMdnVxgE=
github.com/aws/aws-sdk-go v1.35.35/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@ -105,6 +107,10 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M=
github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7 h1:KfgG9LzI+pYjr4xvmz/5H4FXjokeP+rlHLhv3iH62Fo=
@ -151,6 +157,7 @@ github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJ
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
@ -207,8 +214,8 @@ go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191112222119-e1110fd1c708 h1:pXVtWnwHkrWD9ru3sDxY/qFK/bfc0egRovX91EjWjf4=
golang.org/x/crypto v0.0.0-20191112222119-e1110fd1c708/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
@ -228,6 +235,8 @@ golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190611141213-3f473d35a33a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980 h1:dfGZHvZk057jK2MCeWus/TowKpJ8y4AmooUzdBSR9GU=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -248,10 +257,14 @@ golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190610200419-93c9922d18ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191010194322-b09406accb47 h1:/XfQ9z7ib8eEJX2hdgFTZJ/ntt0swNk5oYBziWeTCvY=
golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@ -288,6 +301,8 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWD
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View file

@ -25,11 +25,12 @@ import (
"bytes"
"encoding/json"
"fmt"
"github.com/gin-gonic/gin"
"github.com/nsf/jsondiff"
"log"
"net/http"
"net/http/httptest"
"github.com/gin-gonic/gin"
"github.com/nsf/jsondiff"
)
// data type used in testing
@ -169,8 +170,47 @@ func TestEndpoint(router *gin.Engine, token string, url string,
}
req.Header.Set("Content-Type", "application/json")
req.Header.Add("Authorization", "Bearer "+token)
router.ServeHTTP(w, req)
return handleRedirect(w, req)
}
func handleRedirect(w *httptest.ResponseRecorder, req *http.Request) (int, *bytes.Buffer, error) {
if w.Code == http.StatusMovedPermanently ||
w.Code == http.StatusFound ||
w.Code == http.StatusTemporaryRedirect ||
w.Code == http.StatusPermanentRedirect {
// Follow external redirect
redirURL, err := w.Result().Location()
if err != nil {
return 0, nil, fmt.Errorf("invalid location header")
}
log.Println("redirecting request to", redirURL.String())
req, err := http.NewRequest(req.Method, redirURL.String(), req.Body)
if err != nil {
return 0, nil, fmt.Errorf("handle redirect: failed to create new request: %v", err)
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return 0, nil, fmt.Errorf("handle redirect: failed to follow redirect: %v", err)
}
buf := new(bytes.Buffer)
_, err = buf.ReadFrom(resp.Body)
if err != nil {
return 0, nil, fmt.Errorf("handle redirect: failed to follow redirect: %v", err)
}
return resp.StatusCode, buf, nil
}
// No redirect
return w.Code, w.Body, nil
}

View file

@ -1,21 +1,99 @@
#!/bin/bash
kubectl -n services port-forward svc/postgres-postgresql 5432:5432 & PF1=$!
kubectl -n services port-forward svc/broker 5672:5672 & PF2=$!
set -e
NS=villas-demo
NS=${NS:-villas}
MK="minikube"
KCTL="kubectl -n${NS}"
HELM="helm -n${NS}"
# CHART="villas"
CHART="~/workspace/rwth/acs/public/catalogue/charts/villas"
POSTGRES_USER=$(kubectl -n ${NS} get secret postgres-credentials -o json | jq -r .data.username | base64 -d)
POSTGRES_PASS=$(kubectl -n ${NS} get secret postgres-credentials -o json | jq -r .data.password | base64 -d)
RABBITMQ_USER=$(kubectl -n ${NS} get secret postgres-credentials -o json | jq -r .data.username | base64 -d)
RABBITMQ_PASS=$(kubectl -n ${NS} get secret postgres-credentials -o json | jq -r .data.password | base64 -d)
CONFIG=$(mktemp)
NAME="villas"
go test ./routes/healthz -p 1 --args \
-mode test \
-db-host localhost -db-name villas \
-db-user ${POSTGRES_USER} -db-pass ${POSTGRES_PASS} \
-amqp-host localhost \
-amqp-user ${RABBITMQ_USER} -amqp-pass ${RABBITMQ_PASS}
function cleanup() {
kill $PF1 $PF2 $PF3
wait $PF1 $PF2 $PF3
kill $PF1 $PF2
wait $PF1 $PF2
rm ${CONFIG}
echo "Goodbye"
}
trap cleanup EXIT
if [ -n "${USE_MINIKUBE}" ]; then
MK_START_OPTS="--addons=ingress"
if [ $(uname -s) == Darwin ]; then
# https://github.com/kubernetes/minikube/issues/7332
MK_START_OPTS+="--vm=true"
fi
${MK} start ${MK_START_OPTS}
kubectl -n kube-system expose deployment ingress-nginx-controller --type=LoadBalancer || true
IP=$(minikube ip)
PORT=$(kubectl -n kube-system get service ingress-nginx-controller --output='jsonpath={.spec.ports[0].nodePort}')
# Add pseudo hostname to /etc/hosts
echo "Please provide your root password for modifiying /etc/hosts"
sudo sed -in "/^${IP}.*/d" /etc/hosts
echo "${IP} minikube" | sudo tee -a /etc/hosts
HELM_OPTS="--set web.backend.enabled=false \
--set web.backend.external.ip=${IP} \
--set web.backend.external.port=4000 \
--set web.admin.password=admin \
--set ingress.host=minikube"
# Check if chart has already been deployed before
if helm get values villas > /dev/null; then
RABBITMQ_ERLANG_COOKIE=$(kubectl get secret --namespace default villas-broker -o jsonpath="{.data.rabbitmq-erlang-cookie}" | base64 --decode)
RABBITMQ_PASSWORD=$(kubectl get secret --namespace default villas-broker -o jsonpath="{.data.rabbitmq-password}" | base64 --decode)
HELM_OPTS+=" --set broker.auth.password=${RABBITMQ_PASSWORD} --set broker.auth.erlangCookie=${RABBITMQ_ERLANG_COOKIE}"
fi
${HELM} upgrade \
${HELM_OPTS} \
--install \
--create-namespace \
--wait \
--repo https://packages.fein-aachen.org/helm/charts \
${NAME} ${CHART}
fi
# Get backend config from cluster
${KCTL} get cm ${NAME}-web -o 'jsonpath={.data.config\.yaml}' > ${CONFIG}
# Enable access of backend to db, broker and s3
export DB_HOST=localhost
export DB_PASS=$(${KCTL} get secret ${NAME}-database -o 'jsonpath={.data.postgresql-password}' | base64 -d)
export AMQP_HOST=localhost
export AMQP_PASS=$(${KCTL} get secret ${NAME}-broker -o 'jsonpath={.data.rabbitmq-password}' | base64 -d)
export AWS_ACCESS_KEY_ID=$(${KCTL} get secret ${NAME}-s3 -o 'jsonpath={.data.accesskey}' | base64 -d)
export AWS_SECRET_ACCESS_KEY=$(${KCTL} get secret ${NAME}-s3 -o 'jsonpath={.data.secretkey}' | base64 -d)
# Setup port forwards for backend
if [ -n "${USE_EXTERNAL_POSTGRESQL}" ]; then
kubectl -n postgresql port-forward svc/postgresql 5432:5432 & PF1=$!
else
${KCTL} port-forward svc/${NAME}-database 5432:5432 & PF1=$!
fi
${KCTL} port-forward svc/${NAME}-broker 5672:5672 & PF2=$!
sleep 2
if [ -n "${USE_MINIKUBE}" ]; then
# python -mwebbrowser http://minikube:${PORT}
echo
echo "==========================================="
echo "Access VILLASweb at http://minikube:${PORT}"
echo "==========================================="
echo
fi
go run start.go -config ${CONFIG}

View file

@ -97,14 +97,14 @@ func addFile(c *gin.Context) {
}
// Extract file from POST request form
file_header, err := c.FormFile("file")
fileHeader, err := c.FormFile("file")
if err != nil {
helper.BadRequestError(c, fmt.Sprintf("Get form error: %s", err.Error()))
return
}
var newFile File
err = newFile.Register(file_header, so.ID)
err = newFile.Register(fileHeader, so.ID)
if !helper.DBError(c, err) {
c.JSON(http.StatusOK, gin.H{"file": newFile.File})
}

View file

@ -22,8 +22,7 @@
package file
import (
"git.rwth-aachen.de/acs/public/villas/web-backend-go/routes/scenario"
"github.com/gin-gonic/gin"
"fmt"
"image"
_ "image/gif"
_ "image/jpeg"
@ -33,10 +32,13 @@ import (
"mime/multipart"
"net/http"
"path/filepath"
"strconv"
"strings"
"time"
"git.rwth-aachen.de/acs/public/villas/web-backend-go/configuration"
"git.rwth-aachen.de/acs/public/villas/web-backend-go/routes/scenario"
"github.com/gin-gonic/gin"
"git.rwth-aachen.de/acs/public/villas/web-backend-go/database"
)
@ -62,14 +64,20 @@ func (f *File) save() error {
func (f *File) download(c *gin.Context) error {
// create unique file name
filename := "file_" + strconv.FormatUint(uint64(f.ID), 10) + "_" + f.Name
// detect the content type of the file
contentType := http.DetectContentType(f.FileData)
//Seems this headers needed for some browsers (for example without this headers Chrome will download files as txt)
c.Header("Content-Description", "File Transfer")
c.Header("Content-Disposition", "attachment; filename="+filename)
c.Data(http.StatusOK, contentType, f.FileData)
if f.Key == "" {
// Seems this headers needed for some browsers (for example without this headers Chrome will download files as txt)
c.Header("Content-Description", "File Transfer")
c.Header("Content-Disposition", "attachment; filename="+f.Name)
c.Header("Expires", "")
c.Header("Cache-Control", "")
c.Data(http.StatusOK, f.Type, f.FileData)
} else {
url, err := f.getS3Url()
if err != nil {
return fmt.Errorf("failed to presign S3 request: %s", err)
}
c.Redirect(http.StatusFound, url)
}
return nil
}
@ -88,10 +96,20 @@ func (f *File) Register(fileHeader *multipart.FileHeader, scenarioID uint) error
if err != nil {
return err
}
f.FileData, err = ioutil.ReadAll(fileContent)
defer fileContent.Close()
bucket, err := configuration.GolbalConfig.String("s3.bucket")
if bucket == "" {
f.FileData, err = ioutil.ReadAll(fileContent)
f.Key = ""
} else {
err := f.putS3(fileContent)
if err != nil {
return fmt.Errorf("failed to upload to S3 bucket: %s", err)
}
log.Println("Saved new file in S3 object storage")
}
// Add image dimensions in case the file is an image
if strings.Contains(f.Type, "image") || strings.Contains(f.Type, "Image") {
// set the file reader back to the start of the file
@ -100,13 +118,13 @@ func (f *File) Register(fileHeader *multipart.FileHeader, scenarioID uint) error
imageConfig, _, err := image.DecodeConfig(fileContent)
if err != nil {
log.Println("Unable to decode image configuration: Dimensions of image file are not set: ", err)
} else {
f.ImageHeight = imageConfig.Height
f.ImageWidth = imageConfig.Width
return fmt.Errorf("unable to decode image configuration: Dimensions of image file are not set: %v", err)
}
f.ImageHeight = imageConfig.Height
f.ImageWidth = imageConfig.Width
} else {
log.Println("Error on setting file reader back to start of file, dimensions not updated:", err)
return fmt.Errorf("error on setting file reader back to start of file, dimensions not updated: %v", err)
}
}
@ -137,43 +155,57 @@ func (f *File) update(fileHeader *multipart.FileHeader) error {
if err != nil {
return err
}
fileData, err := ioutil.ReadAll(fileContent)
defer fileContent.Close()
fileType := fileHeader.Header.Get("Content-Type")
imageHeight := f.ImageHeight
imageWidth := f.ImageWidth
bucket, err := configuration.GolbalConfig.String("s3.bucket")
if bucket == "" {
f.FileData, err = ioutil.ReadAll(fileContent)
f.Key = ""
} else {
err := f.putS3(fileContent)
if err != nil {
return fmt.Errorf("failed to upload to S3 bucket: %s", err)
}
log.Println("Updated file in S3 object storage")
}
f.Type = fileHeader.Header.Get("Content-Type")
f.Size = uint(fileHeader.Size)
f.Date = time.Now().String()
f.Name = filepath.Base(fileHeader.Filename)
// Update image dimensions in case the file is an image
if strings.Contains(fileType, "image") || strings.Contains(fileType, "Image") {
if strings.Contains(f.Type, "image") || strings.Contains(f.Type, "Image") {
// set the file reader back to the start of the file
_, err := fileContent.Seek(0, 0)
if err == nil {
imageConfig, _, err := image.DecodeConfig(fileContent)
if err != nil {
log.Println("Unable to decode image configuration: Dimensions of image file are not updated.", err)
} else {
imageHeight = imageConfig.Height
imageWidth = imageConfig.Width
}
f.ImageHeight = imageConfig.Height
f.ImageWidth = imageConfig.Width
} else {
log.Println("Error on setting file reader back to start of file, dimensions not updated::", err)
}
} else {
imageWidth = 0
imageHeight = 0
f.ImageWidth = 0
f.ImageHeight = 0
}
// Add File object with parameters to DB
db := database.GetDB()
err = db.Model(f).Updates(map[string]interface{}{
"Size": uint(fileHeader.Size),
"FileData": fileData,
"Date": time.Now().String(),
"Name": filepath.Base(fileHeader.Filename),
"Type": fileType,
"ImageHeight": imageHeight,
"ImageWidth": imageWidth,
"Size": f.Size,
"FileData": f.FileData,
"Date": f.Date,
"Name": f.Name,
"Type": f.Type,
"ImageHeight": f.ImageHeight,
"ImageWidth": f.ImageWidth,
"Key": f.Key,
}).Error
return err
@ -189,6 +221,16 @@ func (f *File) Delete() error {
if err != nil {
return err
}
// delete file from s3 bucket
if f.Key != "" {
err = f.deleteS3()
if err != nil {
return err
}
log.Println("Deleted file in S3 object storage")
}
err = db.Model(&so).Association("Files").Delete(f).Error
if err != nil {
return err

159
routes/file/file_s3.go Normal file
View file

@ -0,0 +1,159 @@
/** File package, S3 uploads.
*
* @author Steffen Vogel <svogel2@eonerc.rwth-aachen.de>
* @copyright 2014-2019, Institute for Automation of Complex Power Systems, EONERC
* @license GNU General Public License (version 3)
*
* VILLASweb-backend-go
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*********************************************************************************/
package file
import (
"fmt"
"io"
"time"
"git.rwth-aachen.de/acs/public/villas/web-backend-go/configuration"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/s3"
"github.com/aws/aws-sdk-go/service/s3/s3manager"
"github.com/google/uuid"
)
// Global session
var s3Session *session.Session = nil
func getS3Session() (*session.Session, string, error) {
bucket, err := configuration.GolbalConfig.String("s3.bucket")
if err != nil || bucket == "" {
return nil, "", fmt.Errorf("no S3 bucket configured: %s", err)
}
if s3Session == nil {
var err error
s3Session, err = createS3Session()
if err != nil {
return nil, "", err
}
}
return s3Session, bucket, nil
}
func createS3Session() (*session.Session, error) {
endpoint, err := configuration.GolbalConfig.String("s3.endpoint")
region, err := configuration.GolbalConfig.String("s3.region")
pathStyle, err := configuration.GolbalConfig.Bool("s3.pathstyle")
nossl, err := configuration.GolbalConfig.Bool("s3.nossl")
sess, err := session.NewSession(
&aws.Config{
Region: aws.String(region),
Endpoint: aws.String(endpoint),
DisableSSL: aws.Bool(nossl),
S3ForcePathStyle: aws.Bool(pathStyle),
},
)
if err != nil {
return nil, fmt.Errorf("failed to create session: %s", err)
}
return sess, nil
}
func (f *File) putS3(fileContent io.Reader) error {
// The session the S3 Uploader will use
sess, bucket, err := getS3Session()
if err != nil {
return err
}
// Create an uploader with the session and default options
uploader := s3manager.NewUploader(sess)
f.Key = uuid.New().String()
f.FileData = nil
// Upload the file to S3.
_, err = uploader.Upload(&s3manager.UploadInput{
Bucket: aws.String(bucket),
Key: aws.String(f.Key),
Body: fileContent,
})
if err != nil {
return fmt.Errorf("failed to upload file, %v", err)
}
return nil
}
func (f *File) getS3Url() (string, error) {
// The session the S3 Uploader will use
sess, bucket, err := getS3Session()
if err != nil {
return "", err
}
// Create S3 service client
svc := s3.New(sess)
req, _ := svc.GetObjectRequest(&s3.GetObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(f.Key),
ResponseContentType: aws.String(f.Type),
// ResponseContentDisposition: aws.String("attachment; filename=" + f.Name),
// ResponseContentEncoding: aws.String(),
// ResponseContentLanguage: aws.String(),
// ResponseCacheControl: aws.String(),
// ResponseExpires: aws.String(),
})
urlStr, err := req.Presign(5 * 24 * 60 * time.Minute)
if err != nil {
return "", err
}
return urlStr, nil
}
func (f *File) deleteS3() error {
// The session the S3 Uploader will use
sess, bucket, err := getS3Session()
if err != nil {
return err
}
// Create S3 service client
svc := s3.New(sess)
_, err = svc.DeleteObject(&s3.DeleteObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(f.Key),
})
if err != nil {
return err
}
f.Key = ""
return nil
}