Use Go 1.13
This commit is contained in:
parent
6a50b1b1c7
commit
0a81f4a031
14 changed files with 84 additions and 84 deletions
|
@ -1,9 +1,11 @@
|
||||||
linters:
|
linters:
|
||||||
enable-all: true
|
enable-all: true
|
||||||
disable:
|
disable:
|
||||||
- lll
|
|
||||||
- dupl
|
- dupl
|
||||||
|
- funlen
|
||||||
|
- lll
|
||||||
- scopelint
|
- scopelint
|
||||||
|
- wsl
|
||||||
|
|
||||||
linters-settings:
|
linters-settings:
|
||||||
govet:
|
govet:
|
||||||
|
@ -12,6 +14,3 @@ linters-settings:
|
||||||
suggest-new: true
|
suggest-new: true
|
||||||
misspell:
|
misspell:
|
||||||
locale: US
|
locale: US
|
||||||
gocritic:
|
|
||||||
disabled-checks:
|
|
||||||
- unlambda # Until https://github.com/go-critic/go-critic/issues/716 is fixed
|
|
||||||
|
|
17
Makefile
17
Makefile
|
@ -1,23 +1,28 @@
|
||||||
.PHONY: all lint test build-docker deploy-cf clean
|
.PHONY: build
|
||||||
|
build:
|
||||||
.EXPORT_ALL_VARIABLES:
|
|
||||||
GO111MODULE = on
|
|
||||||
|
|
||||||
all:
|
|
||||||
go build -o bin/s3manager ./cmd/s3manager
|
go build -o bin/s3manager ./cmd/s3manager
|
||||||
|
|
||||||
|
.PHONY: run
|
||||||
|
run:
|
||||||
|
go run cmd/s3manager/main.go
|
||||||
|
|
||||||
|
.PHONY: lint
|
||||||
lint:
|
lint:
|
||||||
golangci-lint run
|
golangci-lint run
|
||||||
|
|
||||||
|
.PHONY: test
|
||||||
test:
|
test:
|
||||||
go test -race -cover ./...
|
go test -race -cover ./...
|
||||||
|
|
||||||
|
.PHONY: build-docker
|
||||||
build-docker:
|
build-docker:
|
||||||
docker build -t s3manager .
|
docker build -t s3manager .
|
||||||
|
|
||||||
|
.PHONY: deploy-cf
|
||||||
deploy-cf:
|
deploy-cf:
|
||||||
GOOS=linux go build -ldflags="-s -w" -o bin/s3manager ./cmd/s3manager
|
GOOS=linux go build -ldflags="-s -w" -o bin/s3manager ./cmd/s3manager
|
||||||
cf push -f deployments/cf/manifest.yml
|
cf push -f deployments/cf/manifest.yml
|
||||||
|
|
||||||
|
.PHONY: clean
|
||||||
clean:
|
clean:
|
||||||
rm -rf bin
|
rm -rf bin
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
[![Go Report Card](https://goreportcard.com/badge/github.com/mastertinner/s3manager?style=flat-square)](https://goreportcard.com/report/github.com/mastertinner/s3manager)
|
[![Go Report Card](https://goreportcard.com/badge/github.com/mastertinner/s3manager?style=flat-square)](https://goreportcard.com/report/github.com/mastertinner/s3manager)
|
||||||
[![Build Status](https://img.shields.io/travis/mastertinner/s3manager.svg?style=flat-square)](https://travis-ci.org/mastertinner/s3manager)
|
[![Build Status](https://img.shields.io/travis/mastertinner/s3manager.svg?style=flat-square)](https://travis-ci.org/mastertinner/s3manager)
|
||||||
[![Docker Build](https://img.shields.io/docker/build/mastertinner/s3manager.svg?style=flat-square)](https://hub.docker.com/r/mastertinner/s3manager)
|
[![Docker Build](https://img.shields.io/docker/cloud/build/mastertinner/s3manager.svg?style=flat-square)](https://hub.docker.com/r/mastertinner/s3manager)
|
||||||
|
|
||||||
A Web GUI written in Go to manage S3 buckets from any provider.
|
A Web GUI written in Go to manage S3 buckets from any provider.
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
@ -10,7 +11,6 @@ import (
|
||||||
"github.com/mastertinner/s3manager/internal/app/s3manager"
|
"github.com/mastertinner/s3manager/internal/app/s3manager"
|
||||||
"github.com/matryer/way"
|
"github.com/matryer/way"
|
||||||
minio "github.com/minio/minio-go"
|
minio "github.com/minio/minio-go"
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
@ -18,14 +18,17 @@ func main() {
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Fatal("please provide ACCESS_KEY_ID")
|
log.Fatal("please provide ACCESS_KEY_ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
secretAccessKey, ok := os.LookupEnv("SECRET_ACCESS_KEY")
|
secretAccessKey, ok := os.LookupEnv("SECRET_ACCESS_KEY")
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Fatal("please provide SECRET_ACCESS_KEY")
|
log.Fatal("please provide SECRET_ACCESS_KEY")
|
||||||
}
|
}
|
||||||
|
|
||||||
port, ok := os.LookupEnv("PORT")
|
port, ok := os.LookupEnv("PORT")
|
||||||
if !ok {
|
if !ok {
|
||||||
port = "8080"
|
port = "8080"
|
||||||
}
|
}
|
||||||
|
|
||||||
endpoint, ok := os.LookupEnv("ENDPOINT")
|
endpoint, ok := os.LookupEnv("ENDPOINT")
|
||||||
if !ok {
|
if !ok {
|
||||||
endpoint = "s3.amazonaws.com"
|
endpoint = "s3.amazonaws.com"
|
||||||
|
@ -36,7 +39,7 @@ func main() {
|
||||||
// Set up S3 client
|
// Set up S3 client
|
||||||
s3, err := minio.New(endpoint, accessKeyID, secretAccessKey, true)
|
s3, err := minio.New(endpoint, accessKeyID, secretAccessKey, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln(errors.Wrap(err, "error creating s3 client"))
|
log.Fatalln(fmt.Errorf("error creating s3 client: %w", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up router
|
// Set up router
|
||||||
|
|
19
go.mod
19
go.mod
|
@ -1,20 +1,19 @@
|
||||||
module github.com/mastertinner/s3manager
|
module github.com/mastertinner/s3manager
|
||||||
|
|
||||||
go 1.12
|
go 1.13
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/go-ini/ini v1.46.0 // indirect
|
github.com/go-ini/ini v1.52.0 // indirect
|
||||||
github.com/gopherjs/gopherjs v0.0.0-20190812055157-5d271430af9f // indirect
|
github.com/gopherjs/gopherjs v0.0.0-20200209183636-89e6cbcd0b6d // indirect
|
||||||
github.com/mastertinner/adapters v0.0.0-20190721184605-a42866602363
|
github.com/mastertinner/adapters v0.0.0-20200131203008-495931e29440
|
||||||
github.com/matryer/is v1.2.0
|
github.com/matryer/is v1.2.0
|
||||||
github.com/matryer/way v0.0.0-20180416093233-9632d0c407b0
|
github.com/matryer/way v0.0.0-20180416093233-9632d0c407b0
|
||||||
github.com/minio/minio-go v6.0.14+incompatible
|
github.com/minio/minio-go v6.0.14+incompatible
|
||||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||||
github.com/pkg/errors v0.8.1
|
|
||||||
github.com/smartystreets/assertions v1.0.1 // indirect
|
github.com/smartystreets/assertions v1.0.1 // indirect
|
||||||
github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337 // indirect
|
github.com/smartystreets/goconvey v1.6.4 // indirect
|
||||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 // indirect
|
golang.org/x/crypto v0.0.0-20200214034016-1d94cc7ab1c6 // indirect
|
||||||
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 // indirect
|
golang.org/x/net v0.0.0-20200202094626-16171245cfb2 // indirect
|
||||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a // indirect
|
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4 // indirect
|
||||||
gopkg.in/ini.v1 v1.46.0 // indirect
|
gopkg.in/ini.v1 v1.52.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
52
go.sum
52
go.sum
|
@ -1,18 +1,18 @@
|
||||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||||
github.com/go-ini/ini v1.46.0 h1:hDJFfs/9f75875scvqLkhNB5Jz5/DybKEOZ5MLF+ng4=
|
github.com/go-ini/ini v1.52.0 h1:3UeUAveYUTCYV/G0jNDiIrrtIeAl1oAjshYyU2PaAlQ=
|
||||||
github.com/go-ini/ini v1.46.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
github.com/go-ini/ini v1.52.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||||
github.com/gopherjs/gopherjs v0.0.0-20190812055157-5d271430af9f h1:KMlcu9X58lhTA/KrfX8Bi1LQSO4pzoVjTiL3h4Jk+Zk=
|
github.com/gopherjs/gopherjs v0.0.0-20200209183636-89e6cbcd0b6d h1:vr95xIx8Eg3vCzZPxY3rCwTfkjqNDt/FgVqTOk0WByk=
|
||||||
github.com/gopherjs/gopherjs v0.0.0-20190812055157-5d271430af9f/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
github.com/gopherjs/gopherjs v0.0.0-20200209183636-89e6cbcd0b6d/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
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/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||||
github.com/mastertinner/adapters v0.0.0-20190721184605-a42866602363 h1:E6BWLMQ8LIKoBR5jdjUR4BhyGghJlDaL2kBR3tNLEb8=
|
github.com/mastertinner/adapters v0.0.0-20200131203008-495931e29440 h1:e44E87HFsnhpi9zhuMTwgfCPlUF41jO2uABeQKZ+TT0=
|
||||||
github.com/mastertinner/adapters v0.0.0-20190721184605-a42866602363/go.mod h1:1OWy7Ss0OIcoX6ND/EtxY5S3ECx6R4qGQQEEIfSSUXY=
|
github.com/mastertinner/adapters v0.0.0-20200131203008-495931e29440/go.mod h1:Ru6G6trKa1TIj3dY3oK1VFP/EeoF00S/A0UHhAUvHcM=
|
||||||
github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A=
|
github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A=
|
||||||
github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA=
|
github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA=
|
||||||
github.com/matryer/way v0.0.0-20180416093233-9632d0c407b0 h1:KWiqy3hl8yCUPAq1frD0DKXKyn7d9h2nVhj2r5ISq2o=
|
github.com/matryer/way v0.0.0-20180416093233-9632d0c407b0 h1:KWiqy3hl8yCUPAq1frD0DKXKyn7d9h2nVhj2r5ISq2o=
|
||||||
|
@ -21,44 +21,38 @@ github.com/minio/minio-go v6.0.14+incompatible h1:fnV+GD28LeqdN6vT2XdGKW8Qe/IfjJ
|
||||||
github.com/minio/minio-go v6.0.14+incompatible/go.mod h1:7guKYtitv8dktvNUGrhzmNlA5wrAABTQXCoesZdFQO8=
|
github.com/minio/minio-go v6.0.14+incompatible/go.mod h1:7guKYtitv8dktvNUGrhzmNlA5wrAABTQXCoesZdFQO8=
|
||||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
|
||||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||||
github.com/smartystreets/assertions v1.0.1 h1:voD4ITNjPL5jjBfgR/r8fPIIBrliWrWHeiJApdr3r4w=
|
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/assertions v1.0.1/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM=
|
||||||
github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337 h1:WN9BUFbdyOsSH/XohnWpXOlq9NBD5sGAB2FciQMUEe8=
|
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||||
github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5 h1:58fnuSXlxZmFdJyvtTFVmVhcMLU6v5fEb/ok4wyqtNU=
|
golang.org/x/crypto v0.0.0-20200214034016-1d94cc7ab1c6 h1:Sy5bstxEqwwbYs6n0/pBuxKENqOeZUgD45Gp3Q3pqLg=
|
||||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20200214034016-1d94cc7ab1c6/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
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-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
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/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||||
golang.org/x/net v0.0.0-20190628185345-da137c7871d7 h1:rTIdg5QFRR7XCaK4LCjBiPbx8j4DQRpdYMnGn/bJUEU=
|
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa h1:F+8P+gmewFQYRk6JoLQLwjBCTu3mcIURZfNkVweuRKA=
|
||||||
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 h1:fHDIZ2oxGnUZRN6WgWFCbYBjH9uqVPRCUVUDhs0wnbA=
|
golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI=
|
||||||
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
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/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c h1:+EXw7AwNOKzPFXMZ1yNjO40aWCh3PIquJB2fYlv9wcs=
|
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4 h1:sfkvUWPNGwSV+8/fNqctR5lS2AqCSqYwXdrjCxp/dXo=
|
||||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ=
|
|
||||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
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.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
|
||||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||||
gopkg.in/ini.v1 v1.46.0 h1:VeDZbLYGaupuvIrsYCEOe/L/2Pcs5n7hdO1ZTjporag=
|
gopkg.in/ini.v1 v1.52.0 h1:j+Lt/M1oPPejkniCg1TkWE2J3Eh1oZTsHSXzMTzUXn4=
|
||||||
gopkg.in/ini.v1 v1.46.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
gopkg.in/ini.v1 v1.52.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package s3manager
|
package s3manager
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path"
|
"path"
|
||||||
|
@ -8,7 +9,6 @@ import (
|
||||||
|
|
||||||
"github.com/matryer/way"
|
"github.com/matryer/way"
|
||||||
minio "github.com/minio/minio-go"
|
minio "github.com/minio/minio-go"
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// HandleBucketView shows the details page of a bucket.
|
// HandleBucketView shows the details page of a bucket.
|
||||||
|
@ -32,7 +32,7 @@ func HandleBucketView(s3 S3, tmplDir string) http.HandlerFunc {
|
||||||
objectCh := s3.ListObjectsV2(bucketName, "", true, doneCh)
|
objectCh := s3.ListObjectsV2(bucketName, "", true, doneCh)
|
||||||
for object := range objectCh {
|
for object := range objectCh {
|
||||||
if object.Err != nil {
|
if object.Err != nil {
|
||||||
handleHTTPError(w, errors.Wrap(object.Err, "error listing objects"))
|
handleHTTPError(w, fmt.Errorf("error listing objects: %w", object.Err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
obj := objectWithIcon{Info: object, Icon: icon(object.Key)}
|
obj := objectWithIcon{Info: object, Icon: icon(object.Key)}
|
||||||
|
@ -47,12 +47,12 @@ func HandleBucketView(s3 S3, tmplDir string) http.HandlerFunc {
|
||||||
p := filepath.Join(tmplDir, "bucket.html.tmpl")
|
p := filepath.Join(tmplDir, "bucket.html.tmpl")
|
||||||
t, err := template.ParseFiles(l, p)
|
t, err := template.ParseFiles(l, p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleHTTPError(w, errors.Wrap(err, "error parsing template files"))
|
handleHTTPError(w, fmt.Errorf("error parsing template files: %w", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = t.ExecuteTemplate(w, "layout", data)
|
err = t.ExecuteTemplate(w, "layout", data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleHTTPError(w, errors.Wrap(err, "error executing template"))
|
handleHTTPError(w, fmt.Errorf("error executing template: %w", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -69,5 +69,6 @@ func icon(fileName string) string {
|
||||||
case ".mp3", ".wav":
|
case ".mp3", ".wav":
|
||||||
return "music_note"
|
return "music_note"
|
||||||
}
|
}
|
||||||
|
|
||||||
return "insert_drive_file"
|
return "insert_drive_file"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
package s3manager
|
package s3manager
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// HandleBucketsView renders all buckets on an HTML page.
|
// HandleBucketsView renders all buckets on an HTML page.
|
||||||
|
@ -13,7 +12,7 @@ func HandleBucketsView(s3 S3, tmplDir string) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, _ *http.Request) {
|
return func(w http.ResponseWriter, _ *http.Request) {
|
||||||
buckets, err := s3.ListBuckets()
|
buckets, err := s3.ListBuckets()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleHTTPError(w, errors.Wrap(err, "error listing buckets"))
|
handleHTTPError(w, fmt.Errorf("error listing buckets: %w", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,12 +20,12 @@ func HandleBucketsView(s3 S3, tmplDir string) http.HandlerFunc {
|
||||||
p := filepath.Join(tmplDir, "buckets.html.tmpl")
|
p := filepath.Join(tmplDir, "buckets.html.tmpl")
|
||||||
t, err := template.ParseFiles(l, p)
|
t, err := template.ParseFiles(l, p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleHTTPError(w, errors.Wrap(err, "error parsing template files"))
|
handleHTTPError(w, fmt.Errorf("error parsing template files: %w", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = t.ExecuteTemplate(w, "layout", buckets)
|
err = t.ExecuteTemplate(w, "layout", buckets)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleHTTPError(w, errors.Wrap(err, "error executing template"))
|
handleHTTPError(w, fmt.Errorf("error executing template: %w", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,10 +2,10 @@ package s3manager
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
minio "github.com/minio/minio-go"
|
minio "github.com/minio/minio-go"
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// HandleCreateBucket creates a new bucket.
|
// HandleCreateBucket creates a new bucket.
|
||||||
|
@ -14,13 +14,13 @@ func HandleCreateBucket(s3 S3) http.HandlerFunc {
|
||||||
var bucket minio.BucketInfo
|
var bucket minio.BucketInfo
|
||||||
err := json.NewDecoder(r.Body).Decode(&bucket)
|
err := json.NewDecoder(r.Body).Decode(&bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleHTTPError(w, errors.Wrap(err, "error decoding body JSON"))
|
handleHTTPError(w, fmt.Errorf("error decoding body JSON: %w", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = s3.MakeBucket(bucket.Name, "")
|
err = s3.MakeBucket(bucket.Name, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleHTTPError(w, errors.Wrap(err, "error making bucket"))
|
handleHTTPError(w, fmt.Errorf("error making bucket: %w", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,7 +28,7 @@ func HandleCreateBucket(s3 S3) http.HandlerFunc {
|
||||||
w.WriteHeader(http.StatusCreated)
|
w.WriteHeader(http.StatusCreated)
|
||||||
err = json.NewEncoder(w).Encode(bucket)
|
err = json.NewEncoder(w).Encode(bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleHTTPError(w, errors.Wrap(err, "error encoding JSON"))
|
handleHTTPError(w, fmt.Errorf("error encoding JSON: %w", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
package s3manager
|
package s3manager
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/matryer/way"
|
"github.com/matryer/way"
|
||||||
minio "github.com/minio/minio-go"
|
minio "github.com/minio/minio-go"
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// HandleCreateObject uploads a new object.
|
// HandleCreateObject uploads a new object.
|
||||||
|
@ -13,14 +13,14 @@ func HandleCreateObject(s3 S3) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
bucketName := way.Param(r.Context(), "bucketName")
|
bucketName := way.Param(r.Context(), "bucketName")
|
||||||
|
|
||||||
err := r.ParseMultipartForm(32 << 20)
|
err := r.ParseMultipartForm(0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleHTTPError(w, errors.Wrap(err, "error parsing multipart form"))
|
handleHTTPError(w, fmt.Errorf("error parsing multipart form: %w", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
file, handler, err := r.FormFile("file")
|
file, handler, err := r.FormFile("file")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleHTTPError(w, errors.Wrap(err, "error getting file from form"))
|
handleHTTPError(w, fmt.Errorf("error getting file from form: %w", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
@ -28,7 +28,7 @@ func HandleCreateObject(s3 S3) http.HandlerFunc {
|
||||||
opts := minio.PutObjectOptions{ContentType: "application/octet-stream"}
|
opts := minio.PutObjectOptions{ContentType: "application/octet-stream"}
|
||||||
_, err = s3.PutObject(bucketName, handler.Filename, file, 1, opts)
|
_, err = s3.PutObject(bucketName, handler.Filename, file, 1, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleHTTPError(w, errors.Wrap(err, "error putting object"))
|
handleHTTPError(w, fmt.Errorf("error putting object: %w", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
package s3manager
|
package s3manager
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/matryer/way"
|
"github.com/matryer/way"
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// HandleDeleteBucket deletes a bucket.
|
// HandleDeleteBucket deletes a bucket.
|
||||||
|
@ -14,7 +14,7 @@ func HandleDeleteBucket(s3 S3) http.HandlerFunc {
|
||||||
|
|
||||||
err := s3.RemoveBucket(bucketName)
|
err := s3.RemoveBucket(bucketName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleHTTPError(w, errors.Wrap(err, "error removing bucket"))
|
handleHTTPError(w, fmt.Errorf("error removing bucket: %w", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
package s3manager
|
package s3manager
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/matryer/way"
|
"github.com/matryer/way"
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// HandleDeleteObject deletes an object.
|
// HandleDeleteObject deletes an object.
|
||||||
|
@ -15,7 +15,7 @@ func HandleDeleteObject(s3 S3) http.HandlerFunc {
|
||||||
|
|
||||||
err := s3.RemoveObject(bucketName, objectName)
|
err := s3.RemoveObject(bucketName, objectName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleHTTPError(w, errors.Wrap(err, "error removing object"))
|
handleHTTPError(w, fmt.Errorf("error removing object: %w", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,11 +2,11 @@ package s3manager
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Error codes that may be returned from an S3 client.
|
// Error codes that may be returned from an S3 client.
|
||||||
|
@ -17,24 +17,25 @@ const (
|
||||||
|
|
||||||
// handleHTTPError handles HTTP errors.
|
// handleHTTPError handles HTTP errors.
|
||||||
func handleHTTPError(w http.ResponseWriter, err error) {
|
func handleHTTPError(w http.ResponseWriter, err error) {
|
||||||
cause := errors.Cause(err)
|
|
||||||
code := http.StatusInternalServerError
|
code := http.StatusInternalServerError
|
||||||
|
|
||||||
_, ok := cause.(*json.SyntaxError)
|
var se *json.SyntaxError
|
||||||
|
ok := errors.As(err, &se)
|
||||||
if ok {
|
if ok {
|
||||||
code = http.StatusUnprocessableEntity
|
code = http.StatusUnprocessableEntity
|
||||||
}
|
}
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case cause == io.EOF || cause == io.ErrUnexpectedEOF:
|
case errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF):
|
||||||
code = http.StatusUnprocessableEntity
|
code = http.StatusUnprocessableEntity
|
||||||
case cause.Error() == ErrBucketDoesNotExist || cause.Error() == ErrKeyDoesNotExist:
|
case strings.Contains(err.Error(), ErrBucketDoesNotExist) || strings.Contains(err.Error(), ErrKeyDoesNotExist):
|
||||||
code = http.StatusNotFound
|
code = http.StatusNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
http.Error(w, http.StatusText(code), code)
|
http.Error(w, http.StatusText(code), code)
|
||||||
|
|
||||||
// Log if server error
|
// Log if server error
|
||||||
if code >= 500 {
|
if code >= http.StatusInternalServerError {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,6 @@ import (
|
||||||
|
|
||||||
"github.com/matryer/way"
|
"github.com/matryer/way"
|
||||||
minio "github.com/minio/minio-go"
|
minio "github.com/minio/minio-go"
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// HandleGetObject downloads an object to the client.
|
// HandleGetObject downloads an object to the client.
|
||||||
|
@ -18,7 +17,7 @@ func HandleGetObject(s3 S3) http.HandlerFunc {
|
||||||
|
|
||||||
object, err := s3.GetObject(bucketName, objectName, minio.GetObjectOptions{})
|
object, err := s3.GetObject(bucketName, objectName, minio.GetObjectOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleHTTPError(w, errors.Wrap(err, "error getting object"))
|
handleHTTPError(w, fmt.Errorf("error getting object: %w", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,7 +25,7 @@ func HandleGetObject(s3 S3) http.HandlerFunc {
|
||||||
w.Header().Set("Content-Type", "application/octet-stream")
|
w.Header().Set("Content-Type", "application/octet-stream")
|
||||||
_, err = io.Copy(w, object)
|
_, err = io.Copy(w, object)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleHTTPError(w, errors.Wrap(err, "error copying object to response writer"))
|
handleHTTPError(w, fmt.Errorf("error copying object to response writer: %w", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue