Skip to content

Commit e15a8a4

Browse files
authored
✨ Upload files instead of multi-part form. (#743)
Post a _manifest_ file instead of separate multi-part form files. Much simpler and more easily supports the addon staging the issues and deps files on disk rather than streaming. The more atomic approach will prevent transaction deadlock which can more easily occur when the addon-analyzer builder reported an error (which it should never do). The uploaded file contains markers used to delimited the documents. `^]` = `\x1D` = GS (group separator). ``` ^]BEGIN-MAIN^] --- commit: 1234 ^]END-MAIN^] ^]BEGIN-ISSUES^] --- ruleset: ruleset-1 rule: rule-1 incidents: ... ^]END-ISSUES^] ^]BEGIN-DEPS^] --- name: github.com/jboss version: 4.0 labels: - konveyor.io/language=java - konveyor.io/otherA=dog ^]END-DEPS^] ``` Flow: 1. post (upload) manifest.yaml file. 2. post `ref` to the manifest file. 3. delete manifest file. Orphaned files will be reaped. --- The binding client needed to be updated to handle different file encoding (MIME). --------- Signed-off-by: Jeff Ortel <[email protected]>
1 parent 7477257 commit e15a8a4

15 files changed

+1374
-151
lines changed

api/analysis.go

+163-35
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
package api
22

33
import (
4+
"bufio"
45
"bytes"
56
"encoding/json"
67
"errors"
8+
"fmt"
79
"io"
810
"net/http"
911
"os"
12+
"regexp"
1013
"sort"
1114
"strconv"
1215
"strings"
@@ -53,9 +56,15 @@ const (
5356
AppAnalysisIssuesRoot = AppAnalysisRoot + "/issues"
5457
)
5558

59+
// Manifest markers.
60+
// The GS=\x1D (group separator).
5661
const (
57-
IssueField = "issues"
58-
DepField = "dependencies"
62+
BeginMainMarker = "\x1DBEGIN-MAIN\x1D"
63+
EndMainMarker = "\x1DEND-MAIN\x1D"
64+
BeginIssuesMarker = "\x1DBEGIN-ISSUES\x1D"
65+
EndIssuesMarker = "\x1DEND-ISSUES\x1D"
66+
BeginDepsMarker = "\x1DBEGIN-DEPS\x1D"
67+
EndDepsMarker = "\x1DEND-DEPS\x1D"
5968
)
6069

6170
// AnalysisHandler handles analysis resource routes.
@@ -315,9 +324,20 @@ func (h AnalysisHandler) AppList(ctx *gin.Context) {
315324
// @summary Create an analysis.
316325
// @description Create an analysis.
317326
// @description Form fields:
318-
// @description - file: file that contains the api.Analysis resource.
319-
// @description - issues: file that multiple api.Issue resources.
320-
// @description - dependencies: file that multiple api.TechDependency resources.
327+
// @description file: A manifest file that contains 3 sections
328+
// @description containing documents delimited by markers.
329+
// @description The manifest must contain ALL markers even when sections are empty.
330+
// @description Note: `^]` = `\x1D` = GS (group separator).
331+
// @description Section markers:
332+
// @description ^]BEGIN-MAIN^]
333+
// @description ^]END-MAIN^]
334+
// @description ^]BEGIN-ISSUES^]
335+
// @description ^]END-ISSUES^]
336+
// @description ^]BEGIN-DEPS^]
337+
// @description ^]END-DEPS^]
338+
// @description The encoding must be:
339+
// @description - application/json
340+
// @description - application/x-yaml
321341
// @tags analyses
322342
// @produce json
323343
// @success 201 {object} api.Analysis
@@ -337,32 +357,40 @@ func (h AnalysisHandler) AppCreate(ctx *gin.Context) {
337357
return
338358
}
339359
}
360+
db := h.DB(ctx)
340361
//
341-
// Analysis
342-
input, err := ctx.FormFile(FileField)
362+
// Manifest
363+
fh := FileHandler{}
364+
name := fmt.Sprintf("app.%d.manifest", id)
365+
file, err := fh.create(ctx, name)
343366
if err != nil {
344-
err = &BadRequestError{err.Error()}
345367
_ = ctx.Error(err)
346368
return
347369
}
348-
reader, err := input.Open()
370+
defer func() {
371+
err = fh.delete(ctx, file)
372+
if err != nil {
373+
_ = ctx.Error(err)
374+
}
375+
}()
376+
reader := &ManifestReader{}
377+
f, err := reader.open(file.Path, BeginMainMarker, EndMainMarker)
349378
if err != nil {
350379
err = &BadRequestError{err.Error()}
351380
_ = ctx.Error(err)
352381
return
353382
}
354383
defer func() {
355-
_ = reader.Close()
384+
_ = f.Close()
356385
}()
357-
encoding := input.Header.Get(ContentType)
358-
d, err := h.Decoder(ctx, encoding, reader)
386+
d, err := h.Decoder(ctx, file.Encoding, reader)
359387
if err != nil {
360388
err = &BadRequestError{err.Error()}
361389
_ = ctx.Error(err)
362390
return
363391
}
364-
r := Analysis{}
365-
err = d.Decode(&r)
392+
r := &Analysis{}
393+
err = d.Decode(r)
366394
if err != nil {
367395
err = &BadRequestError{err.Error()}
368396
_ = ctx.Error(err)
@@ -371,7 +399,6 @@ func (h AnalysisHandler) AppCreate(ctx *gin.Context) {
371399
analysis := r.Model()
372400
analysis.ApplicationID = id
373401
analysis.CreateUser = h.BaseHandler.CurrentUser(ctx)
374-
db := h.DB(ctx)
375402
db.Logger = db.Logger.LogMode(logger.Error)
376403
err = db.Create(analysis).Error
377404
if err != nil {
@@ -380,23 +407,17 @@ func (h AnalysisHandler) AppCreate(ctx *gin.Context) {
380407
}
381408
//
382409
// Issues
383-
input, err = ctx.FormFile(IssueField)
384-
if err != nil {
385-
err = &BadRequestError{err.Error()}
386-
_ = ctx.Error(err)
387-
return
388-
}
389-
reader, err = input.Open()
410+
reader = &ManifestReader{}
411+
f, err = reader.open(file.Path, BeginIssuesMarker, EndIssuesMarker)
390412
if err != nil {
391413
err = &BadRequestError{err.Error()}
392414
_ = ctx.Error(err)
393415
return
394416
}
395417
defer func() {
396-
_ = reader.Close()
418+
_ = f.Close()
397419
}()
398-
encoding = input.Header.Get(ContentType)
399-
d, err = h.Decoder(ctx, encoding, reader)
420+
d, err = h.Decoder(ctx, file.Encoding, reader)
400421
if err != nil {
401422
err = &BadRequestError{err.Error()}
402423
_ = ctx.Error(err)
@@ -425,23 +446,17 @@ func (h AnalysisHandler) AppCreate(ctx *gin.Context) {
425446
}
426447
//
427448
// Dependencies
428-
input, err = ctx.FormFile(DepField)
429-
if err != nil {
430-
err = &BadRequestError{err.Error()}
431-
_ = ctx.Error(err)
432-
return
433-
}
434-
reader, err = input.Open()
449+
reader = &ManifestReader{}
450+
f, err = reader.open(file.Path, BeginDepsMarker, EndDepsMarker)
435451
if err != nil {
436452
err = &BadRequestError{err.Error()}
437453
_ = ctx.Error(err)
438454
return
439455
}
440456
defer func() {
441-
_ = reader.Close()
457+
_ = f.Close()
442458
}()
443-
encoding = input.Header.Get(ContentType)
444-
d, err = h.Decoder(ctx, encoding, reader)
459+
d, err = h.Decoder(ctx, file.Encoding, reader)
445460
if err != nil {
446461
err = &BadRequestError{err.Error()}
447462
_ = ctx.Error(err)
@@ -2860,3 +2875,116 @@ func (r *yamlEncoder) embed(object any) encoder {
28602875
r.write(s)
28612876
return r
28622877
}
2878+
2879+
// ManifestReader analysis manifest reader.
2880+
// The manifest contains 3 sections containing documents delimited by markers.
2881+
// The manifest must contain ALL markers even when sections are empty.
2882+
// Note: `^]` = `\x1D` = GS (group separator).
2883+
// Section markers:
2884+
//
2885+
// ^]BEGIN-MAIN^]
2886+
// ^]END-MAIN^]
2887+
// ^]BEGIN-ISSUES^]
2888+
// ^]END-ISSUES^]
2889+
// ^]BEGIN-DEPS^]
2890+
// ^]END-DEPS^]
2891+
type ManifestReader struct {
2892+
file *os.File
2893+
marker map[string]int64
2894+
begin int64
2895+
end int64
2896+
read int64
2897+
}
2898+
2899+
// scan manifest and catalog position of markers.
2900+
func (r *ManifestReader) scan(path string) (err error) {
2901+
if r.marker != nil {
2902+
return
2903+
}
2904+
r.file, err = os.Open(path)
2905+
if err != nil {
2906+
return
2907+
}
2908+
defer func() {
2909+
_ = r.file.Close()
2910+
}()
2911+
pattern, err := regexp.Compile(`^\x1D[A-Z-]+\x1D$`)
2912+
if err != nil {
2913+
return
2914+
}
2915+
p := int64(0)
2916+
r.marker = make(map[string]int64)
2917+
scanner := bufio.NewScanner(r.file)
2918+
for scanner.Scan() {
2919+
content := scanner.Text()
2920+
matched := strings.TrimSpace(content)
2921+
if pattern.Match([]byte(matched)) {
2922+
r.marker[matched] = p
2923+
}
2924+
p += int64(len(content))
2925+
p++
2926+
}
2927+
2928+
return
2929+
}
2930+
2931+
// open returns a read delimited by the specified markers.
2932+
func (r *ManifestReader) open(path, begin, end string) (reader io.ReadCloser, err error) {
2933+
found := false
2934+
err = r.scan(path)
2935+
if err != nil {
2936+
return
2937+
}
2938+
r.begin, found = r.marker[begin]
2939+
if !found {
2940+
err = &BadRequestError{
2941+
Reason: fmt.Sprintf("marker: %s not found.", begin),
2942+
}
2943+
return
2944+
}
2945+
r.end, found = r.marker[end]
2946+
if !found {
2947+
err = &BadRequestError{
2948+
Reason: fmt.Sprintf("marker: %s not found.", end),
2949+
}
2950+
return
2951+
}
2952+
if r.begin >= r.end {
2953+
err = &BadRequestError{
2954+
Reason: fmt.Sprintf("marker: %s must preceed %s.", begin, end),
2955+
}
2956+
return
2957+
}
2958+
r.begin += int64(len(begin))
2959+
r.begin++
2960+
r.read = r.end - r.begin
2961+
r.file, err = os.Open(path)
2962+
if err != nil {
2963+
return
2964+
}
2965+
_, err = r.file.Seek(r.begin, io.SeekStart)
2966+
reader = r
2967+
return
2968+
}
2969+
2970+
// Read bytes.
2971+
func (r *ManifestReader) Read(b []byte) (n int, err error) {
2972+
n, err = r.file.Read(b)
2973+
if n == 0 || err != nil {
2974+
return
2975+
}
2976+
if int64(n) > r.read {
2977+
n = int(r.read)
2978+
}
2979+
r.read -= int64(n)
2980+
if n < 1 {
2981+
err = io.EOF
2982+
}
2983+
return
2984+
}
2985+
2986+
// Close the reader.
2987+
func (r *ManifestReader) Close() (err error) {
2988+
err = r.file.Close()
2989+
return
2990+
}

0 commit comments

Comments
 (0)