package api import ( "crypto/sha256" "encoding/hex" "fmt" "io" "log" "net/http" "os" "git.sp4ke.com/sp4ke/bit4sat/db" "git.sp4ke.com/sp4ke/bit4sat/ln" "git.sp4ke.com/sp4ke/bit4sat/storage" "git.sp4ke.com/sp4ke/bit4sat/utils" "github.com/gin-gonic/gin" ) type UploadCtrl struct{} //TODO: tell client to avoid sending duplicate if we already have hash func (ctrl UploadCtrl) New(c *gin.Context) { uploadForm := UploadForm{} if err := c.ShouldBindJSON(&uploadForm); err != nil { log.Printf("error parsing form: %s", err) utils.JSONErr(c, http.StatusNotAcceptable, "invalid form") return } // Create unique id uploadId, err := storage.GetShortId() if err != nil { utils.JSONErrPriv(c, http.StatusInternalServerError, err) return } tx, err := db.DB.Sql.Beginx() if err != nil { utils.JSONErrPriv(c, http.StatusInternalServerError, err) return } if uploadForm.RequestPayment { if _, ok := ln.CurrencyID[uploadForm.PaymentCurrency]; !ok { utils.JSONErr(c, http.StatusInternalServerError, "currency not handled or recognized") return } } // Create new upload up := &storage.Upload{} up.UploadId = uploadId up.AskFee = uploadForm.RequestPayment up.AskCurrency = uploadForm.PaymentCurrency up.AskAmount = uploadForm.RequestPaymentAmount // Generate unique download id // dlId, err := storage.GetShortId() if err != nil { utils.JSONErrPriv(c, http.StatusInternalServerError, err) return } up.DownloadId = dlId err = up.TxWrite(tx) if err != nil { utils.JSONErrPriv(c, http.StatusInternalServerError, err) db.RollbackTx(tx, nil) return } for _, file := range uploadForm.Files { fup := &storage.FileUpload{} fup.UploadId = uploadId fup.FileName, fup.FileExt = utils.CleanFileName(file.Name) fup.FileSize = file.Size fup.FileType = file.Type fup.SHA256 = file.SHA256 err := fup.TxWrite(tx) if err != nil { utils.JSONErrPriv(c, http.StatusInternalServerError, err) db.RollbackTx(tx, nil) return } } err = tx.Commit() if err != nil { utils.JSONErrPriv(c, http.StatusInternalServerError, err) return } // Set upload status to new err = storage.SetUploadStatus(uploadId, storage.UpNew) if err != nil { utils.JSONErrPriv(c, http.StatusInternalServerError, err) return } // Upload seems valid, handle new upload procedure // // Get the upload session upSession, err := SessionStore.Get(c.Request, UpSessionKey) if err != nil { utils.JSONErrPriv(c, http.StatusInternalServerError, err) return } // Set the uploadId as last upload for session upSession.AddFlash(uploadId) err = upSession.Save(c.Request, c.Writer) if err != nil { utils.JSONErrPriv(c, http.StatusInternalServerError, err) return } // If not free upload generate a an invoice // TODO: check if upload fee is free if uploadForm.RequestPayment { invoiceOpts := ln.InvoiceOpts{ Amount: uploadForm.RequestPaymentAmount, Curr: ln.CurrencyID[uploadForm.PaymentCurrency], Memo: fmt.Sprintf("bit4sat upload: %s", uploadId), } invoice, err := ln.NewInvoice(invoiceOpts) if err != nil { utils.JSONErrPriv(c, http.StatusInternalServerError, err) return } // Update Upload Invoice err = storage.SetUploadInvoice(uploadId, invoice) if err != nil { utils.JSONErrPriv(c, http.StatusInternalServerError, err) return } // Start watching upload payment status for this client //err = WatchUploadPayment(invoice) //if err != nil { //log.Println("error watching payment ", err) //utils.JSONErrPriv(c, http.StatusInternalServerError, err) //return //} c.JSON(http.StatusOK, gin.H{ "status": storage.UpNew, "invoice": invoice, "upload_id": uploadId, }) return // Handle free uploads } else { log.Println("new upload created") c.JSON(http.StatusOK, gin.H{ "status": "ready for upload", "result": gin.H{ "id": uploadId, }, }) } } func (ctrl UploadCtrl) CheckStatus(c *gin.Context) { uploadId := c.Param("id") exists, err := storage.IdExists(uploadId) if err != nil { utils.JSONErrPriv(c, http.StatusInternalServerError, err) return } if !exists { utils.JSONErr(c, http.StatusNotFound, "upload id not found") return } // Get upload status uploadStatus, err := storage.GetUploadStatus(uploadId) if err != nil { utils.JSONErrPriv(c, http.StatusInternalServerError, err) return } if uploadStatus.IsNew() { c.JSON(http.StatusOK, gin.H{ "status": uploadStatus, }) return } // Get invoice id for upload invoiceId, err := storage.GetUploadIdInvoiceId(uploadId) if err != nil { utils.JSONErrPriv(c, http.StatusInternalServerError, err) return } // No invoice found if invoiceId == "" { utils.JSONErr(c, http.StatusNotFound, "no invoice found") return } // Get invoice invoice, err := ln.CheckInvoice(invoiceId) if err != nil { utils.JSONErrPriv(c, http.StatusInternalServerError, err) return } c.JSON(ln.InvoiceHttpCode(invoice), gin.H{ "upload_id": uploadId, "invoice": invoice, "status": uploadStatus, }) } func (ctrl UploadCtrl) PollStatus(c *gin.Context) { uploadId := c.Param("id") exists, err := storage.IdExists(uploadId) if err != nil { utils.JSONErrPriv(c, http.StatusInternalServerError, err) return } if !exists { utils.JSONErr(c, http.StatusNotFound, "id not found") return } uploadStatus, err := storage.GetUploadStatus(uploadId) if err != nil { utils.JSONErrPriv(c, http.StatusInternalServerError, err) return } if uploadStatus&storage.UpNew != 0 { c.JSON(http.StatusCreated, gin.H{ "status": uploadStatus, }) return } // we are not in UpNew state so we should have an invoice invoice, err := storage.GetUploadInvoice(uploadId) if err != nil { utils.JSONErrPriv(c, http.StatusInternalServerError, err) return } if invoice == nil { utils.JSONErr(c, http.StatusInternalServerError, "no invoice found") return } if uploadStatus.WaitPay() { invoiceChan := make(chan *ln.Invoice) errorChan := make(chan error) log.Printf("starting invoice poll for %s", invoice.RHash) // If waiting payment, wait until invoice is paid go ln.PollPaidInvoice(invoice.RHash, invoiceChan, errorChan) // Block until payment done or error log.Println("blocking") select { case invoice := <-invoiceChan: log.Printf("poll: received invoice notif %s", invoice.RHash) //////////////// ///// Invoice was paid //////////////// // log.Println("POLL: invoice was paid") uploadStatus |= storage.UpPaid err := storage.SetUploadStatus(uploadId, uploadStatus) if err != nil { log.Printf("Redis error: %s", err) } // Update Upload Invoice err = storage.SetUploadInvoice(uploadId, invoice) if err != nil { utils.JSONErrPriv(c, http.StatusInternalServerError, err) return } // Generate admin token and save it adminToken, err := storage.GetLongId() if err != nil { utils.JSONErrPriv(c, http.StatusInternalServerError, err) return } err = storage.SetUploadAdminToken(uploadId, adminToken) if err != nil { utils.JSONErrPriv(c, http.StatusInternalServerError, err) return } // Get the download id dlId, err := storage.GetDownloadId(uploadId) if err != nil { utils.JSONErrPriv(c, http.StatusInternalServerError, err) return } // Clear the session upSession, err := SessionStore.Get(c.Request, UpSessionKey) if err != nil { utils.JSONErrPriv(c, http.StatusInternalServerError, err) return } upSession.Flashes() err = upSession.Save(c.Request, c.Writer) if err != nil { utils.JSONErrPriv(c, http.StatusInternalServerError, err) return } // invoice was paid c.JSON(http.StatusAccepted, gin.H{ "status": uploadStatus, "invoice": invoice, "admin_token": adminToken, "download_id": dlId, }) return case err := <-errorChan: utils.JSONErrPriv(c, http.StatusInternalServerError, err) return } // We stopped waiting for the invoice, it must have expired err := storage.SetUploadStatus(uploadId, storage.UpPayExpired) if err != nil { log.Printf("Redis error: %s", err) } c.JSON(http.StatusGone, gin.H{ "status": uploadStatus, "invoice": invoice, }) return } else { c.JSON(http.StatusOK, gin.H{ "status": uploadStatus, "invoice": invoice, }) } return } func (ctrl UploadCtrl) Upload(c *gin.Context) { // Check that upload ID exists uploadId := c.Param("id") exists, err := storage.IdExists(uploadId) if err != nil { utils.JSONErrPriv(c, http.StatusInternalServerError, err) return } if !exists { utils.JSONErr(c, http.StatusNotFound, "id not found") return } // Check if payment done // NOTE: we pre upload and remove the files if the payment was expired //uploadStatus, err := GetUploadStatus(uploadId) //if err != nil { //utils.JSONErrPriv(c, http.StatusInternalServerError, err) //return //} //if uploadStatus < UpWaitingStorage { //utils.JSONErr(c, http.StatusPaymentRequired, //fmt.Sprintf("invoice not paid")) //return //} form, err := c.MultipartForm() if err != nil { utils.JSONErr(c, http.StatusNotAcceptable, fmt.Sprintf("Could not parse form: %s", err.Error())) return } files := form.File["upload[]"] tx, err := db.DB.Sql.Beginx() if err != nil { utils.JSONErrPriv(c, http.StatusInternalServerError, err) return } for _, file := range files { log.Printf("Handling %s", file.Filename) // Get the file's sha256 hasher := sha256.New() formFd, err := file.Open() defer formFd.Close() if err != nil { utils.JSONErr( c, http.StatusInternalServerError, fmt.Sprintf("Could not read file %s", file.Filename), ) db.RollbackTx(tx, nil) return } _, err = io.Copy(hasher, formFd) if err != nil { utils.JSONErr(c, http.StatusInternalServerError, "") db.RollbackTx(tx, nil) return } sum256 := hex.EncodeToString(hasher.Sum(nil)) // Get the file's metadata from upload table up, err := storage.GetByHashID(sum256, uploadId) if err != nil { utils.JSONErrPriv(c, http.StatusNotFound, err) db.RollbackTx(tx, nil) return } log.Printf("Found received file's %s metadata in local database\n", up.FileName) // If file already stored for this upload report conflict if up.Stored { utils.JSONErr(c, http.StatusConflict, fmt.Sprintf("%s already uploaded", up.FileName)) return } // // log.Println("Storing file") err = storage.StoreFormFile(formFd, up.SHA256+up.FileExt) if err != nil { utils.JSONErrPriv(c, http.StatusInternalServerError, err) db.RollbackTx(tx, nil) return } log.Printf("%s stored at %s", up.FileName, storage.GetStoreDestination(up.SHA256+up.FileExt)) // Setting status to stored log.Println("Updating file upload stored status") err = up.TxSetFileStored(tx) if err != nil { utils.JSONErrPriv(c, http.StatusInternalServerError, err) // Rollback and remove the file db.RollbackTx(tx, func() { err := os.Remove(storage.GetStoreDestination(up.SHA256 + up.FileExt)) if err != nil { log.Println(err) } }) return } } err = tx.Commit() if err != nil { utils.JSONErrPriv(c, http.StatusInternalServerError, err) return } // Set the whole transaction as stored err = storage.SetUploadStatus(uploadId, storage.UpStored) if err != nil { utils.JSONErrPriv(c, http.StatusInternalServerError, err) return } c.JSON(http.StatusOK, gin.H{ "status": http.StatusOK, "store_status": storage.UpStored, "upload_id": uploadId, }) }