view pkg/controllers/srimports.go @ 5490:5f47eeea988d logging

Use own logging package.
author Sascha L. Teichmann <sascha.teichmann@intevation.de>
date Mon, 20 Sep 2021 17:45:39 +0200
parents 850f5847d18a
children 6270951dda28
line wrap: on
line source

// This is Free Software under GNU Affero General Public License v >= 3.0
// without warranty, see README.md and license for details.
//
// SPDX-License-Identifier: AGPL-3.0-or-later
// License-Filename: LICENSES/AGPL-3.0.txt
//
// Copyright (C) 2018 by via donau
//   – Österreichische Wasserstraßen-Gesellschaft mbH
// Software engineering by Intevation GmbH
//
// Author(s):
//  * Sascha L. Teichmann <sascha.teichmann@intevation.de>

package controllers

import (
	"archive/zip"
	"encoding/hex"
	"fmt"
	"net/http"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/gorilla/mux"

	"gemma.intevation.de/gemma/pkg/auth"
	"gemma.intevation.de/gemma/pkg/common"
	"gemma.intevation.de/gemma/pkg/config"
	"gemma.intevation.de/gemma/pkg/imports"
	"gemma.intevation.de/gemma/pkg/log"
	"gemma.intevation.de/gemma/pkg/misc"
	"gemma.intevation.de/gemma/pkg/models"

	mw "gemma.intevation.de/gemma/pkg/middleware"
)

const (
	soundingResultName    = "soundingresult"
	maxSoundingResultSize = 50 * 1024 * 1024
)

func fetchSoundingResult(req *http.Request) (string, error) {

	// Check first if we have a token.
	if token := req.FormValue("token"); token != "" {
		if _, err := hex.DecodeString(token); err != nil {
			return "", err
		}
		dir := config.TmpDir()
		if dir == "" {
			dir = os.TempDir()
		}
		// XXX: This should hopefully be race-free enough.
		now := time.Now().Format("2006-15-04-05")
		dst := filepath.Join(dir, soundingResultName+"-"+token+"-"+now)
		if err := misc.UnmakeTempFile(token, dst); err != nil {
			return "", err
		}
		return dst, nil
	}

	return misc.StoreUploadedFileCheck(
		req,
		soundingResultName,
		"sr.zip",
		maxSoundingResultSize,
		true,
	)
}

func fetchSoundingResultMetaOverrides(sr *imports.SoundingResult, req *http.Request) error {

	if v := req.FormValue("epsg"); v != "" {
		epsg, err := strconv.ParseUint(v, 10, 32)
		if err != nil {
			return err
		}
		srid := uint(epsg)
		sr.EPSG = &srid
	}

	if v := req.FormValue("date"); v != "" {
		date, err := time.Parse(common.DateFormat, v)
		if err != nil {
			return err
		}
		sr.Date = &models.Date{Time: date}
	}

	if v := req.FormValue("depth-reference"); v != "" {
		sr.DepthReference = &v
	}

	if v := req.FormValue("bottleneck"); v != "" {
		sr.Bottleneck = &v
	}

	if v := req.FormValue("negate-z"); v != "" {
		var negateZ bool
		switch strings.ToLower(v) {
		case "true", "1":
			negateZ = true
		case "false", "0":
			negateZ = false
		default:
			return fmt.Errorf("unknown negate-z '%s'", v)
		}
		sr.NegateZ = &negateZ
	}

	// Kept this in for compat.
	if v := req.FormValue("single-beam"); v != "" {
		var surveyType models.SurveyType
		switch strings.ToLower(v) {
		case "true", "1", "singlebeam", "single-beam":
			surveyType = models.SurveyTypeSingleBeam
		case "false", "0", "multibeam", "multi-beam":
			surveyType = models.SurveyTypeMultiBeam
		default:
			return fmt.Errorf("unknown single-beam '%s'", v)
		}
		sr.SurveyType = &surveyType
	}

	if v := req.FormValue("survey-type"); v != "" {
		var surveyType models.SurveyType
		switch strings.ToLower(v) {
		case "2", "marking":
			surveyType = models.SurveyTypeMarking
		case "true", "1", "singlebeam", "single-beam", "single":
			surveyType = models.SurveyTypeSingleBeam
		case "false", "0", "multibeam", "multi-beam", "multi":
			surveyType = models.SurveyTypeMultiBeam
		default:
			return fmt.Errorf("unknown survey-type '%s'", v)
		}
		sr.SurveyType = &surveyType
	}

	return nil
}

func importSoundingResult(rw http.ResponseWriter, req *http.Request) {

	sr := new(imports.SoundingResult)

	if err := fetchSoundingResultMetaOverrides(sr, req); err != nil {
		log.Errorf("%v\n", err)
		http.Error(rw, "error: "+err.Error(), http.StatusBadRequest)
		return
	}

	dir, err := fetchSoundingResult(req)
	if err != nil {
		log.Errorf("%v\n", err)
		http.Error(rw, "error: "+err.Error(), http.StatusInternalServerError)
		return
	}
	sr.Dir = dir

	serialized, err := common.ToJSONString(sr)
	if err != nil {
		log.Errorf("%v\n", err)
		http.Error(rw, "error: "+err.Error(), http.StatusInternalServerError)
		return
	}

	session, _ := auth.GetSession(req)

	sendEmail := req.FormValue("send-email") != ""

	jobID, err := imports.AddJob(
		imports.SRJobKind,
		time.Time{}, // due
		nil,         // trys
		nil,         // retry wait
		session.User,
		sendEmail,
		serialized)

	if err != nil {
		log.Errorf("%v\n", err)
		http.Error(rw, "error: "+err.Error(), http.StatusInternalServerError)
		return
	}

	log.Infof("added import #%d to queue\n", jobID)

	result := struct {
		ID int64 `json:"id"`
	}{
		ID: jobID,
	}
	mw.SendJSON(rw, http.StatusCreated, &result)
}

func loadMeta(f *zip.File) (*models.SoundingResultMeta, error) {
	r, err := f.Open()
	if err != nil {
		return nil, err
	}
	defer r.Close()
	var m models.SoundingResultMeta
	return &m, m.Decode(r)
}

func uploadSoundingResult(req *http.Request) (jr mw.JSONResult, err error) {

	var dir string
	if dir, err = misc.StoreUploadedFileCheck(
		req,
		soundingResultName,
		"sr.zip",
		maxSoundingResultSize,
		true,
	); err != nil {
		return
	}

	var messages []string

	srFile := filepath.Join(dir, "sr.zip")

	var zr *zip.ReadCloser
	var once sync.Once
	closeOnce := func() { zr.Close() }

	var isZIP bool
	if zr, err = zip.OpenReader(srFile); err != nil {
		messages = append(messages, fmt.Sprintf("%v - "+
			"Trying TXT file mode.", err))
	} else {
		isZIP = true
		defer once.Do(closeOnce)
	}

	var result struct {
		Token    string      `json:"token,omitempty"`
		Meta     interface{} `json:"meta,omitempty"`
		Messages []string    `json:"messages,omitempty"`
	}

	find := func(ext string) *zip.File { return common.FindInZIP(zr, ext) }

	var noXYZ bool
	if isZIP {
		if zr != nil {
			noXYZ = find(".xyz") == nil && find(".txt") == nil
		}

		if noXYZ {
			messages = append(messages, "no .xyz or .txt file found.")
		}

		if mj := find("meta.json"); mj == nil {
			messages = append(messages, "no 'meta.json' file found.")
		} else {
			if meta, err := loadMeta(mj); err != nil {
				messages = append(messages,
					fmt.Sprintf("'meta.json' found but invalid: %v", err))
			} else {
				errs := meta.Validate(req.Context(), mw.JSONConn(req))
				for _, err := range errs {
					messages = append(messages,
						fmt.Sprintf("invalid 'meta.json': %v", err))
				}
				result.Meta = meta
			}
		}
	}

	if zr != nil {
		once.Do(closeOnce)
	}

	code := http.StatusCreated

	// If there are no XYZ data we cant help the user anyway.
	if noXYZ {
		code = http.StatusBadRequest
		if err2 := os.RemoveAll(dir); err2 != nil {
			log.Errorf("%v\n", err2)
		}
	} else if result.Token, err = misc.MakeTempFile(dir); err != nil {
		if err2 := os.RemoveAll(dir); err2 != nil {
			log.Errorf("%v\n", err2)
		}
		return
	}

	result.Messages = messages

	jr = mw.JSONResult{
		Code:   code,
		Result: &result,
	}
	return
}

func deleteSoundingUpload(rw http.ResponseWriter, req *http.Request) {
	token := mux.Vars(req)["token"]
	if _, err := hex.DecodeString(token); err != nil {
		http.Error(rw, "Invalid token", http.StatusBadRequest)
		return
	}
	if err := misc.DeleteTempFile(token); err != nil {
		http.Error(rw, fmt.Sprintf("error: %v", err), http.StatusInternalServerError)
		return
	}
	result := struct {
		Message string `json:"message"`
	}{
		Message: fmt.Sprintf("Token %s deleted.", token),
	}
	mw.SendJSON(rw, http.StatusOK, &result)
}