1
0
mirror of https://github.com/Luzifer/tex-api.git synced 2024-09-16 16:18:24 +00:00
tex-api/processing.go
Knut Ahlers ab63204ee8
Add URL reporting parameter
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2023-09-06 22:56:25 +02:00

206 lines
5.3 KiB
Go

package main
import (
"archive/zip"
"bytes"
"encoding/json"
"io"
"math"
"net/http"
"os"
"os/exec"
"path"
"strconv"
"time"
"github.com/gofrs/uuid"
"github.com/gorilla/mux"
"github.com/sirupsen/logrus"
"github.com/spf13/afero"
"github.com/spf13/afero/zipfs"
)
const (
createModeDir = 0o700
createModeFile = 0o600
)
func jobProcessor(uid uuid.UUID) {
logger := logrus.WithField("uuid", uid)
logger.Info("Started processing")
processingDir := path.Dir(pathFromUUID(uid, filenameStatus))
status, err := loadStatusByUUID(uid)
if err != nil {
logger.WithError(err).Error("Unable to load status file in processing job")
return
}
cmd := exec.Command("/bin/bash", cfg.Script) // #nosec G204
cmd.Dir = processingDir
cmd.Stderr = logger.WriterLevel(logrus.InfoLevel) // Bash uses stderr for `-x` parameter
status.UpdateStatus(statusStarted)
if err := status.Save(); err != nil {
logger.WithError(err).Error("Unable to save status file")
return
}
if err := cmd.Run(); err != nil {
logger.WithError(err).Error("Processing failed")
status.UpdateStatus(statusError)
if err := status.Save(); err != nil {
logger.WithError(err).Error("Unable to save status file")
return
}
return
}
status.UpdateStatus(statusFinished)
if err := status.Save(); err != nil {
logger.WithError(err).Error("Unable to save status file")
return
}
logger.Info("Finished processing")
}
func startNewJob(res http.ResponseWriter, r *http.Request) {
jobUUID := uuid.Must(uuid.NewV4())
inputFile := pathFromUUID(jobUUID, filenameInput)
statusFile := pathFromUUID(jobUUID, filenameStatus)
// Create the target directory
if err := os.Mkdir(path.Dir(inputFile), createModeDir); err != nil {
logrus.WithError(err).Errorf("Unable to create job dir %q", path.Dir(inputFile))
}
// Create a copy-on-write fs for the target
targetFs := afero.NewBasePathFs(afero.NewOsFs(), path.Dir(inputFile))
// Cache the input body
body := new(bytes.Buffer)
if _, err := io.Copy(body, r.Body); err != nil {
serverErrorf(res, err, "reading input body")
return
}
// Pull in the new files into the target dir
var inputSource afero.Fs
if hasZipHeader(body.Bytes()) {
// We got an archive
zipR, err := zip.NewReader(bytes.NewReader(body.Bytes()), int64(body.Len()))
if err != nil {
serverErrorf(res, err, "opening input body as zip")
return
}
inputSource = zipfs.New(zipR)
} else {
// We assume that was a single TeX file
inputSource = afero.NewMemMapFs()
if err := afero.WriteFile(inputSource, "main.tex", body.Bytes(), createModeFile); err != nil {
serverErrorf(res, err, "writing input to in-mem fs")
return
}
}
if err := syncFilesToOverlay(inputSource, targetFs); err != nil {
serverErrorf(res, err, "copying files to target dir")
return
}
// If set copy the default env into the target dir
if cfg.DefaultEnv != "" {
if err := syncFilesToOverlay(
afero.NewReadOnlyFs(afero.NewBasePathFs(afero.NewOsFs(), cfg.DefaultEnv)),
targetFs,
); err != nil {
serverErrorf(res, err, "copying default env files to target dir")
return
}
}
status := jobStatus{
UUID: jobUUID.String(),
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
Status: statusCreated,
}
if err := status.Save(); err != nil {
serverErrorf(res, err, "Unable to create status file %q", statusFile)
return
}
go jobProcessor(jobUUID)
u := urlMust(router.Get("waitForJob").URL("uid", jobUUID.String()))
u.RawQuery = r.URL.Query().Encode()
if r.URL.Query().Has("report-urls") {
if err := json.NewEncoder(res).Encode(map[string]string{
"download": urlMust(router.Get("downloadAssets").URL("uid", jobUUID.String())).String(),
"status": urlMust(router.Get("getJobStatus").URL("uid", jobUUID.String())).String(),
"wait": urlMust(router.Get("waitForJob").URL("uid", jobUUID.String())).String(),
}); err != nil {
serverErrorf(res, err, "encoding url response")
}
return
}
http.Redirect(res, r, u.String(), http.StatusFound)
}
func waitForJob(res http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
uid, err := uuid.FromString(vars["uid"])
if err != nil {
http.Error(res, "UUID had unexpected format!", http.StatusBadRequest)
return
}
var loop int
if v := r.URL.Query().Get("loop"); v != "" {
if pv, convErr := strconv.Atoi(v); convErr == nil {
loop = pv
}
}
loop++
status, err := loadStatusByUUID(uid)
if err != nil {
serverErrorf(res, err, "Unable to read status file")
return
}
switch status.Status {
case statusCreated:
fallthrough
case statusStarted:
<-time.After(time.Duration(math.Pow(sleepBase, float64(loop))) * time.Second)
params := r.URL.Query()
params.Set("loop", strconv.Itoa(loop))
u := urlMust(router.Get("waitForJob").URL("uid", uid.String()))
u.RawQuery = params.Encode()
http.Redirect(res, r, u.String(), http.StatusFound)
return
case statusError:
if r.URL.Query().Has("log-on-error") {
u := urlMust(router.Get("downloadAssets").URL("uid", uid.String()))
u.RawQuery = r.URL.Query().Encode()
http.Redirect(res, r, u.String(), http.StatusFound)
return
}
http.Error(res, "Processing ran into an error.", http.StatusInternalServerError)
case statusFinished:
u := urlMust(router.Get("downloadAssets").URL("uid", uid.String()))
u.RawQuery = r.URL.Query().Encode()
http.Redirect(res, r, u.String(), http.StatusFound)
}
}