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  }