First checkpoint
This commit is contained in:
commit
b08e71b3bb
7
.env.example
Normal file
7
.env.example
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
PORT: 8080
|
||||||
|
DB_URL: postgres://gis:gis@postgres:5432/gis?sslmode=disable
|
||||||
|
S3_ENDPOINT: minio:9000
|
||||||
|
S3_ACCESS_KEY: minioadmin
|
||||||
|
S3_SECRET_KEY: minioadmin
|
||||||
|
S3_BUCKET: geofiles
|
||||||
|
S3_USE_SSL: "false"
|
||||||
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
.env
|
||||||
|
.claude
|
||||||
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
17
.idea/dataSources.xml
generated
Normal file
17
.idea/dataSources.xml
generated
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||||
|
<data-source source="LOCAL" name="gis@localhost" uuid="0b44c99f-aa96-4dad-ad42-9bac71e16d4c">
|
||||||
|
<driver-ref>postgresql</driver-ref>
|
||||||
|
<synchronize>true</synchronize>
|
||||||
|
<jdbc-driver>org.postgresql.Driver</jdbc-driver>
|
||||||
|
<jdbc-url>jdbc:postgresql://localhost:5432/gis</jdbc-url>
|
||||||
|
<jdbc-additional-properties>
|
||||||
|
<property name="com.intellij.clouds.kubernetes.db.host.port" />
|
||||||
|
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
|
||||||
|
<property name="com.intellij.clouds.kubernetes.db.container.port" />
|
||||||
|
</jdbc-additional-properties>
|
||||||
|
<working-dir>$ProjectFileDir$</working-dir>
|
||||||
|
</data-source>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
9
.idea/gis.iml
generated
Normal file
9
.idea/gis.iml
generated
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="WEB_MODULE" version="4">
|
||||||
|
<component name="Go" enabled="true" />
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/gis.iml" filepath="$PROJECT_DIR$/.idea/gis.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
7
.idea/sqldialects.xml
generated
Normal file
7
.idea/sqldialects.xml
generated
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="SqlDialectMappings">
|
||||||
|
<file url="file://$PROJECT_DIR$/migrations/20260604134433_create_datasets_table.sql" dialect="GenericSQL" />
|
||||||
|
<file url="PROJECT" dialect="PostgreSQL" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
21
Dockerfile
Normal file
21
Dockerfile
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
FROM golang:1.26.1-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux go build -o /gis .
|
||||||
|
|
||||||
|
FROM alpine:3.20
|
||||||
|
|
||||||
|
RUN apk add --no-cache ca-certificates tzdata
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=builder /gis .
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
ENTRYPOINT ["/app/gis"]
|
||||||
|
CMD ["serve"]
|
||||||
34
app/config.go
Normal file
34
app/config.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/caarlos0/env/v11"
|
||||||
|
"github.com/joho/godotenv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Port int `env:"PORT" envDefault:"8080"`
|
||||||
|
DBURL string `env:"DB_URL"`
|
||||||
|
|
||||||
|
S3Endpoint string `env:"S3_ENDPOINT"`
|
||||||
|
S3AccessKey string `env:"S3_ACCESS_KEY"`
|
||||||
|
S3SecretKey string `env:"S3_SECRET_KEY"`
|
||||||
|
S3Bucket string `env:"S3_BUCKET" envDefault:"geofiles"`
|
||||||
|
S3UseSSL bool `env:"S3_USE_SSL" envDefault:"false"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadConfig() (*Config, error) {
|
||||||
|
|
||||||
|
if err := godotenv.Load(); err != nil {
|
||||||
|
log.Println("No .env file found, relying on system env")
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := &Config{}
|
||||||
|
|
||||||
|
if err := env.Parse(cfg); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
29
app/database.go
Normal file
29
app/database.go
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Store struct {
|
||||||
|
pool *pgxpool.Pool
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDB(ctx context.Context, cfg *Config) (*Store, error) {
|
||||||
|
pool, err := pgxpool.New(ctx, cfg.DBURL)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := pool.Ping(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Store{pool: pool}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) closeDB() {
|
||||||
|
s.pool.Close()
|
||||||
|
}
|
||||||
44
app/init.go
Normal file
44
app/init.go
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
"github.com/minio/minio-go/v7"
|
||||||
|
)
|
||||||
|
|
||||||
|
type App struct {
|
||||||
|
Ctx context.Context
|
||||||
|
Cfg *Config
|
||||||
|
Db *pgxpool.Pool
|
||||||
|
S3 *minio.Client
|
||||||
|
Validator *validator.Validate
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewApp(ctx context.Context) *App {
|
||||||
|
cfg, err := loadConfig()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
db, err := newDB(ctx, cfg)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s3, err := newS3Client(ctx, cfg)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &App{
|
||||||
|
Ctx: ctx,
|
||||||
|
Cfg: cfg,
|
||||||
|
Db: db.pool,
|
||||||
|
S3: s3,
|
||||||
|
Validator: validator.New(validator.WithRequiredStructEnabled()),
|
||||||
|
}
|
||||||
|
}
|
||||||
31
app/storage.go
Normal file
31
app/storage.go
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/minio/minio-go/v7"
|
||||||
|
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newS3Client(ctx context.Context, cfg *Config) (*minio.Client, error) {
|
||||||
|
client, err := minio.New(cfg.S3Endpoint, &minio.Options{
|
||||||
|
Creds: credentials.NewStaticV4(cfg.S3AccessKey, cfg.S3SecretKey, ""),
|
||||||
|
Secure: cfg.S3UseSSL,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("s3 client: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
exists, err := client.BucketExists(ctx, cfg.S3Bucket)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("s3 bucket check: %w", err)
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
|
if err := client.MakeBucket(ctx, cfg.S3Bucket, minio.MakeBucketOptions{}); err != nil {
|
||||||
|
return nil, fmt.Errorf("s3 make bucket: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
23
cmd/root.go
Normal file
23
cmd/root.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var rootCmd = &cobra.Command{
|
||||||
|
Use: "gis",
|
||||||
|
Short: "Microservices for parsing geo files to geojson",
|
||||||
|
}
|
||||||
|
|
||||||
|
func Execute() {
|
||||||
|
err := rootCmd.Execute()
|
||||||
|
if err != nil {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.gis.yaml)")
|
||||||
|
}
|
||||||
72
cmd/serve.go
Normal file
72
cmd/serve.go
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"gis/app"
|
||||||
|
"gis/server"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// serveCmd represents the serve command
|
||||||
|
var serveCmd = &cobra.Command{
|
||||||
|
Use: "serve",
|
||||||
|
Short: "Serve HTTP server",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
application := app.NewApp(cmd.Context())
|
||||||
|
|
||||||
|
srv := &http.Server{
|
||||||
|
Addr: fmt.Sprintf(":%d", application.Cfg.Port),
|
||||||
|
Handler: server.AppRouter(application),
|
||||||
|
ReadHeaderTimeout: 5 * time.Second,
|
||||||
|
ReadTimeout: 120 * time.Second,
|
||||||
|
WriteTimeout: 120 * time.Second,
|
||||||
|
IdleTimeout: 60 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
idleClosed := make(chan struct{})
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
sigint := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigint, os.Interrupt, syscall.SIGTERM)
|
||||||
|
<-sigint
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := srv.Shutdown(ctx); err != nil {
|
||||||
|
log.Printf("shutdown server error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
close(idleClosed)
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
|
log.Fatalf("listen: %s\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
<-idleClosed
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(serveCmd)
|
||||||
|
|
||||||
|
// Here you will define your flags and configuration settings.
|
||||||
|
|
||||||
|
// Cobra supports Persistent Flags which will work for this command
|
||||||
|
// and all subcommands, e.g.:
|
||||||
|
// serveCmd.PersistentFlags().String("foo", "", "A help for foo")
|
||||||
|
|
||||||
|
// Cobra supports local flags which will only run when this command
|
||||||
|
// is called directly, e.g.:
|
||||||
|
// serveCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
|
||||||
|
}
|
||||||
58
docker-compose.yml
Normal file
58
docker-compose.yml
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
services:
|
||||||
|
app:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
environment:
|
||||||
|
PORT: 8080
|
||||||
|
DB_URL: postgres://gis:gis@postgres:5432/gis?sslmode=disable
|
||||||
|
S3_ENDPOINT: minio:9000
|
||||||
|
S3_ACCESS_KEY: minioadmin
|
||||||
|
S3_SECRET_KEY: minioadmin
|
||||||
|
S3_BUCKET: geofiles
|
||||||
|
S3_USE_SSL: "false"
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
minio:
|
||||||
|
condition: service_healthy
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
postgres:
|
||||||
|
image: postgres:17
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: gis
|
||||||
|
POSTGRES_PASSWORD: gis
|
||||||
|
POSTGRES_DB: gis
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U gis -d gis"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
minio:
|
||||||
|
image: minio/minio:latest
|
||||||
|
command: server /data --console-address ":9001"
|
||||||
|
ports:
|
||||||
|
- "9000:9000"
|
||||||
|
- "9001:9001"
|
||||||
|
environment:
|
||||||
|
MINIO_ROOT_USER: minioadmin
|
||||||
|
MINIO_ROOT_PASSWORD: minioadmin
|
||||||
|
volumes:
|
||||||
|
- minio_data:/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "mc", "ready", "local"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
minio_data:
|
||||||
44
go.mod
Normal file
44
go.mod
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
module gis
|
||||||
|
|
||||||
|
go 1.26.1
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/caarlos0/env/v11 v11.4.1
|
||||||
|
github.com/go-playground/validator/v10 v10.30.3
|
||||||
|
github.com/jackc/pgx/v5 v5.10.0
|
||||||
|
github.com/joho/godotenv v1.5.1
|
||||||
|
github.com/minio/minio-go/v7 v7.2.0
|
||||||
|
github.com/spf13/cobra v1.10.2
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
||||||
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
|
github.com/klauspost/compress v1.18.6 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.11 // indirect
|
||||||
|
github.com/klauspost/crc32 v1.3.0 // indirect
|
||||||
|
github.com/kr/text v0.2.0 // indirect
|
||||||
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
|
github.com/minio/crc64nvme v1.1.1 // indirect
|
||||||
|
github.com/minio/md5-simd v1.1.2 // indirect
|
||||||
|
github.com/philhofer/fwd v1.2.0 // indirect
|
||||||
|
github.com/rs/xid v1.6.0 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.10 // indirect
|
||||||
|
github.com/tinylib/msgp v1.6.1 // indirect
|
||||||
|
github.com/zeebo/xxh3 v1.1.0 // indirect
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
|
golang.org/x/crypto v0.52.0 // indirect
|
||||||
|
golang.org/x/net v0.54.0 // indirect
|
||||||
|
golang.org/x/sync v0.20.0 // indirect
|
||||||
|
golang.org/x/sys v0.45.0 // indirect
|
||||||
|
golang.org/x/text v0.37.0 // indirect
|
||||||
|
gopkg.in/ini.v1 v1.67.2 // indirect
|
||||||
|
)
|
||||||
105
go.sum
Normal file
105
go.sum
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
github.com/caarlos0/env/v11 v11.4.1 h1:fYwH0sWEsBSMPG7t4e/PEfTFzrWrpjyygXyUnWiSwEw=
|
||||||
|
github.com/caarlos0/env/v11 v11.4.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
|
github.com/go-playground/validator/v10 v10.30.3 h1:4MU6YkEwx7GbcPJOZxrtbu+QfF3pJLJuaYTeAH0DYy8=
|
||||||
|
github.com/go-playground/validator/v10 v10.30.3/go.mod h1:4Axh7oCNGcoGkqLoE4YWt6n20mcEIsPRlB7vPk3lpyc=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||||
|
github.com/jackc/pgx/v5 v5.10.0 h1:VhSvgU2jSli8o3AqIEOTJr7rZwAEUVo4E4XhR94Zfr0=
|
||||||
|
github.com/jackc/pgx/v5 v5.10.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
||||||
|
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||||
|
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||||
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
|
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
|
||||||
|
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.11 h1:0OwqZRYI2rFrjS4kvkDnqJkKHdHaRnCm68/DY4OxRzU=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.11/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
|
github.com/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM=
|
||||||
|
github.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw=
|
||||||
|
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||||
|
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
|
github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI=
|
||||||
|
github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
|
||||||
|
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
|
||||||
|
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
|
||||||
|
github.com/minio/minio-go/v7 v7.2.0 h1:RCJM0R1XOsRs+A3x3UCaf3ZYbByDaLjFeAi+YCQEPhs=
|
||||||
|
github.com/minio/minio-go/v7 v7.2.0/go.mod h1:EU9hENAStx/xXduNdrGO5e4X5vk19NtgB+RIPjZO8o0=
|
||||||
|
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
||||||
|
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
||||||
|
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/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
|
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
||||||
|
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||||
|
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||||
|
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||||
|
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
github.com/tinylib/msgp v1.6.1 h1:ESRv8eL3u+DNHUoSAAQRE50Hm162zqAnBoGv9PzScPY=
|
||||||
|
github.com/tinylib/msgp v1.6.1/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
|
||||||
|
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
|
||||||
|
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
||||||
|
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
|
||||||
|
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
|
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
|
||||||
|
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
|
||||||
|
golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
|
||||||
|
golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
|
||||||
|
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||||
|
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
|
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
||||||
|
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
|
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||||
|
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/ini.v1 v1.67.2 h1:JtOSMb9OuaCZKr7h5D/h6iii14sK0hLbplTc6frx4Ss=
|
||||||
|
gopkg.in/ini.v1 v1.67.2/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
11
main.go
Normal file
11
main.go
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
/*
|
||||||
|
Copyright © 2026 NAME HERE <EMAIL ADDRESS>
|
||||||
|
|
||||||
|
*/
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "gis/cmd"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cmd.Execute()
|
||||||
|
}
|
||||||
11
migrations/20260604134433_create_datasets_table.sql
Normal file
11
migrations/20260604134433_create_datasets_table.sql
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
-- +goose Up
|
||||||
|
CREATE TABLE datasets (
|
||||||
|
id UUID PRIMARY KEY default gen_random_uuid(),
|
||||||
|
name varchar(255),
|
||||||
|
description text,
|
||||||
|
created_at timestamp default now(),
|
||||||
|
updated_at timestamp default now()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
DROP TABLE datasets;
|
||||||
12
migrations/20260604141221_create_categories_table.sql
Normal file
12
migrations/20260604141221_create_categories_table.sql
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
-- +goose Up
|
||||||
|
CREATE TABLE categories
|
||||||
|
(
|
||||||
|
id UUID PRIMARY KEY default gen_random_uuid(),
|
||||||
|
name varchar(255),
|
||||||
|
description text,
|
||||||
|
created_at timestamp default now(),
|
||||||
|
updated_at timestamp default now()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
DROP TABLE categories;
|
||||||
22
migrations/20260604141656_create_files_table.sql
Normal file
22
migrations/20260604141656_create_files_table.sql
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
-- +goose Up
|
||||||
|
CREATE TYPE file_type AS ENUM ('vector_with_table', 'vector', 'raster');
|
||||||
|
CREATE TYPE file_validation_status AS ENUM ('pending', 'valid', 'failed');
|
||||||
|
|
||||||
|
CREATE TABLE files (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
filename VARCHAR(255) NOT NULL,
|
||||||
|
storage_key TEXT NOT NULL,
|
||||||
|
file_type file_type NOT NULL,
|
||||||
|
validation_status file_validation_status NOT NULL DEFAULT 'pending',
|
||||||
|
validation_error TEXT,
|
||||||
|
kato_column VARCHAR(255),
|
||||||
|
crs VARCHAR(64),
|
||||||
|
feature_count INTEGER,
|
||||||
|
uploaded_at TIMESTAMP NOT NULL DEFAULT now(),
|
||||||
|
updated_at TIMESTAMP NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
DROP TABLE files;
|
||||||
|
DROP TYPE file_validation_status;
|
||||||
|
DROP TYPE file_type;
|
||||||
38
server/categories/create.go
Normal file
38
server/categories/create.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package categories
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gis/app"
|
||||||
|
"gis/server/httputil"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CreateCategoryRequest struct {
|
||||||
|
Name string `json:"name" validate:"required,max=255"`
|
||||||
|
Description string `json:"description" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func createCategoryRoute(application *app.App) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
req, err := httputil.DecodeJSON[CreateCategoryRequest](w, r)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Invalid request", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := application.Validator.Struct(req); err != nil {
|
||||||
|
httputil.WriteValidationErrors(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = application.Db.Exec(application.Ctx,
|
||||||
|
"INSERT INTO categories (name, description) VALUES ($1, $2)",
|
||||||
|
req.Name, req.Description,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
}
|
||||||
|
}
|
||||||
27
server/categories/delete.go
Normal file
27
server/categories/delete.go
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package categories
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gis/app"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func deleteCategoryRoute(application *app.App) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id := r.PathValue("id")
|
||||||
|
|
||||||
|
tag, err := application.Db.Exec(application.Ctx,
|
||||||
|
"DELETE FROM categories WHERE id=$1",
|
||||||
|
id,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if tag.RowsAffected() == 0 {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
39
server/categories/index.go
Normal file
39
server/categories/index.go
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package categories
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"gis/app"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func listCategoriesRoute(application *app.App) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
rows, err := application.Db.Query(application.Ctx, "SELECT id, name FROM categories")
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
cats := make([]Category, 0)
|
||||||
|
for rows.Next() {
|
||||||
|
var c Category
|
||||||
|
if err := rows.Scan(&c.ID, &c.Name); err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cats = append(cats, c)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.NewEncoder(w).Encode(map[string][]Category{"data": cats}); err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
server/categories/routes.go
Normal file
13
server/categories/routes.go
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package categories
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gis/app"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func AddCategoriesRoutes(application *app.App, mux *http.ServeMux) {
|
||||||
|
mux.HandleFunc("GET /categories", listCategoriesRoute(application))
|
||||||
|
mux.HandleFunc("POST /categories", createCategoryRoute(application))
|
||||||
|
mux.HandleFunc("PUT /categories/{id}", updateCategoryRoute(application))
|
||||||
|
mux.HandleFunc("DELETE /categories/{id}", deleteCategoryRoute(application))
|
||||||
|
}
|
||||||
6
server/categories/types.go
Normal file
6
server/categories/types.go
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package categories
|
||||||
|
|
||||||
|
type Category struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
44
server/categories/update.go
Normal file
44
server/categories/update.go
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
package categories
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gis/app"
|
||||||
|
"gis/server/httputil"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UpdateCategoryRequest struct {
|
||||||
|
Name string `json:"name" validate:"required,max=255"`
|
||||||
|
Description string `json:"description" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateCategoryRoute(application *app.App) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id := r.PathValue("id")
|
||||||
|
|
||||||
|
req, err := httputil.DecodeJSON[UpdateCategoryRequest](w, r)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Invalid request", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := application.Validator.Struct(req); err != nil {
|
||||||
|
httputil.WriteValidationErrors(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tag, err := application.Db.Exec(application.Ctx,
|
||||||
|
"UPDATE categories SET name=$1, description=$2, updated_at=now() WHERE id=$3",
|
||||||
|
req.Name, req.Description, id,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if tag.RowsAffected() == 0 {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
30
server/datasets/create.go
Normal file
30
server/datasets/create.go
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
package datasets
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gis/app"
|
||||||
|
"gis/server/httputil"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CreateDatasetRequest struct {
|
||||||
|
Name string `json:"name" validate:"required,max=255"`
|
||||||
|
Description string `json:"description" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func createDatasetRoute(application *app.App) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
req, err := httputil.DecodeJSON[CreateDatasetRequest](w, r)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Invalid request", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := application.Validator.Struct(req); err != nil {
|
||||||
|
httputil.WriteValidationErrors(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
}
|
||||||
|
}
|
||||||
27
server/datasets/delete.go
Normal file
27
server/datasets/delete.go
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package datasets
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gis/app"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func deleteDatasetRoute(application *app.App) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id := r.PathValue("id")
|
||||||
|
|
||||||
|
tag, err := application.Db.Exec(application.Ctx,
|
||||||
|
"DELETE FROM datasets WHERE id=$1",
|
||||||
|
id,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if tag.RowsAffected() == 0 {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
45
server/datasets/index.go
Normal file
45
server/datasets/index.go
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package datasets
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"gis/app"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func listDatasetsRoute(application *app.App) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
rows, err := application.Db.Query(application.Ctx, "select id, name from datasets")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
datasets := make([]Dataset, 0)
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var dataset Dataset
|
||||||
|
if err := rows.Scan(&dataset.ID, &dataset.Name); err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
datasets = append(datasets, dataset)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.NewEncoder(w).Encode(map[string][]Dataset{"data": datasets})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
server/datasets/routes.go
Normal file
13
server/datasets/routes.go
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package datasets
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gis/app"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func AddDatasetsRoutes(application *app.App, mux *http.ServeMux) {
|
||||||
|
mux.HandleFunc("GET /datasets", listDatasetsRoute(application))
|
||||||
|
mux.HandleFunc("POST /datasets", createDatasetRoute(application))
|
||||||
|
mux.HandleFunc("PUT /datasets/{id}", updateDatasetRoute(application))
|
||||||
|
mux.HandleFunc("DELETE /datasets/{id}", deleteDatasetRoute(application))
|
||||||
|
}
|
||||||
6
server/datasets/types.go
Normal file
6
server/datasets/types.go
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package datasets
|
||||||
|
|
||||||
|
type Dataset struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
44
server/datasets/update.go
Normal file
44
server/datasets/update.go
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
package datasets
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gis/app"
|
||||||
|
"gis/server/httputil"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UpdateDatasetRequest struct {
|
||||||
|
Name string `json:"name" validate:"required,max=255"`
|
||||||
|
Description string `json:"description" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateDatasetRoute(application *app.App) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id := r.PathValue("id")
|
||||||
|
|
||||||
|
req, err := httputil.DecodeJSON[UpdateDatasetRequest](w, r)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Invalid request", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := application.Validator.Struct(req); err != nil {
|
||||||
|
httputil.WriteValidationErrors(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tag, err := application.Db.Exec(application.Ctx,
|
||||||
|
"UPDATE datasets SET name=$1, description=$2, updated_at=now() WHERE id=$3",
|
||||||
|
req.Name, req.Description, id,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if tag.RowsAffected() == 0 {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
49
server/files/delete.go
Normal file
49
server/files/delete.go
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
package files
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"gis/app"
|
||||||
|
"gis/server/httputil"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
"github.com/minio/minio-go/v7"
|
||||||
|
)
|
||||||
|
|
||||||
|
func deleteFileRoute(application *app.App) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id := r.PathValue("file_id")
|
||||||
|
|
||||||
|
var storageKey string
|
||||||
|
err := application.Db.QueryRow(r.Context(),
|
||||||
|
"SELECT storage_key FROM files WHERE id=$1",
|
||||||
|
id,
|
||||||
|
).Scan(&storageKey)
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
httputil.WriteJSON(w, http.StatusNotFound, map[string]string{"error": "not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := application.S3.RemoveObject(
|
||||||
|
r.Context(),
|
||||||
|
application.Cfg.S3Bucket,
|
||||||
|
storageKey,
|
||||||
|
minio.RemoveObjectOptions{},
|
||||||
|
); err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = application.Db.Exec(r.Context(), "DELETE FROM files WHERE id=$1", id)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
39
server/files/get.go
Normal file
39
server/files/get.go
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package files
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"gis/app"
|
||||||
|
"gis/server/httputil"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getFileRoute(application *app.App) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id := r.PathValue("file_id")
|
||||||
|
|
||||||
|
var gf GeoFile
|
||||||
|
err := application.Db.QueryRow(r.Context(),
|
||||||
|
`SELECT id, filename, file_type, validation_status,
|
||||||
|
validation_error, kato_column, crs, feature_count,
|
||||||
|
uploaded_at, updated_at
|
||||||
|
FROM files WHERE id=$1`,
|
||||||
|
id,
|
||||||
|
).Scan(
|
||||||
|
&gf.ID, &gf.Filename, &gf.FileType, &gf.ValidationStatus,
|
||||||
|
&gf.ValidationError, &gf.KatoColumn, &gf.CRS, &gf.FeatureCount,
|
||||||
|
&gf.UploadedAt, &gf.UpdatedAt,
|
||||||
|
)
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
httputil.WriteJSON(w, http.StatusNotFound, map[string]string{"error": "not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
httputil.WriteJSON(w, http.StatusOK, gf)
|
||||||
|
}
|
||||||
|
}
|
||||||
12
server/files/routes.go
Normal file
12
server/files/routes.go
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package files
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gis/app"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func AddFilesRoutes(application *app.App, mux *http.ServeMux) {
|
||||||
|
mux.HandleFunc("POST /files", uploadFileRoute(application))
|
||||||
|
mux.HandleFunc("GET /files/{file_id}", getFileRoute(application))
|
||||||
|
mux.HandleFunc("DELETE /files/{file_id}", deleteFileRoute(application))
|
||||||
|
}
|
||||||
40
server/files/types.go
Normal file
40
server/files/types.go
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
package files
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type FileType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
FileTypeVectorWithTable FileType = "vector_with_table"
|
||||||
|
FileTypeVector FileType = "vector"
|
||||||
|
FileTypeRaster FileType = "raster"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ValidationStatus string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ValidationStatusPending ValidationStatus = "pending"
|
||||||
|
ValidationStatusValid ValidationStatus = "valid"
|
||||||
|
ValidationStatusFailed ValidationStatus = "failed"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GeoFile struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Filename string `json:"filename"`
|
||||||
|
FileType FileType `json:"file_type"`
|
||||||
|
ValidationStatus ValidationStatus `json:"validation_status"`
|
||||||
|
ValidationError *string `json:"validation_error"`
|
||||||
|
KatoColumn *string `json:"kato_column"`
|
||||||
|
CRS *string `json:"crs"`
|
||||||
|
FeatureCount *int `json:"feature_count"`
|
||||||
|
UploadedAt time.Time `json:"uploaded_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var allowedExtensions = map[string]FileType{
|
||||||
|
".zip": FileTypeVectorWithTable,
|
||||||
|
".geojson": FileTypeVectorWithTable,
|
||||||
|
".gpkg": FileTypeVectorWithTable,
|
||||||
|
".tif": FileTypeRaster,
|
||||||
|
".tiff": FileTypeRaster,
|
||||||
|
}
|
||||||
85
server/files/upload.go
Normal file
85
server/files/upload.go
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
package files
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"gis/app"
|
||||||
|
"gis/server/httputil"
|
||||||
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/minio/minio-go/v7"
|
||||||
|
)
|
||||||
|
|
||||||
|
func uploadFileRoute(application *app.App) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if err := r.ParseMultipartForm(64 << 20); err != nil {
|
||||||
|
httputil.WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "request too large or not multipart"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rawFileType := r.FormValue("file_type")
|
||||||
|
if rawFileType == "" {
|
||||||
|
httputil.WriteJSON(w, http.StatusUnprocessableEntity, map[string]string{"error": "file_type is required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ft := FileType(rawFileType)
|
||||||
|
if ft != FileTypeVectorWithTable && ft != FileTypeVector && ft != FileTypeRaster {
|
||||||
|
httputil.WriteJSON(w, http.StatusUnprocessableEntity, map[string]string{"error": "invalid file_type"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
f, header, err := r.FormFile("file")
|
||||||
|
if err != nil {
|
||||||
|
httputil.WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "file is required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
ext := strings.ToLower(filepath.Ext(header.Filename))
|
||||||
|
if ext == "" {
|
||||||
|
httputil.WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "unsupported file format"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, ok := allowedExtensions[ext]; !ok {
|
||||||
|
httputil.WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "unsupported file format"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
storageKey := fmt.Sprintf("%d_%s", time.Now().UnixNano(), header.Filename)
|
||||||
|
|
||||||
|
_, err = application.S3.PutObject(
|
||||||
|
r.Context(),
|
||||||
|
application.Cfg.S3Bucket,
|
||||||
|
storageKey,
|
||||||
|
f,
|
||||||
|
header.Size,
|
||||||
|
minio.PutObjectOptions{ContentType: header.Header.Get("Content-Type")},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
httputil.WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to store file"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var gf GeoFile
|
||||||
|
err = application.Db.QueryRow(r.Context(),
|
||||||
|
`INSERT INTO files (filename, storage_key, file_type)
|
||||||
|
VALUES ($1, $2, $3)
|
||||||
|
RETURNING id, filename, file_type, validation_status,
|
||||||
|
validation_error, kato_column, crs, feature_count,
|
||||||
|
uploaded_at, updated_at`,
|
||||||
|
header.Filename, storageKey, ft,
|
||||||
|
).Scan(
|
||||||
|
&gf.ID, &gf.Filename, &gf.FileType, &gf.ValidationStatus,
|
||||||
|
&gf.ValidationError, &gf.KatoColumn, &gf.CRS, &gf.FeatureCount,
|
||||||
|
&gf.UploadedAt, &gf.UpdatedAt,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
httputil.WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to save file record"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
httputil.WriteJSON(w, http.StatusAccepted, gf)
|
||||||
|
}
|
||||||
|
}
|
||||||
18
server/helpers.go
Normal file
18
server/helpers.go
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gis/server/httputil"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func writeJSON(w http.ResponseWriter, status int, data any) {
|
||||||
|
httputil.WriteJSON(w, status, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeJSON[T any](w http.ResponseWriter, r *http.Request) (T, error) {
|
||||||
|
return httputil.DecodeJSON[T](w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeValidationErrors(w http.ResponseWriter, err error) {
|
||||||
|
httputil.WriteValidationErrors(w, err)
|
||||||
|
}
|
||||||
62
server/httputil/httputil.go
Normal file
62
server/httputil/httputil.go
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
package httputil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
)
|
||||||
|
|
||||||
|
func WriteJSON(w http.ResponseWriter, status int, data any) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(status)
|
||||||
|
_ = json.NewEncoder(w).Encode(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecodeJSON[T any](w http.ResponseWriter, r *http.Request) (T, error) {
|
||||||
|
var v T
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
|
||||||
|
|
||||||
|
dec := json.NewDecoder(r.Body)
|
||||||
|
dec.DisallowUnknownFields()
|
||||||
|
|
||||||
|
if err := dec.Decode(&v); err != nil {
|
||||||
|
return v, err
|
||||||
|
}
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func WriteValidationErrors(w http.ResponseWriter, err error) {
|
||||||
|
var ve validator.ValidationErrors
|
||||||
|
if !errors.As(err, &ve) {
|
||||||
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
problems := make(map[string]string, len(ve))
|
||||||
|
for _, fe := range ve {
|
||||||
|
problems[fe.Field()] = messageForTag(fe)
|
||||||
|
}
|
||||||
|
WriteJSON(w, http.StatusBadRequest, map[string]any{"errors": problems})
|
||||||
|
}
|
||||||
|
|
||||||
|
func messageForTag(fe validator.FieldError) string {
|
||||||
|
switch fe.Tag() {
|
||||||
|
case "required":
|
||||||
|
return "is required"
|
||||||
|
case "email":
|
||||||
|
return "must be a valid email address"
|
||||||
|
case "min":
|
||||||
|
return fmt.Sprintf("must be at least %s characters", fe.Param())
|
||||||
|
case "max":
|
||||||
|
return fmt.Sprintf("must be at most %s characters", fe.Param())
|
||||||
|
case "gte":
|
||||||
|
return fmt.Sprintf("must be %s or greater", fe.Param())
|
||||||
|
case "lte":
|
||||||
|
return fmt.Sprintf("must be %s or less", fe.Param())
|
||||||
|
default:
|
||||||
|
return "is invalid"
|
||||||
|
}
|
||||||
|
}
|
||||||
20
server/router.go
Normal file
20
server/router.go
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gis/app"
|
||||||
|
"gis/server/categories"
|
||||||
|
"gis/server/datasets"
|
||||||
|
"gis/server/files"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func AppRouter(application *app.App) http.Handler {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
|
mux.Handle("GET /up", upRoute(application))
|
||||||
|
datasets.AddDatasetsRoutes(application, mux)
|
||||||
|
categories.AddCategoriesRoutes(application, mux)
|
||||||
|
files.AddFilesRoutes(application, mux)
|
||||||
|
|
||||||
|
return mux
|
||||||
|
}
|
||||||
14
server/up.go
Normal file
14
server/up.go
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"gis/app"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func upRoute(application *app.App) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user