github.com/moov-io/imagecashletter@v0.10.1/internal/files/v2/files.go (about) 1 package v2 2 3 import ( 4 "bufio" 5 "fmt" 6 "io" 7 "net/http" 8 "os" 9 "strconv" 10 "strings" 11 12 "github.com/google/uuid" 13 "github.com/gorilla/mux" 14 "github.com/moov-io/base/log" 15 "github.com/moov-io/imagecashletter" 16 "github.com/moov-io/imagecashletter/internal/metrics" 17 "github.com/moov-io/imagecashletter/internal/responder" 18 "github.com/moov-io/imagecashletter/internal/storage" 19 ) 20 21 var ( 22 maxReaderBufferSize = func() int { 23 v, exists := os.LookupEnv("READER_BUFFER_SIZE") 24 if exists { 25 n, _ := strconv.ParseInt(v, 10, 32) 26 return int(n) 27 } 28 return bufio.MaxScanTokenSize 29 }() 30 ) 31 32 type Controller struct { 33 logger log.Logger 34 repo storage.ICLFileRepository 35 } 36 37 func NewController(logger log.Logger, fileRepo storage.ICLFileRepository) Controller { 38 return Controller{ 39 logger: logger, 40 repo: fileRepo, 41 } 42 } 43 44 func (c Controller) AddRoutes(router *mux.Router) { 45 v2Routes := router.PathPrefix("/v2").Subrouter() 46 47 v2Routes. 48 Path("/files"). 49 Methods(http.MethodPost). 50 HandlerFunc(c.createFile) 51 52 } 53 54 func (c Controller) createFile(w http.ResponseWriter, r *http.Request) { 55 w = metrics.WrapResponseWriter(c.logger, w, r) 56 respond := responder.NewResponder(c.logger, w, r) 57 58 var created *imagecashletter.File 59 var err error 60 61 contentType := r.Header.Get("Content-Type") 62 switch { 63 case strings.Contains(contentType, "application/json"): 64 created, err = c.fileFromJSON(r) 65 case strings.Contains(contentType, "multipart/form-data"): 66 created, err = c.fileFromForm(r) 67 default: 68 err = fmt.Errorf("missing or unsupported Content-Type: %s", contentType) 69 } 70 71 if err != nil { 72 c.logger.Error().LogErrorf("creating file: %v", err) 73 respond.Error(http.StatusBadRequest, err) 74 return 75 } 76 77 if err = c.repo.SaveFile(created); err != nil { 78 c.logger.Error().LogErrorf("saving created file: %v", err) 79 respond.Error(http.StatusInternalServerError, err) 80 return 81 } 82 83 // TODO: Update to the v2 API endpoint once the GET file endpoint is implemented 84 location := fmt.Sprintf("%s://%s/files/%s", r.URL.Scheme, r.URL.Host, created.ID) 85 respond = respond.WithLocation(location) 86 if expectingFile(r) { 87 respond.File(http.StatusCreated, *created, fmt.Sprintf("%s.x9", created.ID)) 88 return 89 } 90 91 respond.JSON(http.StatusCreated, created) 92 } 93 94 func expectingFile(r *http.Request) bool { 95 mimeType := r.Header.Get("Accept") 96 return mimeType == "application/octet-stream" || mimeType == "text/plain" 97 } 98 99 func (c Controller) fileFromJSON(r *http.Request) (*imagecashletter.File, error) { 100 contents, err := io.ReadAll(r.Body) 101 if err != nil { 102 return nil, fmt.Errorf("reading request body: %w", err) 103 } 104 105 file, err := imagecashletter.FileFromJSON(contents) 106 if err != nil { 107 return nil, fmt.Errorf("parsing request body: %w", err) 108 } 109 file.ID = uuid.NewString() 110 111 return file, nil 112 } 113 114 func (c Controller) fileFromForm(r *http.Request) (*imagecashletter.File, error) { 115 if err := r.ParseMultipartForm(int64(maxReaderBufferSize)); err != nil { 116 return nil, fmt.Errorf("parsing multipart form: %v", err) 117 } 118 119 formFile, hdr, err := r.FormFile("file") 120 if err != nil { 121 return nil, fmt.Errorf("reading form file: %w", err) 122 } 123 124 opts := []imagecashletter.ReaderOption{ 125 imagecashletter.ReadVariableLineLengthOption(), 126 imagecashletter.BufferSizeOption(maxReaderBufferSize), 127 } 128 129 // Industry standard encoding is EBCDIC, so unless plain/text was 130 // explicitly requested, default to EBCDIC. 131 contentType := hdr.Header.Get("Content-Type") 132 if contentType != "text/plain" { 133 opts = append(opts, imagecashletter.ReadEbcdicEncodingOption()) 134 } 135 136 file, err := imagecashletter.NewReader(formFile, opts...).Read() 137 if err != nil { 138 return nil, fmt.Errorf("parsing file: %w", err) 139 } 140 file.ID = uuid.NewString() 141 142 return &file, nil 143 }