First checkpoint
This commit is contained in:
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user