Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Upload files instead of multi-part form. #743

Merged
merged 10 commits into from
Sep 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
198 changes: 163 additions & 35 deletions api/analysis.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package api

import (
"bufio"
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"regexp"
"sort"
"strconv"
"strings"
Expand Down Expand Up @@ -53,9 +56,15 @@ const (
AppAnalysisIssuesRoot = AppAnalysisRoot + "/issues"
)

// Manifest markers.
// The GS=\x1D (group separator).
const (
IssueField = "issues"
DepField = "dependencies"
BeginMainMarker = "\x1DBEGIN-MAIN\x1D"
EndMainMarker = "\x1DEND-MAIN\x1D"
BeginIssuesMarker = "\x1DBEGIN-ISSUES\x1D"
EndIssuesMarker = "\x1DEND-ISSUES\x1D"
BeginDepsMarker = "\x1DBEGIN-DEPS\x1D"
EndDepsMarker = "\x1DEND-DEPS\x1D"
)

// AnalysisHandler handles analysis resource routes.
Expand Down Expand Up @@ -315,9 +324,20 @@ func (h AnalysisHandler) AppList(ctx *gin.Context) {
// @summary Create an analysis.
// @description Create an analysis.
// @description Form fields:
// @description - file: file that contains the api.Analysis resource.
// @description - issues: file that multiple api.Issue resources.
// @description - dependencies: file that multiple api.TechDependency resources.
// @description file: A manifest file that contains 3 sections
// @description containing documents delimited by markers.
// @description The manifest must contain ALL markers even when sections are empty.
// @description Note: `^]` = `\x1D` = GS (group separator).
// @description Section markers:
// @description ^]BEGIN-MAIN^]
// @description ^]END-MAIN^]
// @description ^]BEGIN-ISSUES^]
// @description ^]END-ISSUES^]
// @description ^]BEGIN-DEPS^]
// @description ^]END-DEPS^]
// @description The encoding must be:
// @description - application/json
// @description - application/x-yaml
// @tags analyses
// @produce json
// @success 201 {object} api.Analysis
Expand All @@ -337,32 +357,40 @@ func (h AnalysisHandler) AppCreate(ctx *gin.Context) {
return
}
}
db := h.DB(ctx)
//
// Analysis
input, err := ctx.FormFile(FileField)
// Manifest
fh := FileHandler{}
name := fmt.Sprintf("app.%d.manifest", id)
file, err := fh.create(ctx, name)
if err != nil {
err = &BadRequestError{err.Error()}
_ = ctx.Error(err)
return
}
reader, err := input.Open()
defer func() {
err = fh.delete(ctx, file)
if err != nil {
_ = ctx.Error(err)
}
}()
reader := &ManifestReader{}
f, err := reader.open(file.Path, BeginMainMarker, EndMainMarker)
if err != nil {
err = &BadRequestError{err.Error()}
_ = ctx.Error(err)
return
}
defer func() {
_ = reader.Close()
_ = f.Close()
}()
encoding := input.Header.Get(ContentType)
d, err := h.Decoder(ctx, encoding, reader)
d, err := h.Decoder(ctx, file.Encoding, reader)
if err != nil {
err = &BadRequestError{err.Error()}
_ = ctx.Error(err)
return
}
r := Analysis{}
err = d.Decode(&r)
r := &Analysis{}
err = d.Decode(r)
if err != nil {
err = &BadRequestError{err.Error()}
_ = ctx.Error(err)
Expand All @@ -371,7 +399,6 @@ func (h AnalysisHandler) AppCreate(ctx *gin.Context) {
analysis := r.Model()
analysis.ApplicationID = id
analysis.CreateUser = h.BaseHandler.CurrentUser(ctx)
db := h.DB(ctx)
db.Logger = db.Logger.LogMode(logger.Error)
err = db.Create(analysis).Error
if err != nil {
Expand All @@ -380,23 +407,17 @@ func (h AnalysisHandler) AppCreate(ctx *gin.Context) {
}
//
// Issues
input, err = ctx.FormFile(IssueField)
if err != nil {
err = &BadRequestError{err.Error()}
_ = ctx.Error(err)
return
}
reader, err = input.Open()
reader = &ManifestReader{}
f, err = reader.open(file.Path, BeginIssuesMarker, EndIssuesMarker)
if err != nil {
err = &BadRequestError{err.Error()}
_ = ctx.Error(err)
return
}
defer func() {
_ = reader.Close()
_ = f.Close()
}()
encoding = input.Header.Get(ContentType)
d, err = h.Decoder(ctx, encoding, reader)
d, err = h.Decoder(ctx, file.Encoding, reader)
if err != nil {
err = &BadRequestError{err.Error()}
_ = ctx.Error(err)
Expand Down Expand Up @@ -425,23 +446,17 @@ func (h AnalysisHandler) AppCreate(ctx *gin.Context) {
}
//
// Dependencies
input, err = ctx.FormFile(DepField)
if err != nil {
err = &BadRequestError{err.Error()}
_ = ctx.Error(err)
return
}
reader, err = input.Open()
reader = &ManifestReader{}
f, err = reader.open(file.Path, BeginDepsMarker, EndDepsMarker)
if err != nil {
err = &BadRequestError{err.Error()}
_ = ctx.Error(err)
return
}
defer func() {
_ = reader.Close()
_ = f.Close()
}()
encoding = input.Header.Get(ContentType)
d, err = h.Decoder(ctx, encoding, reader)
d, err = h.Decoder(ctx, file.Encoding, reader)
if err != nil {
err = &BadRequestError{err.Error()}
_ = ctx.Error(err)
Expand Down Expand Up @@ -2860,3 +2875,116 @@ func (r *yamlEncoder) embed(object any) encoder {
r.write(s)
return r
}

// ManifestReader analysis manifest reader.
// The manifest contains 3 sections containing documents delimited by markers.
// The manifest must contain ALL markers even when sections are empty.
// Note: `^]` = `\x1D` = GS (group separator).
// Section markers:
//
// ^]BEGIN-MAIN^]
// ^]END-MAIN^]
// ^]BEGIN-ISSUES^]
// ^]END-ISSUES^]
// ^]BEGIN-DEPS^]
// ^]END-DEPS^]
type ManifestReader struct {
file *os.File
marker map[string]int64
begin int64
end int64
read int64
}

// scan manifest and catalog position of markers.
func (r *ManifestReader) scan(path string) (err error) {
if r.marker != nil {
return
}
r.file, err = os.Open(path)
if err != nil {
return
}
defer func() {
_ = r.file.Close()
}()
pattern, err := regexp.Compile(`^\x1D[A-Z-]+\x1D$`)
if err != nil {
return
}
p := int64(0)
r.marker = make(map[string]int64)
scanner := bufio.NewScanner(r.file)
for scanner.Scan() {
content := scanner.Text()
matched := strings.TrimSpace(content)
if pattern.Match([]byte(matched)) {
r.marker[matched] = p
}
p += int64(len(content))
p++
}

return
}

// open returns a read delimited by the specified markers.
func (r *ManifestReader) open(path, begin, end string) (reader io.ReadCloser, err error) {
found := false
err = r.scan(path)
if err != nil {
return
}
r.begin, found = r.marker[begin]
if !found {
err = &BadRequestError{
Reason: fmt.Sprintf("marker: %s not found.", begin),
}
return
}
r.end, found = r.marker[end]
if !found {
err = &BadRequestError{
Reason: fmt.Sprintf("marker: %s not found.", end),
}
return
}
if r.begin >= r.end {
err = &BadRequestError{
Reason: fmt.Sprintf("marker: %s must preceed %s.", begin, end),
}
return
}
r.begin += int64(len(begin))
r.begin++
r.read = r.end - r.begin
r.file, err = os.Open(path)
if err != nil {
return
}
_, err = r.file.Seek(r.begin, io.SeekStart)
reader = r
return
}

// Read bytes.
func (r *ManifestReader) Read(b []byte) (n int, err error) {
n, err = r.file.Read(b)
if n == 0 || err != nil {
return
}
if int64(n) > r.read {
n = int(r.read)
}
r.read -= int64(n)
if n < 1 {
err = io.EOF
}
return
}

// Close the reader.
func (r *ManifestReader) Close() (err error) {
err = r.file.Close()
return
}
Loading
Loading