mvp paid upload (missing download link)

master
Chakib Benziane 5 years ago
parent 93a0260ecc
commit 849d5ebff5

@ -1,20 +1,60 @@
package api
import (
"encoding/json"
"fmt"
"log"
"net/http"
"git.sp4ke.com/sp4ke/bit4sat/bus"
"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-contrib/sessions"
"github.com/gin-gonic/gin"
"github.com/mediocregopher/radix/v3"
)
func sessionHandler(c *gin.Context) {
session := sessions.Default(c)
lastUp := session.Get(storage.SessLastUploadName)
if lastUp != nil {
// Get upload status
key := fmt.Sprintf("upload_status_%s", lastUp.(string))
var upStatus storage.UpStatus
err := db.DB.Redis.Do(radix.FlatCmd(&upStatus, "GET", key))
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError,
fmt.Errorf("error finding upload status %s", lastUp))
fmt.Println(err)
return
}
invoice, err := storage.GetUploadInvoice(lastUp.(string))
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
return
}
if invoice.Id != "" {
c.JSON(http.StatusOK, gin.H{
"uploadId": lastUp,
"status": upStatus,
"invoice": invoice,
})
return
}
}
c.JSON(http.StatusOK, gin.H{
"uploadId": 0,
})
return
}
func invoiceCbHandler(c *gin.Context) {
invoice := ln.Invoice{}
@ -43,9 +83,11 @@ func invoiceCbHandler(c *gin.Context) {
}
invoice.UploadId = uploadId
//
// Set upload status to UpWaitingStorage (paid)
err = storage.SetUploadStatus(invoice.UploadId, storage.UpWaitingStorage)
// Invoice paid !!!!
// Set upload status to paid
err = storage.SetUploadStatus(invoice.UploadId, storage.UpPaid)
if err != nil {
panic(err)
}
@ -57,24 +99,24 @@ func invoiceCbHandler(c *gin.Context) {
}
// publish invoice was updated to upload_id_paid channel
msg := bus.Message{}
msg.Type = bus.PaymentReceived
msg.UploadId = invoice.UploadId
msg.Data = gin.H{
"invoice": invoice,
}
//msg := bus.Message{}
//msg.Type = bus.PaymentReceived
//msg.UploadId = invoice.UploadId
//msg.Data = gin.H{
//"invoice": invoice,
//}
msgJson, err := json.Marshal(msg)
if err != nil {
panic(err)
}
//msgJson, err := json.Marshal(msg)
//if err != nil {
//panic(err)
//}
log.Printf("Notifying upload paid %s", invoice.UploadId)
key := fmt.Sprintf("%s_%s", bus.UploadUpdateChannelPrefix,
invoice.UploadId)
//log.Printf("Notifying upload paid %s", invoice.UploadId)
//key := fmt.Sprintf("%s_%s", bus.UploadUpdateChannelPrefix,
//invoice.UploadId)
err = db.DB.Redis.Do(radix.FlatCmd(nil, "PUBLISH",
key, msgJson))
//err = db.DB.Redis.Do(radix.FlatCmd(nil, "PUBLISH",
//key, msgJson))
return
}

@ -3,7 +3,6 @@ package api
import (
"git.sp4ke.com/sp4ke/bit4sat/config"
"git.sp4ke.com/sp4ke/bit4sat/storage"
"git.sp4ke.com/sp4ke/bit4sat/ws"
"github.com/gin-contrib/cors"
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/redis"
@ -20,14 +19,18 @@ type API struct {
func (api *API) Run() {
// Get last session if it exists
api.router.GET("/api/session", sessionHandler)
uploadRoute := api.router.Group("/api/upload")
{
uploadRoute.POST("", UploadCtrl.New)
uploadRoute.PUT(":id", UploadCtrl.Upload)
uploadRoute.GET("/check/:id", UploadCtrl.CheckStatus)
}
// Websocket server
api.router.GET("/ws", ws.Serve)
//api.router.GET("/ws", ws.Serve)
// LN charge callback
api.router.POST("/"+config.ChargeCallbackEndpoint, invoiceCbHandler)

@ -34,11 +34,14 @@ func Info() (gin.H, error) {
}
// Shoudl be called in goroutine
func PollPaidInvoice(in *Invoice, paidChan chan<- *Invoice) {
func PollPaidInvoice(in *Invoice, invoiceChan chan<- *Invoice, errorChan chan<- error) {
invoice := Invoice{}
invoice.UploadId = in.UploadId
reqParams := url.Values{}
// Timeout in seconds
reqParams.Set("timeout", strconv.Itoa(30))
reqParams.Set("timeout", strconv.Itoa(60))
reqURI := fmt.Sprintf("%s/%s/wait?%s",
getUrl(PollInvoiceEndpoint),
in.Id,
@ -47,6 +50,7 @@ func PollPaidInvoice(in *Invoice, paidChan chan<- *Invoice) {
log.Printf("polling to %s", reqURI)
var err error
var jsonDec *json.Decoder
resp := &http.Response{}
for {
@ -55,6 +59,7 @@ func PollPaidInvoice(in *Invoice, paidChan chan<- *Invoice) {
if err != nil {
log.Printf("Error: ", err)
}
jsonDec = json.NewDecoder(resp.Body)
if resp.StatusCode == http.StatusPaymentRequired {
log.Printf("invoice %s not yet paid", in.Id)
@ -63,26 +68,32 @@ func PollPaidInvoice(in *Invoice, paidChan chan<- *Invoice) {
if resp.StatusCode == http.StatusGone {
log.Printf("invoice expired ", in.Id)
invoice.Expired = true
break
}
if resp.StatusCode == http.StatusOK {
break
}
} else {
log.Println("else !")
// This section handles unknown answer from ln-charge
log.Printf("InvoicePoll Error: unknown resopnse %s", resp.Status)
jsonDec.Decode(&invoice)
//unknownData := gin.H{}
//jsonDec := json.NewDecoder(resp.Body)
//jsonDec.Decode(&data)
log.Printf("InvoicePoll Error: unknown resopnse %s", resp.Status)
close(invoiceChan)
errorChan <- fmt.Errorf("%s: %s", resp.Status, invoice)
return
}
}
invoice := Invoice{}
invoice.UploadId = in.UploadId
jsonDec := json.NewDecoder(resp.Body)
jsonDec.Decode(&invoice)
log.Printf("Invoice paid %s", invoice)
paidChan <- &invoice
invoiceChan <- &invoice
log.Printf("quit polling %s", in.UploadId)
}
func UploadInvoice(amount float32, curr Currency, uploadId string) (*Invoice, error) {

@ -1,6 +1,7 @@
package ln
import (
"encoding/json"
"fmt"
"strconv"
"time"
@ -54,6 +55,11 @@ func (t *timestamp) UnmarshalJSON(in []byte) error {
return nil
}
func (t timestamp) MarshalJSON() ([]byte, error) {
str := strconv.Itoa(int(time.Time(t).Unix()))
return []byte(str), nil
}
func (t timestamp) String() string {
return time.Time(t).Format(time.RFC3339)
}
@ -71,4 +77,13 @@ type Invoice struct {
PaidAt timestamp `json:"paid_at"`
CreatedAt timestamp `json:"created_at"`
ExpiresAt timestamp `json:"expires_at"`
Expired bool `json:"-"`
}
func (i Invoice) MarshalBinary() ([]byte, error) {
return json.Marshal(i)
}
func (i *Invoice) UnmarshalBinary(b []byte) error {
return json.Unmarshal(b, &i)
}

@ -6,6 +6,7 @@ import (
)
func main() {
defer db.DB.Sql.Close()
api := api.NewAPI()

@ -0,0 +1,28 @@
package storage
import (
"testing"
)
func TestUploadStatus(t *testing.T) {
// start with new status
//
//status := UpPaid | UpStored
//j, _ := json.Marshal(status)
//fmt.Printf("%s", j)
//fmt.Printf("%32b\n", UpPaid|UpStored)
//err := SetUploadStatus("1", status)
//if err != nil {
//t.Error(err)
//}
// Set to stored
//status |= UpStored
//fmt.Println(status)
//if !status.IsStored() {
//t.Error()
//}
}

@ -3,21 +3,21 @@ package storage
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"git.sp4ke.com/sp4ke/bit4sat/bus"
"git.sp4ke.com/sp4ke/bit4sat/db"
"git.sp4ke.com/sp4ke/bit4sat/ln"
"git.sp4ke.com/sp4ke/bit4sat/utils"
"git.sp4ke.com/sp4ke/bit4sat/ws"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"github.com/mediocregopher/radix/v3"
)
const (
SessLastUploadName = "last-upload"
)
type UploadCtrl struct{}
@ -54,7 +54,6 @@ func (ctrl UploadCtrl) New(c *gin.Context) {
up.FileSize = file.Size
up.FileType = file.Type
up.SHA256 = file.SHA256
up.Status = UpNew
err := up.TxWrite(tx)
if err != nil {
@ -70,30 +69,19 @@ func (ctrl UploadCtrl) New(c *gin.Context) {
return
}
// Register this uploadId to the client's websocket update channel
// websocket on the pubsub channel update_websocket_[websocket id]
session := sessions.Default(c)
webSocketId := session.Get(ws.WebsocketIdName)
if webSocketId == nil {
utils.JSONErrPriv(c, http.StatusInternalServerError,
fmt.Errorf("no session id found for web socket"))
// Set upload status to new
err = SetUploadStatus(uploadId, UpNew)
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
return
}
msg := bus.Message{
UploadId: uploadId,
}
msg.Type = bus.SetUploadId
jsonMsg, err := json.Marshal(msg)
// Upload seems valid, handle new upload procedure
session := sessions.Default(c)
channelName := fmt.Sprintf("%s_%s", bus.WebsocketPubSubPrefix, webSocketId)
if err = db.DB.Redis.Do(radix.FlatCmd(nil, "PUBLISH",
channelName, jsonMsg)); err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
return
}
// Set the uploadId as last upload for session
session.Set(SessLastUploadName, uploadId)
session.Save()
// If not free upload generate a an invoice
// TODO: check if upload fee is free
@ -116,12 +104,6 @@ func (ctrl UploadCtrl) New(c *gin.Context) {
}
err = SetUploadStatus(uploadId, UpWaitingPayment)
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
return
}
// Start watching upload payment status for this client
//err = WatchUploadPayment(invoice)
@ -131,14 +113,10 @@ func (ctrl UploadCtrl) New(c *gin.Context) {
//return
//}
result := gin.H{
"upload_id": uploadId,
"invoice": invoice,
}
c.JSON(http.StatusOK, gin.H{
"status": "waiting for payment",
"result": result,
"status": UpNew,
"invoice": invoice,
"upload_id": uploadId,
})
return
@ -155,7 +133,107 @@ func (ctrl UploadCtrl) New(c *gin.Context) {
})
}
}
func (ctrl UploadCtrl) CheckStatus(c *gin.Context) {
uploadId := c.Param("id")
exists, err := 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 := GetUploadStatus(uploadId)
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
return
}
if uploadStatus&UpNew != 0 {
c.JSON(http.StatusCreated, gin.H{
"status": uploadStatus,
})
return
}
if uploadStatus.WaitPay() {
// we should be in waiting payment meaning we have an invoice
invoice, err := GetUploadInvoice(uploadId)
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
return
}
invoiceChan := make(chan *ln.Invoice)
errorChan := make(chan error)
log.Println("starting go routine")
// If waiting payment, wait until invoice is paid
go ln.PollPaidInvoice(invoice, invoiceChan, errorChan)
// Block until payment done or error
log.Println("blocking")
select {
case invoice := <-invoiceChan:
// if expired return with expired response
if invoice.Expired {
err := SetUploadStatus(invoice.UploadId, UpPayExpired)
if err != nil {
log.Printf("Redis error: %s", err)
}
c.JSON(http.StatusGone, gin.H{
"status": uploadStatus,
"invoice": invoice,
})
return
}
////////////////
///// Invoice was paid
////////////////
uploadStatus |= UpPaid
err := SetUploadStatus(invoice.UploadId, uploadStatus)
if err != nil {
log.Printf("Redis error: %s", err)
}
// Update Upload Invoice
err = SetUploadInvoice(invoice.UploadId, invoice)
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
return
}
// invoice was paid
c.JSON(http.StatusAccepted, gin.H{
"status": uploadStatus,
"invoice": invoice,
})
return
case err := <-errorChan:
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
}
} else {
c.JSON(http.StatusOK, gin.H{
"status": uploadStatus,
})
}
return
}
func (ctrl UploadCtrl) Upload(c *gin.Context) {
@ -174,6 +252,21 @@ func (ctrl UploadCtrl) Upload(c *gin.Context) {
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,
@ -226,10 +319,9 @@ func (ctrl UploadCtrl) Upload(c *gin.Context) {
}
log.Printf("Found received file's %s metadata in local database\n", up.FileName)
//log.Println(up)
// Store file
if up.Status == UpStored {
// 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
@ -248,11 +340,13 @@ func (ctrl UploadCtrl) Upload(c *gin.Context) {
GetStoreDestination(up.SHA256+up.FileExt))
// Setting status to stored
log.Println("Updating upload status to stored")
up.Status = UpStored
err = up.TxSetState(tx, UpStored)
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(GetStoreDestination(up.SHA256 + up.FileExt))
if err != nil {
@ -269,6 +363,7 @@ func (ctrl UploadCtrl) Upload(c *gin.Context) {
return
}
// Set the whole transaction as stored
err = SetUploadStatus(uploadId, UpStored)
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
@ -276,8 +371,9 @@ func (ctrl UploadCtrl) Upload(c *gin.Context) {
}
c.JSON(http.StatusOK, gin.H{
"status": http.StatusOK,
"result": "uploaded",
"status": http.StatusOK,
"result": "uploaded",
"store_status": UpStored,
})
}

@ -2,12 +2,13 @@ package storage
import (
"database/sql"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"log"
"math/bits"
"git.sp4ke.com/sp4ke/bit4sat/bus"
"git.sp4ke.com/sp4ke/bit4sat/db"
"git.sp4ke.com/sp4ke/bit4sat/ln"
"github.com/jmoiron/sqlx"
@ -29,41 +30,18 @@ const (
file_type varchar(255) DEFAULT '',
file_size integer NOT NULL,
file_ext varchar(255) DEFAULT '',
fee integer NOT NULL DEFAULT 0,
status integer DEFAULT 0 REFERENCES upload_status(type),
ask_fee integer NOT NULL DEFAULT 0,
stored boolean DEFAULT '0',
UNIQUE (upload_id, sha256)
);
`
DBUploadView = `
CREATE OR REPLACE VIEW upload_with_status
AS SELECT
upload.upload_id,
upload.sha256,
upload_status.status,
upload.file_name,
upload.file_type,
upload.file_size,
upload.file_ext,
upload.fee
FROM upload
JOIN upload_status ON
upload.status = upload_status.type
`
DBUploadStatusSchema = `CREATE TABLE IF NOT EXISTS upload_status
(
type INT NOT NULL PRIMARY KEY,
status CHAR(255) NOT NULL UNIQUE
)
`
QNewUpload = `INSERT INTO upload
(upload_id, sha256, file_name, file_type, file_size, file_ext, status, fee)
(upload_id, sha256, file_name, file_type, file_size, file_ext, stored, ask_fee)
VALUES
(:upload_id, :sha256, :file_name, :file_type, :file_size, :file_ext, :status, :fee)`
(:upload_id, :sha256, :file_name, :file_type, :file_size, :file_ext, :stored, :ask_fee)`
QSetStatus = `UPDATE upload SET status = :status WHERE upload_id = :upload_id `
QSetStored = `UPDATE upload SET stored = :stored WHERE upload_id = :upload_id `
QGetByHashID = `SELECT upload_id,
sha256,
@ -71,27 +49,150 @@ upload.status = upload_status.type
file_type,
file_size,
file_ext,
fee,
status
ask_fee,
stored
FROM upload WHERE sha256 = $1 AND upload_id = $2`
)
type UpStatus uint32
func (st UpStatus) MarshalJSON() ([]byte, error) {
res := map[string]string{}
res["pay_status"] = st.PrintPayStatus()
res["store_status"] = st.PrintStoreStatus()
return json.Marshal(res)
}
func (st *UpStatus) UnmarshalBinary(b []byte) error {
fmt.Printf("%#v\n", b)
// first 4 bits are reseved
// Single byte will be for 0 value
if len(b) == 1 {
*st = 0
return nil
}
view := binary.BigEndian.Uint32(b)
view = bits.Reverse32(view)
fmt.Printf("%32b\n", view)
log.Printf("%d\n", view)
// 1. Convert 3 last bytes to int
//res, err := strconv.ParseUint(string(b), 16, )
//if err != nil {
//return err
//}
//fmt.Println(res)
//buf := bytes.NewBuffer(b)
//firstByte := uint8(b[0])
//err := binary.Read(buf, binary.)
*st = UpStatus(view)
return nil
}
// Get list of positions set
func (st UpStatus) GetFlagPositions() []int {
var i uint32
var setBits []int
for i = 31; i > 0; i-- {
if st&(1<<i) != 0 {
setBits = append(setBits, int(i))
}
}
return setBits
}
func (st UpStatus) PrintStoreStatus() string {
if st.Stored() {
return UploadStatus[UpStored]
} else if st.StoreFail() {
return UploadStatus[WaitStore]
} else {
return UploadStatus[WaitStore]
}
}
func (st UpStatus) PrintPayStatus() string {
if st.Paid() {
return UploadStatus[UpPaid]
}
if st.Expired() {
return UploadStatus[UpPayExpired]
}
return UploadStatus[WaitPay]
}
func (st UpStatus) WaitPay() bool {
return (!st.Paid()) && (!st.Expired())
}
func (st UpStatus) Stored() bool {
return (st & UpStored) != 0
}
func (st UpStatus) StoreFail() bool {
return (st & UpStoreFail) != 0
}
func (st UpStatus) Paid() bool {
return (st & UpPaid) != 0
}
func (st UpStatus) Expired() bool {
return (st & UpPayExpired) != 0
}
func (st UpStatus) GetStoreStatus() UpStatus {
return st & StoreMask
}
func (st UpStatus) GetPayStatus() UpStatus {
return st & PayMask
}
// First 4 bits are reserved for easier parsing from redis
const (
UpNew = iota
UpWaitingPayment
UpWaitingStorage
UpStored
UpReady
UpPayExpired UpStatus = 1 << (32 - 1 - iota)
UpPaid
UpStored // All files for this upload where stored
UpStoreFail
// Only used for printing
WaitStore
WaitPay
UpNew = UpStatus(0)
PayMask = UpPaid | UpPayExpired
StoreMask = UpStored
)
// new --> waiting payment (optional) --> waiting storage (client upload)
// --> Sotred --> Ready (sharing url sent to client and acked)
var UploadStatus = map[int]string{
UpNew: "new upload",
UpWaitingPayment: "waiting payment",
UpWaitingStorage: "waiting storage",
UpStored: "stored",
UpReady: "ready",
var UploadStatus = map[UpStatus]string{
UpNew: "new upload",
// Payment
UpPayExpired: "expired",
UpPaid: "paid",
WaitPay: "waiting",
// Storage
WaitStore: "waiting storage",
UpStored: "stored",
}
var (
@ -101,13 +202,14 @@ var (
type Upload struct {
ID string `db:"upload_id"`
Free bool `db:"-"` // is this a free upload
SHA256 string `db:"sha256"`
FileName string `db:"file_name"`
FileType string `db:"file_type"`
FileSize int64 `db:"file_size"`
FileExt string `db:"file_ext"`
Status int `db:"status"`
Fee int `db:"fee"`
Stored bool `db:"stored"`
AskFee int `db:"ask_fee"` // fee asked for download
}
// TODO: sync from redis to db
@ -115,9 +217,53 @@ type Upload struct {
//}
func SetUploadStatus(id string, status int) error {
func GetUploadInvoice(uploadId string) (*ln.Invoice, error) {
invoice := ln.Invoice{}
uploadInvoiceKey := fmt.Sprintf("upload_%s_invoice", uploadId)
err := DB.Redis.Do(radix.FlatCmd(&invoice, "GET", uploadInvoiceKey))
if err != nil {
return nil, err
}
return &invoice, nil
}
func SetUploadStatus(id string, status UpStatus) error {
log.Printf("setting upload status for %s", id)
key := fmt.Sprintf("upload_status_%s", id)
return DB.Redis.Do(radix.FlatCmd(nil, "SET", key, status))
if status == UpNew {
return DB.Redis.Do(radix.FlatCmd(nil, "SETBIT", key, 31, 0))
}
log.Println("setting upload status for bit positions ", status.GetFlagPositions())
// get bit positions
for _, offset := range status.GetFlagPositions() {
log.Printf("setting bit at position %d", offset)
err := DB.Redis.Do(radix.FlatCmd(nil,
"SETBIT", key, offset, 1))
if err != nil {
return err
}
}
log.Println("done set bit")
return nil
}
func GetUploadStatus(id string) (status UpStatus, err error) {
log.Println("Getting upload status")
key := fmt.Sprintf("upload_status_%s", id)
err = DB.Redis.Do(radix.FlatCmd(&status, "GET", key))
return
}
func SetUploadInvoice(uploadId string, invoice *ln.Invoice) error {
@ -138,28 +284,11 @@ func SetUploadInvoice(uploadId string, invoice *ln.Invoice) error {
return DB.Redis.Do(radix.FlatCmd(nil, "SET", invoiceUploadKey, uploadId))
}
func WatchUploadPayment(invoice *ln.Invoice) error {
log.Println("watching invoice ", invoice)
invoiceJson, err := json.Marshal(invoice)
if err != nil {
return err
}
return DB.Redis.Do(radix.FlatCmd(nil, "PUBLISH", bus.WatchInvoicesChannelName, invoiceJson))
//TODO: Notify client on websocket
return nil
}
// Returns true if id exists in DB
func IdExists(id string) (exists bool, err error) {
key := fmt.Sprintf("upload_status_%s", id)
err = DB.Redis.Do(radix.Cmd(&exists, "EXISTS", key))
return
}
@ -180,8 +309,9 @@ func GetByHashID(sha256 string, id string) (*Upload, error) {
return &up, nil
}
func (u *Upload) TxSetState(tx *sqlx.Tx, status int) error {
_, err := tx.NamedExec(QSetStatus, u)
func (u *Upload) TxSetFileStored(tx *sqlx.Tx) error {
u.Stored = true
_, err := tx.NamedExec(QSetStored, u)
if err != nil {
return err
@ -226,30 +356,8 @@ func (u *Upload) Write() error {
}
func init() {
_, err := DB.Sql.Exec(DBUploadStatusSchema)
_, err := DB.Sql.Exec(DBUploadSchema)
if err != nil {
log.Fatal(err)
}
_, err = DB.Sql.Exec(DBUploadSchema)
if err != nil {
log.Fatal(err)
}
_, err = DB.Sql.Exec(DBUploadView)
if err != nil {
log.Fatal(err)
}
// Populate status types
query := `INSERT INTO upload_status (type, status)
VALUES($1,$2) ON CONFLICT (type) DO UPDATE
SET status = $3`
for k, v := range UploadStatus {
_, err := DB.Sql.Exec(query, k, v, v)
if err != nil {
log.Panic(err)
}
}
}

@ -15,5 +15,5 @@ func JSONErr(c *gin.Context, status int, msg string) {
func JSONErrPriv(c *gin.Context, status int, err error) {
log.Println(err)
JSONErr(c, status, "")
JSONErr(c, status, "server error")
}

@ -0,0 +1,88 @@
<template>
<div id="payment">
<img :src="payreqURI" />
<timer v-if="invoice.status != 'paid'" :expires="invoice.expires_at" :expired="expired"></timer>
<p v-if="invoice.status == 'paid'">Paid on {{paidAt}}</p>
<p class='paid' v-if="paid">PAID</p>
<p class='unpaid' v-if="unpaid">UNPAID</p>
<p class='expired' v-if="expired">UNPAID</p>
<canvas id="canvas" hidden>
</div>
</template>
<script charset="utf-8">
import QRCode from 'qrcode';
import Timer from './Timer.vue';
export default {
data() {
return {
payreqURI: "",
expired: false,
}
},
props: ['invoice', 'uploadId', 'status'],
methods:{
makeLnQR(payreq) {
let canvas = this.$el.querySelector('#canvas')
QRCode.toDataURL(canvas, payreq.toUpperCase(), {
margin: 4,
width: 340,
errorCorrectionLevel: 'H',
type: 'png',
renderOpts:{
quality: 1,
}
})
.then(url => {
this.payreqURI = url;
})
.catch(err =>{
console.error(err);
})
}
},
mounted(){
this.expired = (this.invoice.status === 'expired')
},
computed: {
paidAt: function(){
return new Date(this.invoice.paid_at).toGMTString();
},
paid: function() {
return this.invoice.status == 'paid';
},
unpaid: function(){
return this.invoice.status == 'unpaid';
},
},
watch: {
invoice: function(val) {
if (new Date(val.expires_at*1000) - new Date() <= 0){
this.expired = true;
}
this.makeLnQR(val.payreq)
}
},
components:{
Timer,
}
}
</script>
<style>
img{
width: 340px;
height: 340px;
}
</style>

@ -0,0 +1,63 @@
export default class Timer {
// Recevies a date in the future
constructor(expiresAt){
let self = this;
this.secsLeft = 0;
this.minsLeft = 0;
this.hoursLeft= 0;
this.done = false;
// Convert epoch timestamp to date
if (typeof(expiresAt) === 'number'){
this.deadline = new Date(expiresAt);
} else if (expiresAt instanceof Date){
this.deadline = expiresAt
} else {
throw("need instance of Date or unix timestamp")
}
// Timer done
if (this.deadline - new Date() <= 0) {
this.done = true;
} else {
this.secsLeft = new Date(this.deadline - new Date()).getSeconds();
this.minsLeft = new Date(this.deadline - new Date()).getMinutes();
this.hoursLeft = new Date(this.deadline - new Date()).getHours();
// Run ticker
this.interval = setInterval(function(){
self.tick()
}, 1000)
}
}
get secs () {
return this.secsLeft;
}
get mins(){
return this.minsLeft;
}
get hours(){
return this.hoursLeft;
}
tick() {
if (this.deadline - new Date() <= 0) {
this.done;
clearInterval(this.interval);
return
}
this.secsLeft = new Date(this.deadline - new Date()).getSeconds();
this.minsLeft = new Date(this.deadline - new Date()).getMinutes();
this.hoursLeft = new Date(this.deadline - new Date()).getHours();
}
}

@ -0,0 +1,46 @@
<template>
<div class="timer">
<p v-if="!expired && timer">remaining: {{mins}}:{{secs}}</p>
<p class="expired" v-if="expired">expired</p>
</div>
</template>
<script>
import Timer from './Timer.js'
export default {
data(){
return {
timer: null,
}
},
props: ['expires', 'expired'],
computed: {
secs: function() {
if (this.timer){
return this.timer.secs;
}
},
mins: function(){
if (this.timer){
return this.timer.mins;
}
}
},
watch: {
expires: function(val){
let self = this;
this.timer = new Timer(new Date(val*1000));
}
}
}
</script>
<style>
p.expired {
color: red;
}
</style>

@ -0,0 +1,60 @@
<template>
<div id="upload">
<input v-on:change="fileChange($event)" type="file" multiple />
<p>selected {{fileCount}} files</p>
<ul>
<li v-bind:files="files" v-for="file in files">
{{ file.name }}
<ul>
<li>size: {{ file.size / 1000 }} kB </li>
<li>type: {{ file.type }} </li>
</ul>
</li>
</ul>
</div>
</template>
<script charset="utf-8">
import GetWorker from './workerInterface.js';
const w = GetWorker('main');
//w.listenTo('upload-id', (e) => {
// console.log('vue received upload id ', e.data.id)
// console.log('vue received upload id ', e.data.invoice)
//})
export default {
data(){
return {
fileCount: 0,
files: []
}
},
methods: {
fileChange(event) {
// reset
this.files = []
this.fileCount = event.target.files.length
this.files = event.target.files
w.post({
msg: 'new-upload',
payload: [...this.files],
})
// Get sha256 of files
// for (let file of this.files) {
// w.post({
// msg: 'file-sha256',
// payload: file
// })
// }
}
}
}
</script>
Loading…
Cancel
Save