github.com/moov-io/imagecashletter@v0.10.1/internal/files/files.go (about) 1 // Copyright 2020 The Moov Authors 2 // Use of this source code is governed by an Apache License 3 // license that can be found in the LICENSE file. 4 5 package files 6 7 import ( 8 "bufio" 9 "bytes" 10 "encoding/json" 11 "errors" 12 "fmt" 13 "io" 14 "net/http" 15 "os" 16 "strconv" 17 "strings" 18 19 "github.com/gorilla/mux" 20 "github.com/moov-io/base" 21 moovhttp "github.com/moov-io/base/http" 22 "github.com/moov-io/base/log" 23 "github.com/moov-io/imagecashletter" 24 "github.com/moov-io/imagecashletter/internal/metrics" 25 "github.com/moov-io/imagecashletter/internal/storage" 26 ) 27 28 var ( 29 errNoFileId = errors.New("no File ID found") 30 errNoCashLetterId = errors.New("no CashLetter ID found") 31 ) 32 33 func AppendRoutes(logger log.Logger, r *mux.Router, repo storage.ICLFileRepository) { 34 r.Methods("GET").Path("/files").HandlerFunc(getFiles(logger, repo)) 35 r.Methods("POST").Path("/files/create").HandlerFunc(createFile(logger, repo)) 36 r.Methods("GET").Path("/files/{fileId}").HandlerFunc(getFile(logger, repo)) 37 r.Methods("POST").Path("/files/{fileId}").HandlerFunc(updateFileHeader(logger, repo)) 38 r.Methods("DELETE").Path("/files/{fileId}").HandlerFunc(deleteFile(logger, repo)) 39 40 r.Methods("GET").Path("/files/{fileId}/contents").HandlerFunc(getFileContents(logger, repo)) 41 r.Methods("GET").Path("/files/{fileId}/validate").HandlerFunc(validateFile(logger, repo)) 42 43 r.Methods("POST").Path("/files/{fileId}/cashLetters").HandlerFunc(addCashLetterToFile(logger, repo)) 44 r.Methods("DELETE").Path("/files/{fileId}/cashLetters/{cashLetterId}").HandlerFunc(removeCashLetterFromFile(logger, repo)) 45 } 46 47 func getFileId(w http.ResponseWriter, r *http.Request) string { 48 v, ok := mux.Vars(r)["fileId"] 49 if !ok || v == "" { 50 moovhttp.Problem(w, errNoFileId) 51 return "" 52 } 53 return v 54 } 55 56 func getCashLetterId(w http.ResponseWriter, r *http.Request) string { 57 v, ok := mux.Vars(r)["cashLetterId"] 58 if !ok || v == "" { 59 moovhttp.Problem(w, errNoCashLetterId) 60 return "" 61 } 62 return v 63 } 64 65 func getFiles(logger log.Logger, repo storage.ICLFileRepository) http.HandlerFunc { 66 return func(w http.ResponseWriter, r *http.Request) { 67 if requestID := moovhttp.GetRequestID(r); requestID != "" { 68 logger = logger.Set("requestID", log.String(requestID)) 69 } 70 71 w = metrics.WrapResponseWriter(logger, w, r) 72 73 files, err := repo.GetFiles() // TODO(adam): implement soft and hard limits 74 if err != nil { 75 err = logger.LogErrorf("error getting ICL files: %v", err).Err() 76 moovhttp.Problem(w, err) 77 return 78 } 79 logger.Logf("found %d files", len(files)) 80 81 w.Header().Set("X-Total-Count", fmt.Sprintf("%d", len(files))) 82 w.Header().Set("Content-Type", "application/json; charset=utf-8") 83 w.WriteHeader(http.StatusOK) 84 json.NewEncoder(w).Encode(files) 85 } 86 } 87 88 var ( 89 maxReaderBufferSize = determineBufferSize("READER_BUFFER_SIZE", bufio.MaxScanTokenSize) 90 ) 91 92 func determineBufferSize(env string, nominal int) int { 93 v, exists := os.LookupEnv(env) 94 if exists { 95 n, _ := strconv.ParseInt(v, 10, 32) 96 return int(n) 97 } 98 return nominal 99 } 100 101 func createFile(logger log.Logger, repo storage.ICLFileRepository) http.HandlerFunc { 102 return func(w http.ResponseWriter, r *http.Request) { 103 if requestID := moovhttp.GetRequestID(r); requestID != "" { 104 logger = logger.Set("requestID", log.String(requestID)) 105 } 106 107 w = metrics.WrapResponseWriter(logger, w, r) 108 109 req := imagecashletter.NewFile() 110 if req.ID == "" { 111 req.ID = base.ID() 112 } 113 114 bs, err := io.ReadAll(r.Body) 115 if err != nil { 116 err = logger.LogErrorf("error reading request body: %v", err).Err() 117 moovhttp.Problem(w, err) 118 return 119 } 120 121 h := r.Header.Get("Content-Type") 122 if strings.Contains(h, "application/json") { 123 file, err := imagecashletter.FileFromJSON(bs) 124 if err != nil { 125 err = logger.LogErrorf("error creating file from JSON: %v", err).Err() 126 moovhttp.Problem(w, err) 127 return 128 } else { 129 req = file 130 } 131 } else { 132 reader := bytes.NewReader(bs) 133 opts := []imagecashletter.ReaderOption{ 134 imagecashletter.ReadVariableLineLengthOption(), 135 imagecashletter.ReadEbcdicEncodingOption(), 136 imagecashletter.BufferSizeOption(maxReaderBufferSize), 137 } 138 f, err := imagecashletter.NewReader(reader, opts...).Read() 139 if err != nil { 140 err = logger.LogErrorf("error reading image cache letter: %v", err).Err() 141 moovhttp.Problem(w, err) 142 return 143 } else { 144 req = &f 145 } 146 } 147 if req.ID == "" { 148 req.ID = base.ID() 149 } 150 151 // Save the ICL file 152 if err := repo.SaveFile(req); err != nil { 153 err = logger.LogErrorf("problem saving file %s: %v", req.ID, err).Err() 154 moovhttp.Problem(w, err) 155 return 156 } 157 logger.Logf("created file=%s", req.ID) 158 159 w.Header().Set("Content-Type", "application/json; charset=utf-8") 160 w.WriteHeader(http.StatusCreated) 161 json.NewEncoder(w).Encode(req) 162 } 163 } 164 165 func getFile(logger log.Logger, repo storage.ICLFileRepository) http.HandlerFunc { 166 return func(w http.ResponseWriter, r *http.Request) { 167 if requestID := moovhttp.GetRequestID(r); requestID != "" { 168 logger = logger.Set("requestID", log.String(requestID)) 169 } 170 171 w = metrics.WrapResponseWriter(logger, w, r) 172 173 fileId := getFileId(w, r) 174 if fileId == "" { 175 return 176 } 177 178 file, err := repo.GetFile(fileId) 179 if err != nil { 180 err = logger.LogErrorf("problem reading file=%s: %v", fileId, err).Err() 181 moovhttp.Problem(w, err) 182 return 183 } 184 185 if file == nil { 186 logger.Logf("file %q was not found", fileId) 187 http.NotFound(w, r) 188 return 189 } 190 191 logger.Log("rendering file") 192 193 w.Header().Set("Content-Type", "application/json; charset=utf-8") 194 w.WriteHeader(http.StatusOK) 195 json.NewEncoder(w).Encode(file) 196 } 197 } 198 199 func updateFileHeader(logger log.Logger, repo storage.ICLFileRepository) http.HandlerFunc { 200 return func(w http.ResponseWriter, r *http.Request) { 201 if requestID := moovhttp.GetRequestID(r); requestID != "" { 202 logger = logger.Set("requestID", log.String(requestID)) 203 } 204 205 w = metrics.WrapResponseWriter(logger, w, r) 206 207 var req imagecashletter.FileHeader 208 if err := json.NewDecoder(r.Body).Decode(&req); err != nil { 209 err = logger.LogErrorf("error reading request body: %v", err).Err() 210 moovhttp.Problem(w, err) 211 return 212 } 213 214 fileId := getFileId(w, r) 215 if fileId == "" { 216 logger.LogError(errNoFileId) 217 return 218 } 219 logger = logger.Set("fileID", log.String(fileId)) 220 221 file, err := repo.GetFile(fileId) 222 if err != nil { 223 err = logger.LogErrorf("error retrieving file: %v", err).Err() 224 moovhttp.Problem(w, err) 225 return 226 } 227 228 if file == nil { 229 logger.Logf("file %q was not found", fileId) 230 http.NotFound(w, r) 231 return 232 } 233 234 file.Header = req 235 if err := repo.SaveFile(file); err != nil { 236 err = logger.LogErrorf("error saving file: %v", err).Err() 237 moovhttp.Problem(w, err) 238 return 239 } 240 logger.Log("updated FileHeader") 241 242 w.Header().Set("Content-Type", "application/json; charset=utf-8") 243 w.WriteHeader(http.StatusCreated) 244 json.NewEncoder(w).Encode(file) 245 } 246 } 247 248 func deleteFile(logger log.Logger, repo storage.ICLFileRepository) http.HandlerFunc { 249 return func(w http.ResponseWriter, r *http.Request) { 250 if requestID := moovhttp.GetRequestID(r); requestID != "" { 251 logger = logger.Set("requestID", log.String(requestID)) 252 } 253 254 w = metrics.WrapResponseWriter(logger, w, r) 255 256 fileId := getFileId(w, r) 257 if fileId == "" { 258 logger.LogError(errNoFileId) 259 return 260 } 261 logger = logger.Set("fileID", log.String(fileId)) 262 263 file, err := repo.GetFile(fileId) 264 if err != nil { 265 err = logger.LogErrorf("error retrieving file: %v", err).Err() 266 moovhttp.Problem(w, err) 267 return 268 } 269 270 if file == nil { 271 logger.Logf("file %q was not found", fileId) 272 http.NotFound(w, r) 273 return 274 } 275 276 if err := repo.DeleteFile(fileId); err != nil { 277 err = logger.LogErrorf("error deleting file: %v", err).Err() 278 moovhttp.Problem(w, err) 279 return 280 } 281 282 logger.Log("deleted file") 283 284 w.Header().Set("Content-Type", "application/json; charset=utf-8") 285 w.WriteHeader(http.StatusOK) 286 json.NewEncoder(w).Encode(`{"error": null}`) 287 } 288 } 289 290 func getFileContents(logger log.Logger, repo storage.ICLFileRepository) http.HandlerFunc { 291 return func(w http.ResponseWriter, r *http.Request) { 292 if requestID := moovhttp.GetRequestID(r); requestID != "" { 293 logger = logger.Set("requestID", log.String(requestID)) 294 } 295 296 w = metrics.WrapResponseWriter(logger, w, r) 297 298 fileId := getFileId(w, r) 299 if fileId == "" { 300 logger.LogError(errNoFileId) 301 return 302 } 303 logger = logger.Set("fileID", log.String(fileId)) 304 305 file, err := repo.GetFile(fileId) 306 if err != nil { 307 err = logger.LogErrorf("error retrieving file: %v", err).Err() 308 moovhttp.Problem(w, err) 309 return 310 } 311 312 if file == nil { 313 logger.Logf("file %q was not found", fileId) 314 http.NotFound(w, r) 315 return 316 } 317 318 logger.Log("rendering file contents") 319 320 opts := []imagecashletter.WriterOption{ 321 imagecashletter.WriteVariableLineLengthOption(), 322 imagecashletter.WriteEbcdicEncodingOption(), 323 } 324 325 w.Header().Set("Content-Type", "text/plain") 326 if err := imagecashletter.NewWriter(w, opts...).Write(file); err != nil { 327 err = logger.LogErrorf("problem rendering file contents: %v", err).Err() 328 moovhttp.Problem(w, err) 329 return 330 } 331 w.WriteHeader(http.StatusOK) 332 } 333 } 334 335 func validateFile(logger log.Logger, repo storage.ICLFileRepository) http.HandlerFunc { 336 return func(w http.ResponseWriter, r *http.Request) { 337 if requestID := moovhttp.GetRequestID(r); requestID != "" { 338 logger = logger.Set("requestID", log.String(requestID)) 339 } 340 341 w = metrics.WrapResponseWriter(logger, w, r) 342 343 fileId := getFileId(w, r) 344 if fileId == "" { 345 logger.LogError(errNoFileId) 346 return 347 } 348 logger = logger.Set("fileID", log.String(fileId)) 349 350 file, err := repo.GetFile(fileId) 351 if err != nil { 352 err = logger.LogErrorf("error retrieving file: %v", err).Err() 353 moovhttp.Problem(w, err) 354 return 355 } 356 357 if file == nil { 358 logger.Logf("file %q was not found", fileId) 359 http.NotFound(w, r) 360 return 361 } 362 363 if err := file.Create(); err != nil { // Create calls Validate 364 err = logger.LogErrorf("file=%s was invalid: %v", fileId, err).Err() 365 moovhttp.Problem(w, err) 366 return 367 } 368 369 logger.Log("validated file") 370 371 w.Header().Set("Content-Type", "application/json; charset=utf-8") 372 w.WriteHeader(http.StatusOK) 373 json.NewEncoder(w).Encode(`{"error": null}`) 374 } 375 } 376 377 func addCashLetterToFile(logger log.Logger, repo storage.ICLFileRepository) http.HandlerFunc { 378 return func(w http.ResponseWriter, r *http.Request) { 379 if requestID := moovhttp.GetRequestID(r); requestID != "" { 380 logger = logger.Set("requestID", log.String(requestID)) 381 } 382 383 w = metrics.WrapResponseWriter(logger, w, r) 384 385 var req imagecashletter.CashLetter 386 if err := json.NewDecoder(r.Body).Decode(&req); err != nil { 387 err = logger.LogErrorf("error reading request body: %v", err).Err() 388 moovhttp.Problem(w, err) 389 return 390 } 391 392 fileId := getFileId(w, r) 393 if fileId == "" { 394 logger.LogError(errNoFileId) 395 return 396 } 397 logger = logger.Set("fileID", log.String(fileId)) 398 399 file, err := repo.GetFile(fileId) 400 if err != nil { 401 err = logger.LogErrorf("error retrieving file: %v", err).Err() 402 moovhttp.Problem(w, err) 403 return 404 } 405 406 if file == nil { 407 logger.Logf("file %q was not found", fileId) 408 http.NotFound(w, r) 409 return 410 } 411 412 file.CashLetters = append(file.CashLetters, req) 413 if err := repo.SaveFile(file); err != nil { 414 err = logger.LogErrorf("error saving file: %v", err).Err() 415 moovhttp.Problem(w, err) 416 return 417 } 418 419 logger.Logf("added CashLetter=%s to file", req.ID) 420 421 w.Header().Set("Content-Type", "application/json; charset=utf-8") 422 w.WriteHeader(http.StatusOK) 423 json.NewEncoder(w).Encode(file) 424 } 425 } 426 427 func removeCashLetterFromFile(logger log.Logger, repo storage.ICLFileRepository) http.HandlerFunc { 428 return func(w http.ResponseWriter, r *http.Request) { 429 if requestID := moovhttp.GetRequestID(r); requestID != "" { 430 logger = logger.Set("requestID", log.String(requestID)) 431 } 432 433 w = metrics.WrapResponseWriter(logger, w, r) 434 435 fileId := getFileId(w, r) 436 if fileId == "" { 437 logger.LogError(errNoFileId) 438 return 439 } 440 logger = logger.Set("fileID", log.String(fileId)) 441 442 cashLetterId := getCashLetterId(w, r) 443 if cashLetterId == "" { 444 logger.LogError(errNoCashLetterId) 445 return 446 } 447 logger = logger.Set("cashLetterID", log.String(cashLetterId)) 448 449 file, err := repo.GetFile(fileId) 450 if err != nil { 451 err = logger.LogErrorf("error retrieving file: %v", err).Err() 452 moovhttp.Problem(w, err) 453 return 454 } 455 456 if file == nil { 457 logger.Logf("file %q was not found", fileId) 458 http.NotFound(w, r) 459 return 460 } 461 462 for i := 0; i < len(file.CashLetters); i++ { 463 if file.CashLetters[i].ID == cashLetterId { 464 file.CashLetters = append(file.CashLetters[:i], file.CashLetters[i+1:]...) 465 i-- 466 } 467 } 468 if err := repo.SaveFile(file); err != nil { 469 err = logger.LogErrorf("error saving file: %v", err).Err() 470 moovhttp.Problem(w, err) 471 return 472 } 473 474 logger.Log("removed CashLetter from file") 475 476 w.Header().Set("Content-Type", "application/json; charset=utf-8") 477 w.WriteHeader(http.StatusOK) 478 json.NewEncoder(w).Encode(`{"error": null}`) 479 } 480 }