First checkpoint

This commit is contained in:
2026-06-04 19:26:38 +05:00
commit b08e71b3bb
43 changed files with 1252 additions and 0 deletions

49
server/files/delete.go Normal file
View 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
View 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
View 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
View 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
View 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)
}
}