From 0171bd2c49c3b75e21a0cbd8e72e572b1fd39a1e Mon Sep 17 00:00:00 2001 From: sp Date: Mon, 5 Apr 2021 03:29:34 +1000 Subject: [PATCH] uploads with pdf/image/format --- .gitignore | 1 + UploadsOnDisk.go | 275 ++++++++++++++++++++++++++++++++++++ UploadsOnDisk_test.go | 44 ++++++ apiV1UploadAnalysis.go | 35 +++++ apiV1Uploads.go | 222 ++++++++++++++++++++++++----- apiv1.go | 8 ++ assets/no_preview.jpg | Bin 0 -> 49017 bytes assets/no_preview.pdf | Bin 0 -> 31031 bytes assets/thumb_file_icon.webp | Bin 0 -> 4646 bytes config.go | 124 ++++++++++++++-- config.json | 12 +- main_test.go | 15 ++ pay-in-decode.go | 67 +++++---- pay-in-decode_test.go | 10 +- payIn-AAA.go | 12 +- 15 files changed, 732 insertions(+), 93 deletions(-) create mode 100644 UploadsOnDisk.go create mode 100644 UploadsOnDisk_test.go create mode 100644 apiV1UploadAnalysis.go create mode 100644 assets/no_preview.jpg create mode 100644 assets/no_preview.pdf create mode 100644 assets/thumb_file_icon.webp diff --git a/.gitignore b/.gitignore index 5c5050a..7b322d4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /apiv1 /uploads/ +/tmp/ diff --git a/UploadsOnDisk.go b/UploadsOnDisk.go new file mode 100644 index 0000000..8a11942 --- /dev/null +++ b/UploadsOnDisk.go @@ -0,0 +1,275 @@ +package main + +import ( + "biukop.com/sfm/loan" + "errors" + log "github.com/sirupsen/logrus" + "io/ioutil" + "net/http" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" +) + +type uploadsOnDisk struct { + Upload loan.Uploads +} + +func (m *uploadsOnDisk) convertUploadsToPDF() (e error) { + return convertUploadsToPDF(m.Upload) +} +func convertUploadsToPDF(ul loan.Uploads) (e error) { + m := uploadsOnDisk{} + m.Upload = ul + + if strings.Contains(strings.ToLower(ul.Format), "excel") || + strings.Contains(strings.ToLower(ul.Format), "spreadsheet") { + e = m.convertExcelToPDF() + return // excel is converted + } + + if strings.Contains(strings.ToLower(ul.Format), "/pdf") { + return // no need to convert + } + + e = errors.New("don't know how to convert file to PDF") + log.Error("don't know how to convert file to PDF", ul) + return +} + +func (m *uploadsOnDisk) convertUploadsToJpg() (e error) { + return convertUploadsToJpg(m.Upload) +} + +func convertUploadsToJpg(ul loan.Uploads) (e error) { + m := uploadsOnDisk{} + m.Upload = ul + + if strings.Contains(strings.ToLower(ul.Format), "excel") || + strings.Contains(strings.ToLower(ul.Format), "spreadsheet") { + e = m.convertExcelToJpg() + return // excel is converted + } + if strings.Contains(strings.ToLower(ul.Format), "/pdf") { + e = m.convertPDFToJpg() + return // excel is converted + } + + e = errors.New("don't know how to convert file to image") + log.Error("don't know how to convert file to image", ul) + return +} + +func (m *uploadsOnDisk) convertUploadsToThumb() (e error) { + return convertUploadsToThumb(m.Upload) +} +func convertUploadsToThumb(ul loan.Uploads) (e error) { + m := uploadsOnDisk{} + m.Upload = ul + if !fileExists(m.jpgPath()) { + e = m.convertUploadsToJpg() + if e != nil { + return + } + } + + e = ConvertImageToThumbnail(m.jpgPath(), m.thumbPath(), 256) + if e != nil { + log.Error("cannot create thumbnail for uploads", m.Upload, e) + return + } + return +} + +func (m *uploadsOnDisk) filePath() string { + return config.UploadsDir.FileDir + strconv.Itoa(int(m.Upload.Id)) + ".uploads" +} + +func (m *uploadsOnDisk) jpgPath() string { + return config.UploadsDir.JpgDir + strconv.Itoa(int(m.Upload.Id)) + ".jpg" +} + +func (m *uploadsOnDisk) thumbPath() string { + return config.UploadsDir.ThumbDir + strconv.Itoa(int(m.Upload.Id)) + ".webp" +} + +func (m *uploadsOnDisk) pdfPath() string { + return config.UploadsDir.PdfDir + strconv.Itoa(int(m.Upload.Id)) + ".pdf" +} +func (m *uploadsOnDisk) convertExcelToPDF() (e error) { + if fileExists(m.pdfPath()) { + log.Info("Skip conversion excel to PDF , already exists", m) + return + } + return m.convertExcelTo("pdf") +} + +func (m *uploadsOnDisk) convertExcelToJpg() (e error) { + if fileExists(m.jpgPath()) { + log.Info("Skip conversion excel to Jpg , already exists", m) + return + } + return m.convertExcelTo("jpg") +} + +func (m *uploadsOnDisk) convertExcelTo(format string) (e error) { + if format != "pdf" && format != "jpg" { + e = errors.New("unsupported format") + return + } + + dst := m.jpgPath() + if format == "pdf" { + dst = m.pdfPath() + } + + dir, e := ioutil.TempDir(config.TempDir, "tmp-convert-xls-to-"+format+"-") + if e != nil { + log.Error("cannot create tmp dir for converting image", m.Upload, e) + return + } + defer os.RemoveAll(dir) + + cmd := exec.Command("libreoffice", "--convert-to", format, "--outdir", dir, m.filePath()) + strCmd := cmd.String() + log.Debug("command is ", strCmd) + out, e := cmd.Output() + if e != nil { + log.Error("cannot converting Excel to "+format+":", m.Upload, e) + return + } else { // success + log.Info("convert to "+format, m.Upload, " output: ", string(out)) + } + + _, name := filepath.Split(m.filePath()) + + src := dir + string(os.PathSeparator) + fileNameWithoutExtTrimSuffix(name) + "." + format + e = os.Rename(src, dst) // there should be only one jpg + + return +} + +// first page to thumbnail +// all page to single jpg +func (m *uploadsOnDisk) convertPDFToJpg() (e error) { + + if fileExists(m.jpgPath()) { + // no need to reconvert it again + log.Info("PDF to JPG skipped it already exists ", m) + return + } + + dir, e := ioutil.TempDir(config.TempDir, "tmp-convert-pdf-to-jpg-") + if e != nil { + log.Error("cannot create tmp dir for converting image", m.Upload, e) + return + } + defer os.RemoveAll(dir) + + // convert -density 3000 abc.pdf path/tmp/result.png + // could be path/tmp/result-0, result-1, result-2, ... png + target := dir + string(os.PathSeparator) + "result.jpg" //.jpg suffix is important + cmd := exec.Command("convert", "-density", "300", m.filePath(), target) + strCmd := cmd.String() + log.Debug("command is ", strCmd) + out, e := cmd.Output() + if e != nil { + log.Error("cannot create png file for PDF", m.Upload, e) + return + } else { + log.Info("convert ", m.Upload, " output: ", string(out)) + } + + // montage -mode concatenate -tile 1x 30*png 30.jpg + if fileExists(target) { // single file, + e = os.Rename(target, m.jpgPath()) // there should be only one jpg + _ = ConvertImageToThumbnail(target, m.thumbPath(), 256) + } else { // multi-page, we have -0 -1 -2 -3 -4 files + firstPage := dir + string(os.PathSeparator) + "result-0.jpg" + _ = ConvertImageToThumbnail(firstPage, m.thumbPath(), 256) + + batch := dir + string(os.PathSeparator) + "result*jpg" // result* is important + target = dir + string(os.PathSeparator) + "final.jpg" // .jpg suffix is important + cmd = exec.Command("montage", "-mode", "concatenate", "-tile", "1x", batch, target) + + strCmd = cmd.String() + log.Debug("command is ", strCmd) + out, e = cmd.Output() + if e != nil { + return + } else { + log.Info("montage ", m, " output: ", string(out)) + } + e = os.Rename(target, m.jpgPath()) // give combined file to target + } + return +} + +func (m *uploadsOnDisk) GetFileType() (ret string, e error) { + strMime, e := GetFileContentType(m.filePath()) + if e != nil { + return + } + + if strings.ToLower(strMime) == "application/pdf" { + return "pdf", nil + } + + if strings.ToLower(strMime) == "application/vnd.ms-excel" { + return "excel", nil + } + + if strings.ToLower(strMime) == "application/zip" && + strings.ToLower(m.Upload.Format) == + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" { + return "opensheet", nil + } + + return "", nil +} + +// tested, not accurate with xls, xlsx, it becomes zip and octstream sometime. +func GetFileContentType(filename string) (contentType string, e error) { + contentType = "" + input, e := os.OpenFile(filename, os.O_RDONLY, 0755) + // Only the first 512 bytes are used to sniff the content type. + buffer := make([]byte, 512) + + _, e = input.Read(buffer) + if e != nil { + return + } + + // Use the net/http package's handy DectectContentType function. Always returns a valid + // content-type by returning "application/octet-stream" if no others seemed to match. + contentType = http.DetectContentType(buffer) + return +} + +func ConvertImageToThumbnail(srcPath string, dstPath string, size int) (e error) { + if fileExists(dstPath) { + log.Info("skip converting thumbnail it exists", dstPath) + return + } + if size <= 0 { + size = 256 + } + // convert -thumbnail 200 abc.png thumb.abc.png + cmd := exec.Command("convert", "-thumbnail", strconv.Itoa(size), srcPath, dstPath) + strCmd := cmd.String() + log.Debug("create thumbnail: ", strCmd) + + out, e := cmd.Output() + if e != nil { + return + } else { // success + log.Info("success output: \n: ", string(out)) + } + return +} + +func fileNameWithoutExtTrimSuffix(fileName string) string { + return strings.TrimSuffix(fileName, filepath.Ext(fileName)) +} diff --git a/UploadsOnDisk_test.go b/UploadsOnDisk_test.go new file mode 100644 index 0000000..c4b28c6 --- /dev/null +++ b/UploadsOnDisk_test.go @@ -0,0 +1,44 @@ +package main + +import ( + "biukop.com/sfm/loan" + log "github.com/sirupsen/logrus" + "path/filepath" + "testing" +) + +func TestGetConvertUploadsToImage(t *testing.T) { + for id := 30; id <= 44; id++ { + ul := loan.Uploads{} + e := ul.Read(int64(id)) + if e != nil { + log.Error(e) + return + } + + e = convertUploadsToPDF(ul) + if e != nil { + log.Error(e) + return + } + + e = convertUploadsToJpg(ul) + if e != nil { + log.Error(e) + return + } + + e = convertUploadsToThumb(ul) + if e != nil { + log.Error(e) + return + } + } + +} + +func TestConvertImageToThumbnail(t *testing.T) { + _, name := filepath.Split("/home/sp/uploads/abc.uploads") + test := fileNameWithoutExtTrimSuffix(name) + ".jpg" + log.Println(test) +} diff --git a/apiV1UploadAnalysis.go b/apiV1UploadAnalysis.go new file mode 100644 index 0000000..1e8e9d7 --- /dev/null +++ b/apiV1UploadAnalysis.go @@ -0,0 +1,35 @@ +package main + +import ( + "biukop.com/sfm/loan" + log "github.com/sirupsen/logrus" + "net/http" + "strconv" +) + +func apiV1UploadAnalysis(w http.ResponseWriter, r *http.Request, ss *loan.Session) { + strId := r.URL.Path[len(apiV1Prefix+"upload-analysis/"):] //remove prefix + Id, e := strconv.Atoi(strId) + if e != nil { + log.Error("Invalid uploads Id cannot convert to integer", Id, e) + apiV1Client403Error(w, r, ss) + return + } + + ul := loan.Uploads{} + e = ul.Read(int64(Id)) + if e != nil { + log.Error("Upload not found or read error from db", Id, e) + apiV1Client404Error(w, r, ss) + return + } + + ai := AiDecodeIncome{} + e = ai.decodeUploadToPayIn(ul) + if e != nil { + log.Error("Invalid uploads Id cannot conver to integer", Id, e) + apiV1Server500Error(w, r) + return + } + apiV1SendJson(ai, w, r, ss) +} diff --git a/apiV1Uploads.go b/apiV1Uploads.go index a8224f2..7f0073b 100644 --- a/apiV1Uploads.go +++ b/apiV1Uploads.go @@ -10,13 +10,13 @@ import ( "io/ioutil" "net/http" "os" - "path/filepath" "strconv" + "strings" "time" ) func apiV1UploadsGet(w http.ResponseWriter, r *http.Request, ss *loan.Session) { - id := r.URL.Path[len(apiV1Prefix+"lender-upload/"):] //remove prefix + id := r.URL.Path[len(apiV1Prefix+"upload/"):] //remove prefix intId, e := strconv.Atoi(id) if e != nil { log.Println("invalid id for upload get", id, e) @@ -24,8 +24,8 @@ func apiV1UploadsGet(w http.ResponseWriter, r *http.Request, ss *loan.Session) { return } - ul := loan.Uploads{} - e = ul.Read(int64(intId)) + ul := uploadsOnDisk{} + e = ul.Upload.Read(int64(intId)) if e != nil { log.Println("no file uploaded", intId, e) apiV1Client404Error(w, r, ss) // bad request @@ -33,7 +33,7 @@ func apiV1UploadsGet(w http.ResponseWriter, r *http.Request, ss *loan.Session) { } //check local file first - path := config.Uploads + strconv.FormatInt(ul.Id, 10) + ".uploads" + path := ul.filePath() if fileExists(path) { http.ServeFile(w, r, path) return @@ -46,9 +46,9 @@ func apiV1UploadsGet(w http.ResponseWriter, r *http.Request, ss *loan.Session) { func apiV1UploadsPost(w http.ResponseWriter, r *http.Request, ss *loan.Session) { id := r.URL.Path[len(apiV1Prefix+"lender-upload/"):] //remove prefix - filename, e := saveUploadToFile(r) + filepath, e := saveUploadToFile(r) if e != nil { - log.Println("no file uploaded", filename, e) + log.Println("no file uploaded", filepath, e) apiV1Client404Error(w, r, ss) // bad request return } @@ -60,9 +60,9 @@ func apiV1UploadsPost(w http.ResponseWriter, r *http.Request, ss *loan.Session) apiV1Client404Error(w, r, ss) // bad request return } - updateUploads(int64(intId), filename, w, r, ss) + updateUploads(int64(intId), filepath, w, r, ss) } else { - createUploads(filename, w, r, ss) + createUploads(filepath, w, r, ss) } } @@ -76,18 +76,19 @@ func sha256File(input io.Reader) string { return fmt.Sprintf("%x", sum) } -func updateUploads(id int64, fileName string, w http.ResponseWriter, r *http.Request, ss *loan.Session) { - ul := loan.Uploads{} - e := ul.Read(int64(id)) +func updateUploads(id int64, filePath string, w http.ResponseWriter, r *http.Request, ss *loan.Session) { + ul := uploadsOnDisk{} + e := ul.Upload.Read(int64(id)) if e != nil { log.Println("bad upload id is given ", id, e) apiV1Client404Error(w, r, ss) // bad request return } - ul1, _, e := saveUploadsToDB(id, fileName, r, ss) + ul1, isDuplicate, e := saveUploadsMetaToDB(id, filePath, r, ss) + ul.Upload.IsDuplicate = isDuplicate if e != nil { - os.Remove(config.Uploads + ul.FileName) + os.Remove(ul.filePath()) ul1.Delete() log.Println("cannot save file info to db ", e) apiV1Server500Error(w, r) // bad request @@ -97,28 +98,35 @@ func updateUploads(id int64, fileName string, w http.ResponseWriter, r *http.Req apiV1SendJson(ul1, w, r, ss) } -func createUploads(fileName string, w http.ResponseWriter, r *http.Request, ss *loan.Session) { - ul, _, e := saveUploadsToDB(0, fileName, r, ss) +func createUploads(filePath string, w http.ResponseWriter, r *http.Request, ss *loan.Session) { + ul := uploadsOnDisk{} + ulMeta, isDuplicate, e := saveUploadsMetaToDB(0, filePath, r, ss) + ul.Upload = ulMeta + ul.Upload.IsDuplicate = isDuplicate if e != nil { log.Println("cannot save file info to db ", e) - e = ul.Delete() // delete the newly created, if failed, db will clean it + e = ulMeta.Delete() // delete the newly created, if failed, db will clean it if e != nil { log.Error("failed to remove unused uploads", ul) } - e = os.Remove(config.Uploads + fileName) + e = os.Remove(ul.filePath()) if e != nil { - log.Error("failed to remove unused temp file", fileName) + log.Error("failed to remove unused temp file", filePath) } apiV1Server500Error(w, r) // bad request return } - apiV1SendJson(ul, w, r, ss) + + ai := AiDecodeIncome{} + ai.decodeUploadToPayIn(ul.Upload) + + apiV1SendJson(ai, w, r, ss) } -func saveUploadsToDB(id int64, fileName string, - r *http.Request, ss *loan.Session) (ul loan.Uploads, duplicate bool, e error) { +func saveUploadsMetaToDB(id int64, filePath string, + r *http.Request, ss *loan.Session) (ulMeta loan.Uploads, duplicate bool, e error) { duplicate = false e = r.ParseMultipartForm(10 << 20) // we should have ready parsed this, just in case if e != nil { @@ -128,27 +136,29 @@ func saveUploadsToDB(id int64, fileName string, file.Seek(0, 0) //seek to beginning checksum := sha256File(file) - ul.Id = id - ul.Ts = time.Now() - ul.FileName = header.Filename + ulMeta.Id = id + ulMeta.Ts = time.Now() + ulMeta.FileName = header.Filename file.Seek(0, 0) //seek to beginning - ul.Format = header.Header.Get("Content-type") - ul.Size = header.Size // necessary to prevent duplicate - ul.LastModified = 0 - ul.Sha256 = checksum // necessary to prevent duplicate - ul.By = ss.User - e = ul.Write() // this Id will have the real Id if there is duplicates + ulMeta.Format = header.Header.Get("Content-type") + ulMeta.Size = header.Size // necessary to prevent duplicate + ulMeta.LastModified = 0 + ulMeta.Sha256 = checksum // necessary to prevent duplicate + ulMeta.By = ss.User + e = ulMeta.Write() // this Id will have the real Id if there is duplicates if e != nil { - log.Error("Fail to update db ", ul, e) + log.Error("Fail to update db ", ulMeta, e) } else { - if id > 0 && ul.Id != id { + if (id > 0 && ulMeta.Id != id) || (id == 0 && ulMeta.IsDuplicate) { duplicate = true } - target := fmt.Sprintf("%d.uploads", ul.Id) - e = os.Rename(config.Uploads+fileName, config.Uploads+target) + ul := uploadsOnDisk{} + ul.Upload = ulMeta + e = os.Rename(filePath, ul.filePath()) if e != nil { - ul.FileName = fileName // some how failed to rename + os.Remove(filePath) + log.Error("fail to move file from ", filePath, "to", ul.filePath()) } } return @@ -165,7 +175,7 @@ func saveUploadToFile(r *http.Request) (filename string, e error) { return } - out, pathError := ioutil.TempFile(config.Uploads, "can-del-upload-*.tmp") + out, pathError := ioutil.TempFile(config.UploadsDir.FileDir, "can-del-upload-*.tmp") if pathError != nil { log.Println("Error Creating a file for writing", pathError) return @@ -182,5 +192,141 @@ func saveUploadToFile(r *http.Request) (filename string, e error) { if size != header.Size { e = errors.New("written file with incorrect size") } - return filepath.Base(out.Name()), e + return out.Name(), e +} + +func getRequestedUpload(w http.ResponseWriter, r *http.Request, ss *loan.Session) (ret uploadsOnDisk, e error) { + strId := r.URL.Path[len(apiV1Prefix+"upload-as-image/"):] //remove prefix + Id, e := strconv.Atoi(strId) + if e != nil { + log.Error("Invalid uploads Id cannot convert to integer", Id, e) + apiV1Client403Error(w, r, ss) + return + } + + ul := loan.Uploads{} + e = ul.Read(int64(Id)) + if e != nil { + log.Error("Upload not found or read error from db", Id, e) + apiV1Client404Error(w, r, ss) + return + } + + ret.Upload = ul + return +} + +func apiV1UploadAsImage(w http.ResponseWriter, r *http.Request, ss *loan.Session) { + ul, e := getRequestedUpload(w, r, ss) + if e != nil { + return + } + + // if this is image itself, serve it directly + if strings.Contains(strings.ToLower(ul.Upload.Format), "image") { + f, e := os.Open(ul.filePath()) + if e == nil { + defer f.Close() + fi, e := f.Stat() + if e == nil { + w.Header().Set("Content-Disposition", "attachment; filename="+ul.Upload.FileName) + http.ServeContent(w, r, ul.filePath(), fi.ModTime(), f) + return + } + } + // if we reach here, some err has happened + log.Error("failed to serve image file ", ul, e) + apiV1Server500Error(w, r) + return + } + + // see if a converted image exist, if not convert it and then send + if !fileExists(ul.jpgPath()) { + e = ul.convertUploadsToJpg() + if e != nil { + // serve a default no preview is available + http.ServeFile(w, r, config.UploadsDir.JpgDefault) + log.Error("error creating preview image", ul, e) + return + } + } + if fileExists(ul.jpgPath()) { + http.ServeFile(w, r, ul.jpgPath()) + return + } else { + apiV1Client404Error(w, r, ss) + } + +} +func apiV1UploadAsThumbnail(w http.ResponseWriter, r *http.Request, ss *loan.Session) { + ul, e := getRequestedUpload(w, r, ss) + if e != nil { + return + } + + // see if a thumbnail is available already + if !fileExists(ul.thumbPath()) { + e = ul.convertUploadsToThumb() + if e != nil { + // serve a default no preview is available + http.ServeFile(w, r, config.UploadsDir.ThumbDefault) + log.Error("error creating preview image", ul, e) + return + } + } + if fileExists(ul.thumbPath()) { + http.ServeFile(w, r, ul.thumbPath()) + return + } else { + apiV1Client404Error(w, r, ss) + } +} + +func apiV1UploadAPDF(w http.ResponseWriter, r *http.Request, ss *loan.Session) { + ul, e := getRequestedUpload(w, r, ss) + if e != nil { + return + } + + //get file type + fileType, e := ul.GetFileType() + if e != nil { + apiV1Client403Error(w, r, ss) + return + } + + // if its ready pdf, no need to convert + if fileType == "pdf" { + f, e := os.Open(ul.filePath()) + if e == nil { + defer f.Close() + fi, e := f.Stat() + if e == nil { + w.Header().Set("Content-Disposition", "attachment; filename="+ul.Upload.FileName) + http.ServeContent(w, r, ul.filePath(), fi.ModTime(), f) + return + } + } + // if we reach here, some err has happened + log.Error("failed to serve pdf file ", ul, e) + apiV1Server500Error(w, r) + return + } + + // see if a converted pdf exist, if not convert it and then send + if !fileExists(ul.pdfPath()) { + e = ul.convertUploadsToPDF() + if e != nil { + // serve a default no preview is available + http.ServeFile(w, r, config.UploadsDir.PdfDefault) + log.Error("error creating preview image", ul, e) + return + } + } + if fileExists(ul.pdfPath()) { + http.ServeFile(w, r, ul.pdfPath()) + return + } else { + apiV1Client404Error(w, r, ss) + } } diff --git a/apiv1.go b/apiv1.go index 19627cd..69c0752 100644 --- a/apiv1.go +++ b/apiv1.go @@ -73,6 +73,8 @@ func setupApiV1Handler() []apiV1HandlerMap { {"POST", "lender-upload/", apiV1UploadsPost}, {"GET", "lender-upload/", apiV1UploadsGet}, + {"GET", "upload-as-thumbnail/", apiV1UploadAsThumbnail}, + {"GET", "upload-as-pdf/", apiV1UploadAPDF}, {"GET", "login", apiV1DumpRequest}, } @@ -125,6 +127,12 @@ func setupApiV1Handler() []apiV1HandlerMap { {"POST", "lender-upload/", apiV1UploadsPost}, {"GET", "lender-upload/", apiV1UploadsGet}, + {"GET", "upload-analysis/", apiV1UploadAnalysis}, + {"GET", "upload-as-image/", apiV1UploadAsImage}, + {"GET", "upload-as-thumbnail/", apiV1UploadAsThumbnail}, + {"GET", "upload-as-pdf/", apiV1UploadAPDF}, + {"GET", "upload/", apiV1UploadsGet}, + {"GET", "login", apiV1EmptyResponse}, } } diff --git a/assets/no_preview.jpg b/assets/no_preview.jpg new file mode 100644 index 0000000000000000000000000000000000000000..23ea855f12cc9477ec70c1d1f3a5033de7c9851d GIT binary patch literal 49017 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 DgmkB} literal 0 HcmV?d00001 diff --git a/assets/no_preview.pdf b/assets/no_preview.pdf new file mode 100644 index 0000000000000000000000000000000000000000..f3634c6eab10e118b680e39cf4877e7f6463735c GIT binary patch literal 31031 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 #rSbE7e?ym zm(PglC6;7O@`40-n^oI1c)E2DTdz_k3G52`LjvQT$e?W+bb6i-y_BScT&Pxuf!t1F z`~!9Tn;wKO$Xw+4gAluG6gl%{&LS-=h&+m{*)wjt&g09_xj`1h`%ea)v1i{XNFZ)T zp=c#ZJyq#<1yOfQIP2Uv>n@Tpe>~nCGpy{mOENd)C^Q{gnP%5D%wZR?z7Cv5whHln zDSC4{WIm3Olu%b&2il;^GN{cd!n%zm9Q;Hm{{t3Ak~UYdV=do(C^S{z4(aB&4`E=k zQYMgfys%3yPmY?!*fW<5%gWhr7Igu*<$(p;77;o;zaxBF-+RZ85Hd^dD}FfV*VK2v zH0G;t`F81cl$9EiG9I#hcQ^D2Q8D<@P#}gG+dTg*^o_2btWaA_eC4@E9RW7YABQ|j z@rP)~{F9PK$me%QNrOjU^PZnEXo}-aA3v1ZUh5MlC`duLO$m=jDiZrxFAwt2lWgB> zzu33Cq7Ia94Wbhma>^Oy5a z9&Bb|??B!BKQ&99QAoH6^>kg?4WP4vs9%W}jFSZhXb~Kupwz)V582<7oJ7U}Y;LdG zx|ZLonL4r=eX`km!{+TVY4l*n#*LL7szs_3M|ypSCga>umRVdz)ROC?s4eeB+Y%`6 zlc}u+9>H~z9VE!n+rCO{&xzQHz15bzZf?F|sSgzWRlsOMaNn51{^TIEnElYAn_a^E zqdTkHsdc7GK!<#v0oPe|-uZ`pLAEq%GnNvAR#1DD)WmgMfp_LQ7QpWVS?3?2=d=UWF5bB)Lrdm;jVIw;|U*OS=&?7Z^Y^{$@HG1s*0kWBKjW6Gj z^jc>asw;U>kCT1`Jnyrc&8i1Lh0F4T#G`qBwh>@w; zQzsiE1t%Fr11EC>9zz--L4G7J($dNj&E^9@T3Wztd65D%dT6>HBVP3ScNj?mgJ=YC_-zc0dF4gLe^)|(5}+})v$Ns_0ucxV8v?=xw=n^NczAe#>|h`m z%!-y^wRMEq=^nuG!Hy+%EKxV@bK4GrM;daZt{7iP=$!!R}jxFrz% z1rWpr0e-Itz0Ugw1B0NnnSl-57H(`;pAArf+ZY0j(QGV#f&d5?C;xZl|4`rue&6s< zhWy^RpO9agW@zwdi>>T!EPk}t&;V#;VPyH8_Xlkq=!bQN2D}Ep(N+llaGHXV^=~T{ zW`aMlTl{7Q8vAYKhZOK<-T$GP=+6Dq&cApNsb__@k^P5_Xd&L;xiu8waEt$z6aTKm z|0iJdoh?J~JDUb8I|urq2!ir*@bLbi)Ih)T1OF!ZLlaH1VP@?3-_iU@@_*WQXal6B z|7*8cTK?e~X=z?jxPkrmUX>ITw6`}itp7=EKk5FW@qqsa3(o(8Z2CBt01W~i+5h?s z<7e_bQ8QaB3q414bfaiyhvp0XjoR~v-VatP@ISc6ROO9~feL0$MgTCHy-ovwp2`J$ zXS@83G88?dD-Zn6779Sm@%~sB)!>JUq9Y}4ZVnC(2uMtXL*#p)^mCjf_XrJ85 z0D7|j4~Ppr0sc1(3`S3>{|)0rYyBOAvj0<#hYQ__f7Jzn+0mom?-&=l%l?L;quYOA z9MFI2azoHOmVeb{X9q!k(O!;UV4T0exPF0g{{n;l0^|87_WkmNzxy5x`UUojK0^EX zZ@OR(=s)@h%!zLAUwr^ZpZ)#=1B20*RQyeji<9f`;{eRV4fzEI{i%QLZ1m8inl?We zet+zd5cdD|-SD>^P0z;ew*d}eXXoKWQx4OK$%>4dn#?ZuI{FPW7tn literal 0 HcmV?d00001 diff --git a/assets/thumb_file_icon.webp b/assets/thumb_file_icon.webp new file mode 100644 index 0000000000000000000000000000000000000000..550d62f6e8f2cb6b3caf719ca35f468ec5b6ee18 GIT binary patch literal 4646 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 5 { - row.LoanNUmber = el[0] + row.LoanNumber = el[0] row.Settlement, _ = time.Parse("02-Jan-06", el[1]) row.LoanAmount = m.currencyToFloat64(el[2]) row.Balance = m.currencyToFloat64(el[3])