From 5f47d33299fd94744ce9a90f1d5ab159cc452c4f Mon Sep 17 00:00:00 2001 From: sp Date: Sun, 25 Apr 2021 03:06:35 +1000 Subject: [PATCH] uploads seems working. --- .idea/SFM_Loan_RestApi.iml | 1 + UploadsOnDisk.go | 110 +++++++++++++++++++++--- apiV1UploadAnalysis.go | 30 +++++++ apiV1UploadAsImage.go | 95 +++++++++++++++++++++ apiV1UploadList.go | 31 +++++++ apiV1UploadThumb.go | 63 ++++++++++++++ apiV1Uploads.go | 162 +++++++----------------------------- apiV1UploadsAsPdf.go | 109 ++++++++++++++++++++++++ apiv1.go | 12 +++ assets/thumb_file_icon.webp | Bin 4646 -> 1986 bytes fileUtil.go | 38 +++++++++ 11 files changed, 506 insertions(+), 145 deletions(-) create mode 100644 apiV1UploadAsImage.go create mode 100644 apiV1UploadList.go create mode 100644 apiV1UploadThumb.go create mode 100644 apiV1UploadsAsPdf.go create mode 100644 fileUtil.go diff --git a/.idea/SFM_Loan_RestApi.iml b/.idea/SFM_Loan_RestApi.iml index 5e764c4..0dc9279 100644 --- a/.idea/SFM_Loan_RestApi.iml +++ b/.idea/SFM_Loan_RestApi.iml @@ -2,6 +2,7 @@ + diff --git a/UploadsOnDisk.go b/UploadsOnDisk.go index 46d9d89..02f4363 100644 --- a/UploadsOnDisk.go +++ b/UploadsOnDisk.go @@ -2,6 +2,7 @@ package main import ( "biukop.com/sfm/loan" + "context" "errors" log "github.com/sirupsen/logrus" "io/ioutil" @@ -11,6 +12,8 @@ import ( "path/filepath" "strconv" "strings" + "sync" + "time" ) type uploadsOnDisk struct { @@ -114,9 +117,10 @@ func (m *uploadsOnDisk) convertExcelToJpg() (e error) { return m.convertExcelTo("jpg") } +var libreOfficeMutex sync.Mutex // make sure we only have one libreoffice running at a time func (m *uploadsOnDisk) convertExcelTo(format string) (e error) { if format != "pdf" && format != "jpg" { - e = errors.New("unsupported format") + e = errors.New("convert excel to unsupported format " + format) return } @@ -132,12 +136,59 @@ func (m *uploadsOnDisk) convertExcelTo(format string) (e error) { } defer os.RemoveAll(dir) - cmd := exec.Command("libreoffice", "--convert-to", format, "--outdir", dir, m.filePath()) - strCmd := cmd.String() - log.Debug("command is ", strCmd) + // Create a new context and add a timeout to it + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() // The cancel should be deferred so resources are cleaned up + + // Create the command with our context + cmd := exec.CommandContext(ctx, "libreoffice", "--convert-to", format, "--outdir", dir, m.filePath()) + + libreOfficeMutex.Lock() //ensure only one libreoffice is running + // This time we can simply use Output() to get the result. out, e := cmd.Output() + libreOfficeMutex.Unlock() + + // We want to check the context error to see if the timeout was executed. + // The error returned by cmd.Output() will be OS specific based on what + // happens when a process is killed. + if ctx.Err() == context.DeadlineExceeded { + log.Error(cmd.String(), " timed out") + + switch format { + case "pdf": + _, e = copyFile(config.UploadsDir.PdfDefault, m.pdfPath()) + break + case "jpg": + _, e = copyFile(config.UploadsDir.JpgDefault, m.jpgPath()) + break + } + return + } + + // If there's no context error, we know the command completed (or errored). + //fmt.Println("Output:", string(out)) + //if err != nil { + // fmt.Println("Non-zero exit code:", err) + //} + + // ---------- for cases not using ctx + // for some unknown reason, libreoffice may just hung for ever + //cmd := exec.Command("libreoffice", "--convert-to", format, "--outdir", dir, m.filePath()) + //strCmd := cmd.String() + //log.Debug("command is ", strCmd) + //out, e := cmd.Output() + // ------------ end of without ctx + if e != nil { log.Error("cannot converting Excel to "+format+":", m.Upload, e) + switch format { + case "pdf": + _, e = copyFile(config.UploadsDir.PdfDefault, m.pdfPath()) + break + case "jpg": + _, e = copyFile(config.UploadsDir.JpgDefault, m.jpgPath()) + break + } return } else { // success log.Info("convert to "+format, m.Upload, " output: ", string(out)) @@ -177,6 +228,7 @@ func (m *uploadsOnDisk) convertPDFToJpg() (e error) { out, e := cmd.Output() if e != nil { log.Error("cannot create png file for PDF", m.Upload, e) + _, e = copyFile(config.UploadsDir.JpgDefault, m.jpgPath()) return } else { log.Info("convert ", m.Upload, " output: ", string(out)) @@ -184,8 +236,8 @@ func (m *uploadsOnDisk) convertPDFToJpg() (e error) { // 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) + e = os.Rename(target, m.jpgPath()) // there should be only one jpg } else { // multi-page, we have -0 -1 -2 -3 -4 files firstPage := dir + string(os.PathSeparator) + "result-0.jpg" _ = ConvertImageToThumbnail(firstPage, m.thumbPath(), 256) @@ -245,6 +297,44 @@ func (m *uploadsOnDisk) GetFileType() (ret string, e error) { return "", nil } +func (m *uploadsOnDisk) DeleteAll() (e error) { + eJpg := os.Remove(m.jpgPath()) + eFile := os.Remove(m.filePath()) + ePdf := os.Remove(m.pdfPath()) + eThumb := os.Remove(m.thumbPath()) + eMeta := m.Upload.Delete() + + strId := strconv.Itoa(int(m.Upload.Id)) + + errMsg := "" + if eJpg != nil { + errMsg += " jpg " + } + + if eFile != nil { + errMsg += " original " + } + + if ePdf != nil { + errMsg += " pdf " + } + + if eThumb != nil { + errMsg += " thumb " + } + + if eMeta != nil { + errMsg += " Meta in DB " + e = errors.New(errMsg + " cannot be deleted " + strId) + } + + if errMsg != "" { + log.Error(errMsg, "files on disk cannot be deleted need disk cleaning ", m.Upload) + } + return +} + +// GetFileContentType // tested, not accurate with xls, xlsx, it becomes zip and octstream sometime. func GetFileContentType(filename string) (contentType string, e error) { contentType = "" @@ -257,7 +347,7 @@ func GetFileContentType(filename string) (contentType string, e error) { return } - // Use the net/http package's handy DectectContentType function. Always returns a valid + // Use the net/http package's handy Detect ContentType function. Always returns a valid // content-type by returning "application/octet-stream" if no others seemed to match. contentType = http.DetectContentType(buffer) return @@ -268,7 +358,7 @@ func ConvertImageToThumbnail(srcPath string, dstPath string, size int) (e error) log.Info("skip converting thumbnail it exists", dstPath) return } - if size <= 0 { + if size <= 0 { //thumb nail width size = 256 } // convert -thumbnail 200 abc.png thumb.abc.png @@ -278,13 +368,11 @@ func ConvertImageToThumbnail(srcPath string, dstPath string, size int) (e error) out, e := cmd.Output() if e != nil { + log.Error("Failed to convert thumbnail", e) + _, e = copyFile(config.UploadsDir.ThumbDefault, dstPath) 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/apiV1UploadAnalysis.go b/apiV1UploadAnalysis.go index 1e8e9d7..37db776 100644 --- a/apiV1UploadAnalysis.go +++ b/apiV1UploadAnalysis.go @@ -5,6 +5,7 @@ import ( log "github.com/sirupsen/logrus" "net/http" "strconv" + "time" ) func apiV1UploadAnalysis(w http.ResponseWriter, r *http.Request, ss *loan.Session) { @@ -33,3 +34,32 @@ func apiV1UploadAnalysis(w http.ResponseWriter, r *http.Request, ss *loan.Sessio } apiV1SendJson(ai, w, r, ss) } + +func apiV1UploadCreateAnalysis(w http.ResponseWriter, r *http.Request, ss *loan.Session) { + time.Sleep(1 * time.Second) + + 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("Cannot decode upload", Id, e) + apiV1Server500Error(w, r) + return + } + apiV1SendJson(ai, w, r, ss) +} diff --git a/apiV1UploadAsImage.go b/apiV1UploadAsImage.go new file mode 100644 index 0000000..e1df732 --- /dev/null +++ b/apiV1UploadAsImage.go @@ -0,0 +1,95 @@ +package main + +import ( + "biukop.com/sfm/loan" + log "github.com/sirupsen/logrus" + "net/http" + "os" + "strings" +) + +func apiV1UploadAsImage(w http.ResponseWriter, r *http.Request, ss *loan.Session) { + // time.Sleep(5* time.Second) + + strId := r.URL.Path[len(apiV1Prefix+"upload-as-image/"):] //remove prefix + if strId == "default" { + http.ServeFile(w, r, config.UploadsDir.JpgDefault) + return + } + + ul, e := getRequestedUpload(strId, 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 apiV1UploadCreateImage(w http.ResponseWriter, r *http.Request, ss *loan.Session) { + //time.Sleep(1 * time.Second) + + strId := r.URL.Path[len(apiV1Prefix+"upload-as-image/"):] //remove prefix + if strId == "" { + apiV1Client404Error(w, r, ss) + return + } + + ul, e := getRequestedUpload(strId, w, r, ss) + if e != nil { + return + } + + // if this is image itself, serve it directly + if strings.Contains(strings.ToLower(ul.Upload.Format), "image") { + apiV1SendJson(true, w, r, ss) + return + } + + // see if a converted image exist, if not convert it and then send + if !fileExists(ul.jpgPath()) { + e = ul.convertUploadsToJpg() + if e != nil { + log.Error("error creating preview image", ul, e) + } + } + + if fileExists(ul.jpgPath()) { + apiV1SendJson(true, w, r, ss) + return + } else { + apiV1SendJson(false, w, r, ss) + } +} diff --git a/apiV1UploadList.go b/apiV1UploadList.go new file mode 100644 index 0000000..efbe6ef --- /dev/null +++ b/apiV1UploadList.go @@ -0,0 +1,31 @@ +package main + +import ( + "biukop.com/sfm/loan" + "encoding/json" + log "github.com/sirupsen/logrus" + "net/http" +) + +func decodeUploadsMetaListFilter(r *http.Request) (ret loan.UploadListFilter, e error) { + decoder := json.NewDecoder(r.Body) + e = decoder.Decode(&ret) + if e != nil { + log.Error("failed decoding Upload Filter", e.Error()) + return + } + return +} + +func apiV1UploadMetaList(w http.ResponseWriter, r *http.Request, ss *loan.Session) { + filter, e := decodeUploadsMetaListFilter(r) + if e != nil { + log.Println("invalid filter", e) + apiV1Client403Error(w, r, ss) // bad request + return + } + + data := loan.GetUploadMetaList(filter) + + apiV1SendJson(data, w, r, ss) +} diff --git a/apiV1UploadThumb.go b/apiV1UploadThumb.go new file mode 100644 index 0000000..31ee707 --- /dev/null +++ b/apiV1UploadThumb.go @@ -0,0 +1,63 @@ +package main + +import ( + "biukop.com/sfm/loan" + log "github.com/sirupsen/logrus" + "net/http" +) + +func apiV1UploadAsThumbnail(w http.ResponseWriter, r *http.Request, ss *loan.Session) { + strId := r.URL.Path[len(apiV1Prefix+"upload-as-thumbnail/"):] //remove prefix + if strId == "default" { + http.ServeFile(w, r, config.UploadsDir.ThumbDefault) + return + } + ul, e := getRequestedUpload(strId, 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 apiV1UploadCreateThumbnail(w http.ResponseWriter, r *http.Request, ss *loan.Session) { + //time.Sleep(1 * time.Second) + strId := r.URL.Path[len(apiV1Prefix+"upload-as-thumbnail/"):] //remove prefix + if strId == "" { + apiV1Client404Error(w, r, ss) + return + } + ul, e := getRequestedUpload(strId, w, r, ss) + if e != nil { + return + } + + // see if a thumbnail is available already + if !fileExists(ul.thumbPath()) { + e = ul.convertUploadsToThumb() + if e != nil { + log.Error("error creating thumbNail image", ul, e) + } + } + if fileExists(ul.thumbPath()) { + apiV1SendJson(true, w, r, ss) + return + } else { + apiV1SendJson(false, w, r, ss) + } +} diff --git a/apiV1Uploads.go b/apiV1Uploads.go index eefeb98..209f33d 100644 --- a/apiV1Uploads.go +++ b/apiV1Uploads.go @@ -11,7 +11,6 @@ import ( "net/http" "os" "strconv" - "strings" "time" ) @@ -34,6 +33,34 @@ func apiV1UploadMetaGet(w http.ResponseWriter, r *http.Request, ss *loan.Session apiV1SendJson(ulmeta, w, r, ss) } +func apiV1UploadDelete(w http.ResponseWriter, r *http.Request, ss *loan.Session) { + 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) + apiV1Client403Error(w, r, ss) // bad request + return + } + + ul := uploadsOnDisk{} + ul.Upload = loan.Uploads{} + e = ul.Upload.Read(int64(intId)) + if e != nil { + log.Println("upload not found", id, e) + apiV1Client404Error(w, r, ss) // bad request + return + } + + e = ul.DeleteAll() + if e != nil { + log.Println("upload cannot be deleted", id, e) + apiV1Server500Error(w, r) // bad operation + return + } + + apiV1SendJson(ul.Upload, w, r, ss) +} + func apiV1UploadOriginalFileGet(w http.ResponseWriter, r *http.Request, ss *loan.Session) { id := r.URL.Path[len(apiV1Prefix+"upload-original/"):] //remove prefix intId, e := strconv.Atoi(id) @@ -235,139 +262,6 @@ func getRequestedUpload(strId string, w http.ResponseWriter, r *http.Request, ss return } -func apiV1UploadAsImage(w http.ResponseWriter, r *http.Request, ss *loan.Session) { - strId := r.URL.Path[len(apiV1Prefix+"upload-as-image/"):] //remove prefix - if strId == "default" { - http.ServeFile(w, r, config.UploadsDir.JpgDefault) - return - } - - ul, e := getRequestedUpload(strId, w, r, ss) - if e != nil { - return - } - //time.Sleep(5* time.Second); - // 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) { - strId := r.URL.Path[len(apiV1Prefix+"upload-as-thumbnail/"):] //remove prefix - if strId == "default" { - http.ServeFile(w, r, config.UploadsDir.ThumbDefault) - return - } - ul, e := getRequestedUpload(strId, 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 apiV1UploadAsPDF(w http.ResponseWriter, r *http.Request, ss *loan.Session) { - strId := r.URL.Path[len(apiV1Prefix+"upload-as-pdf/"):] //remove prefix - if strId == "default" { - http.ServeFile(w, r, config.UploadsDir.PdfDefault) - return - } - ul, e := getRequestedUpload(strId, 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 { - if forceHttpDownload(r) { - w.Header().Set("Content-Disposition", "attachment; filename="+ul.Upload.FileName) - } - http.ServeContent(w, r, ul.Upload.FileName, 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) - } -} - func forceHttpDownload(r *http.Request) bool { keys, ok := r.URL.Query()["download"] diff --git a/apiV1UploadsAsPdf.go b/apiV1UploadsAsPdf.go new file mode 100644 index 0000000..1fee129 --- /dev/null +++ b/apiV1UploadsAsPdf.go @@ -0,0 +1,109 @@ +package main + +import ( + "biukop.com/sfm/loan" + log "github.com/sirupsen/logrus" + "net/http" + "os" +) + +func apiV1UploadAsPDF(w http.ResponseWriter, r *http.Request, ss *loan.Session) { + strId := r.URL.Path[len(apiV1Prefix+"upload-as-pdf/"):] //remove prefix + if strId == "default" { + http.ServeFile(w, r, config.UploadsDir.PdfDefault) + return + } + ul, e := getRequestedUpload(strId, 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 { + if forceHttpDownload(r) { + w.Header().Set("Content-Disposition", "attachment; filename="+ul.Upload.FileName) + } + http.ServeContent(w, r, ul.Upload.FileName, 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) + } +} + +func apiV1UploadCreatePDF(w http.ResponseWriter, r *http.Request, ss *loan.Session) { + //time.Sleep(1* time.Second) + + strId := r.URL.Path[len(apiV1Prefix+"upload-as-pdf/"):] //remove prefix + if strId == "" { + apiV1Client404Error(w, r, ss) + return + } + ul, e := getRequestedUpload(strId, 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" { + apiV1SendJson(true, w, r, ss) + return + } + + // see if a converted pdf exist, if not convert it and then send + if !fileExists(ul.pdfPath()) { + e = ul.convertUploadsToPDF() + if e != nil { + log.Error("error creating pdf", ul, e) + _, e = copyFile(config.UploadsDir.PdfDefault, ul.pdfPath()) + if e != nil { + log.Error("failed copy default pdf", e) + } + } + } + if fileExists(ul.pdfPath()) { + apiV1SendJson(true, w, r, ss) + return + } else { + apiV1SendJson(false, w, r, ss) + } +} diff --git a/apiv1.go b/apiv1.go index d8c1b6d..bc1d8f3 100644 --- a/apiv1.go +++ b/apiv1.go @@ -78,11 +78,17 @@ func setupApiV1Handler() []apiV1HandlerMap { {"GET", "lender-upload/", apiV1UploadOriginalFileGet}, {"GET", "upload-analysis/", apiV1UploadAnalysis}, + {"PUT", "upload-analysis/", apiV1UploadCreateAnalysis}, {"GET", "upload-as-image/", apiV1UploadAsImage}, + {"PUT", "upload-as-image/", apiV1UploadCreateImage}, {"GET", "upload-as-thumbnail/", apiV1UploadAsThumbnail}, + {"PUT", "upload-as-thumbnail/", apiV1UploadCreateThumbnail}, {"GET", "upload-as-pdf/", apiV1UploadAsPDF}, + {"PUT", "upload-as-pdf/", apiV1UploadCreatePDF}, {"GET", "upload-original/", apiV1UploadOriginalFileGet}, {"GET", "upload/", apiV1UploadMetaGet}, + {"DELETE", "upload/", apiV1UploadDelete}, + {"POST", "upload-meta-list/", apiV1UploadMetaList}, {"GET", "login", apiV1DumpRequest}, } @@ -139,11 +145,17 @@ func setupApiV1Handler() []apiV1HandlerMap { {"GET", "lender-upload/", apiV1UploadOriginalFileGet}, {"GET", "upload-analysis/", apiV1UploadAnalysis}, + {"PUT", "upload-analysis/", apiV1UploadCreateAnalysis}, {"GET", "upload-as-image/", apiV1UploadAsImage}, + {"PUT", "upload-as-image/", apiV1UploadCreateImage}, {"GET", "upload-as-thumbnail/", apiV1UploadAsThumbnail}, + {"PUT", "upload-as-thumbnail/", apiV1UploadCreateThumbnail}, {"GET", "upload-as-pdf/", apiV1UploadAsPDF}, + {"PUT", "upload-as-pdf/", apiV1UploadCreatePDF}, {"GET", "upload-original/", apiV1UploadOriginalFileGet}, {"GET", "upload-meta/", apiV1UploadMetaGet}, + {"DELETE", "upload/", apiV1UploadDelete}, + {"POST", "upload-meta-list/", apiV1UploadMetaList}, {"GET", "login", apiV1EmptyResponse}, } diff --git a/assets/thumb_file_icon.webp b/assets/thumb_file_icon.webp index 550d62f6e8f2cb6b3caf719ca35f468ec5b6ee18..24c151a5744e7e51af32ee11eb40a7f599063d3a 100644 GIT binary patch literal 1986 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