From 4105c33e2564f44d324299856024d41ea0a99ff0 Mon Sep 17 00:00:00 2001 From: Chakib Benziane Date: Sun, 7 Apr 2019 12:30:43 +0200 Subject: [PATCH] WIP download start and keep dl invoice session --- api/download.go | 99 ++++++++++++++++++++++++++++++------- ln/invoice.go | 4 +- storage/upload_model.go | 102 ++++++++++++++++++++++++++++++++++----- web/Caddyfile | 5 ++ web/src/App.vue | 24 ++++++--- web/src/DownloadLink.vue | 25 ---------- web/src/DownloadView.vue | 69 ++++++++++++++++++++++++++ web/src/Home.vue | 2 +- web/src/Pay.vue | 16 +----- web/src/Upload.vue | 32 ++++++++++-- web/src/UploadView.vue | 25 +++++----- web/src/api.js | 26 +++++----- web/src/index.js | 7 +++ web/src/store.js | 57 ++++++++++++++++------ 14 files changed, 372 insertions(+), 121 deletions(-) delete mode 100644 web/src/DownloadLink.vue create mode 100644 web/src/DownloadView.vue diff --git a/api/download.go b/api/download.go index b02f555..5af3e81 100644 --- a/api/download.go +++ b/api/download.go @@ -1,9 +1,11 @@ package api import ( + "encoding/gob" "fmt" "log" "net/http" + "time" "git.sp4ke.com/sp4ke/bit4sat/ln" "git.sp4ke.com/sp4ke/bit4sat/storage" @@ -23,7 +25,7 @@ func DownloadHandler(c *gin.Context) { return } - // Get the upload session + // Get the download session session, err := SessionStore.Get(c.Request, DlSessionKey) if err != nil { utils.JSONErrPriv(c, http.StatusInternalServerError, err) @@ -31,7 +33,6 @@ func DownloadHandler(c *gin.Context) { } dlSess, exists := session.Values["session-id"] - log.Printf("%#v", dlSess) // Test if we are alread in a download session if !exists { @@ -47,21 +48,28 @@ func DownloadHandler(c *gin.Context) { log.Printf("going to generate invoice for %s", upId) - session.AddFlash(sessId, "session-id") + //TODO: use fee asked by uploader + // + // Get upload data asked by uploader + up, err := storage.GetUploadById(upId) + if err != nil { + utils.JSONErrPriv(c, http.StatusInternalServerError, err) + return + } - session.Save(c.Request, c.Writer) + // Get files metadata for upload + var upFilesMeta []storage.FileUpload + upFilesMeta, err = storage.GetUploadFilesMeta(upId) if err != nil { utils.JSONErrPriv(c, http.StatusInternalServerError, err) return } - //TODO: use fee asked by uploader - // - // Get upload fee asked by uploader - // + session.Values["files"] = upFilesMeta + invoiceOpts := ln.InvoiceOpts{ - Amount: 100, - Curr: ln.CurSat, + Amount: up.AskAmount, + Curr: ln.CurrencyID[up.AskCurrency], Memo: fmt.Sprintf("bit4sat download: %s", sessId), } @@ -71,23 +79,73 @@ func DownloadHandler(c *gin.Context) { return } - // This is a returning download session to pay the invoice - // + session.Values["session-id"] = sessId + session.Values["invoice"] = invoice + + invoiceExpires := time.Time(invoice.ExpiresAt).Sub(time.Now()).Seconds() + + //TODO: this session can be reused during this hour + session.Options.MaxAge = int(invoiceExpires) + + //log.Printf("session: %#v", session.Values) + + err = session.Save(c.Request, c.Writer) + if err != nil { + utils.JSONErrPriv(c, http.StatusInternalServerError, err) + return + } + c.JSON(http.StatusPaymentRequired, gin.H{ - "invoice_rhash": invoice.RHash, + "invoice": invoice, + "files": upFilesMeta, }) + // This is a returning download session to pay the invoice } else { log.Printf("continue download session id: %s", dlSess) - session.Flashes("session-id") - err = session.Save(c.Request, c.Writer) - if err != nil { - utils.JSONErrPriv(c, http.StatusInternalServerError, err) + var ok bool + + invVal := session.Values["invoice"] + var invoice = &ln.Invoice{} + if invoice, ok = invVal.(*ln.Invoice); !ok { + log.Printf("Could not find invoice in session %s", dlSess) + session.Options.MaxAge = -1 + + err = session.Save(c.Request, c.Writer) + if err != nil { + utils.JSONErrPriv(c, http.StatusInternalServerError, err) + return + } + + utils.JSONErr(c, http.StatusExpectationFailed, + "invoice not found, start a new download session session") return } - c.Status(http.StatusOK) + //TODO: Check if invoice paid ?? + + filesVal := session.Values["files"] + var filesMeta = &[]storage.FileUpload{} + if filesMeta, ok = filesVal.(*[]storage.FileUpload); !ok { + // The cookie broke ??? + session.Options.MaxAge = -1 + + err = session.Save(c.Request, c.Writer) + if err != nil { + utils.JSONErrPriv(c, http.StatusInternalServerError, err) + return + } + + utils.JSONErr(c, http.StatusExpectationFailed, + "start a new download session session") + return + } + + c.JSON(http.StatusPaymentRequired, gin.H{ + "invoice": invoice, + "files": filesMeta, + }) } return @@ -110,3 +168,8 @@ func TestDownHandler(c *gin.Context) { return } + +func init() { + gob.Register(&ln.Invoice{}) + gob.Register(&[]storage.FileUpload{}) +} diff --git a/ln/invoice.go b/ln/invoice.go index 2043aad..6412e57 100644 --- a/ln/invoice.go +++ b/ln/invoice.go @@ -112,6 +112,7 @@ type Invoice struct { RHash string `json:"rhash"` PreImage string `json:"-"` // used as secret admin token Status string `json:"status"` + Settled bool `json:"settled"` PaidAt timestamp `json:"paid_at"` CreatedAt timestamp `json:"created_at"` ExpiresAt timestamp `json:"expires_at"` @@ -131,7 +132,6 @@ func (i *Invoice) UnmarshalBinary(b []byte) error { // Create new Invoice from lnrpc.Invoice func InvoiceFromLndIn(lndInvoice *lnrpc.Invoice) *Invoice { - log.Println(hex.EncodeToString(lndInvoice.RPreimage)) invoice := Invoice{ AddIndex: lndInvoice.AddIndex, Description: lndInvoice.GetMemo(), @@ -147,6 +147,7 @@ func InvoiceFromLndIn(lndInvoice *lnrpc.Invoice) *Invoice { // Calculate status if lndInvoice.Settled { + invoice.Settled = true invoice.Status = InvoiceStatus[Paid] } @@ -166,6 +167,7 @@ func UpdateInvoiceFromLnd(storedIn *Invoice, newIn *lnrpc.Invoice) *Invoice { // Calculate status if newIn.Settled { storedIn.Status = InvoiceStatus[Paid] + storedIn.Settled = true } // Handle expired status diff --git a/storage/upload_model.go b/storage/upload_model.go index d96d33e..878d27e 100644 --- a/storage/upload_model.go +++ b/storage/upload_model.go @@ -8,6 +8,7 @@ import ( "fmt" "log" "math/bits" + "time" "git.sp4ke.com/sp4ke/bit4sat/db" "git.sp4ke.com/sp4ke/bit4sat/ln" @@ -44,7 +45,7 @@ const ( CREATE TABLE IF NOT EXISTS uploads ( upload_id text PRIMARY KEY, download_id text DEFAULT '', - created timestamp, + created timestamp DEFAULT now(), ask_fee boolean NOT NULL DEFAULT 'false', ask_currency varchar(3) DEFAULT '', ask_amount real DEFAULT 0, @@ -223,15 +224,32 @@ type FileUpload struct { } type Upload struct { - UploadId string `db:"upload_id"` - DownloadID string `db:"download_id"` - AskFee bool `db:"ask_fee"` - AskCurrency string `db:"ask_currency"` - AskAmount float64 `db:"ask_amount"` - InvoiceRhash string `db:"invoice_rhash"` // used as id - Settled bool `db:"settled"` - UploadStatus uint32 `db:"status"` // upload flag status - AdminToekn string `db:"admin_token"` + UploadId string `db:"upload_id"` + DownloadID string `db:"download_id"` + AskFee bool `db:"ask_fee"` + Created time.Time `db:"created"` + AskCurrency string `db:"ask_currency"` + AskAmount float64 `db:"ask_amount"` + InvoiceRhash string `db:"invoice_rhash"` // used as id + Settled bool `db:"settled"` + UploadStatus uint32 `db:"status"` // upload flag status + AdminToekn string `db:"admin_token"` +} + +func (u Upload) MarshalBinary() ([]byte, error) { + return json.Marshal(u) +} + +func (u *Upload) UnmarshalBinary(b []byte) error { + return json.Unmarshal(b, &u) +} + +func (u FileUpload) MarshalBinary() ([]byte, error) { + return json.Marshal(u) +} + +func (u *FileUpload) UnmarshalBinary(b []byte) error { + return json.Unmarshal(b, &u) } // TODO: sync from redis to db @@ -262,7 +280,7 @@ func GetDownloadId(uploadId string) (string, error) { return "", err } - if len(downloadId) == 0 { + if downloadId == "" { return "", fmt.Errorf("download id missing in %s", uploadId) } @@ -324,6 +342,68 @@ func GetUploadInvoice(uploadId string) (*ln.Invoice, error) { return &invoice, nil } +func GetUploadFilesMeta(uploadId string) ([]FileUpload, error) { + key := fmt.Sprintf("upload_files_%s", uploadId) + var files []FileUpload + var exists bool + + // try redis + err := DB.Redis.Do(radix.FlatCmd(&exists, "EXISTS", key)) + if err != nil { + return nil, err + } + + // Sql + if !exists { + log.Printf("upload %s files not in cache", uploadId) + query := "SELECT * FROM file_uploads WHERE upload_id = $1" + err := DB.Sql.Select(&files, query, uploadId) + if err != nil { + return nil, err + } + + // Store back on redis cache + err = DB.Redis.Do(radix.FlatCmd(nil, "SADD", key, files)) + } else { + err = DB.Redis.Do(radix.FlatCmd(&files, "SMEMBERS", key)) + } + + return files, err + +} + +func GetUploadById(id string) (*Upload, error) { + key := fmt.Sprintf("upload_%s", id) + up := Upload{} + up.UploadId = id + + // Try from redis first + var exists bool + err := DB.Redis.Do(radix.FlatCmd(&exists, "EXISTS", key)) + if err != nil { + return nil, err + } + + // if does not exists, get it from sql then set it on redis as cache + if !exists { + log.Printf("upload %s not found on redis, caching from sql", id) + query := `SELECT * FROM uploads WHERE upload_id = $1` + err := DB.Sql.Get(&up, query, id) + if err != nil { + return nil, err + } + + // Sotre it back on redis + err = DB.Redis.Do(radix.FlatCmd(nil, "SET", key, up)) + + // Get it from redis + } else { + err = DB.Redis.Do(radix.FlatCmd(&up, "GET", key)) + } + + return &up, err +} + func GetUploadIdInvoiceId(uploadId string) (string, error) { invoice, err := GetUploadInvoice(uploadId) diff --git a/web/Caddyfile b/web/Caddyfile index 5526047..82c61cb 100644 --- a/web/Caddyfile +++ b/web/Caddyfile @@ -15,6 +15,11 @@ proxy /api localhost:8880 { transparent } +## Download rewrite +rewrite /d { + to / +} + ## test proxy /t localhost:8880 { transparent diff --git a/web/src/App.vue b/web/src/App.vue index 5632f42..535d0ea 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -12,6 +12,9 @@ diff --git a/web/src/DownloadLink.vue b/web/src/DownloadLink.vue deleted file mode 100644 index c7a3ae6..0000000 --- a/web/src/DownloadLink.vue +++ /dev/null @@ -1,25 +0,0 @@ - - - diff --git a/web/src/DownloadView.vue b/web/src/DownloadView.vue new file mode 100644 index 0000000..db06789 --- /dev/null +++ b/web/src/DownloadView.vue @@ -0,0 +1,69 @@ + + + diff --git a/web/src/Home.vue b/web/src/Home.vue index eaa78fb..75b8d1c 100644 --- a/web/src/Home.vue +++ b/web/src/Home.vue @@ -55,7 +55,7 @@ export default { // Send to upload view this.$store.commit('setInvoice', data.invoice) - this.$store.commit('setStatus', data.status) + this.$store.commit('setUpStatus', data.status) console.log("push to upload view ", data.uploadId) this.$router.push({name: 'upload', params: { uploadId: data.upload_id } diff --git a/web/src/Pay.vue b/web/src/Pay.vue index 1324bb8..d3f92c0 100644 --- a/web/src/Pay.vue +++ b/web/src/Pay.vue @@ -5,7 +5,7 @@
- ID: {{uploadId}} + ID: {{objectId}}
@@ -54,7 +54,7 @@ export default { expires: 0, } }, - props: ['invoice', 'uploadId', 'status'], + props: ['invoice', 'objectId'], methods:{ makeLnQR(payreq) { let canvas = this.$el.querySelector('#canvas') @@ -94,21 +94,9 @@ export default { paidAt: function(){ return new Date(this.invoice.paid_at).toGMTString(); }, - //paid: function() { - // return this.invoice.status == 'paid'; - //}, - - //unpaid: function(){ - // return this.invoice.status == 'unpaid'; - //}, showTimer: function(){ return !this.paid - //if (this.invoice.status !== undefined) { - // return this.invoice.status != 'paid'; - //} - - //return false; } }, watch: { diff --git a/web/src/Upload.vue b/web/src/Upload.vue index a1e270c..81f116b 100644 --- a/web/src/Upload.vue +++ b/web/src/Upload.vue @@ -13,13 +13,13 @@
- +
- + -
@@ -67,7 +67,31 @@ export default { computed:{ ...mapState({ options: state => state.upload.options - }) + }), + payAmount: { + get(){ + return this.$store.state.upload.options.request_payment_amount + }, + set (value) { + this.$store.commit('setPayAmount', value) + } + }, + payCurrency: { + get(){ + return this.$store.state.upload.options.payment_currency + }, + set(val){ + this.$store.commit('setCurrency', val) + } + }, + requestPay: { + get(){ + return this.$store.state.upload.options.request_payment + }, + set(val){ + return this.$store.commit('setRequestPay', val) + } + } }, methods: { drop(event){ diff --git a/web/src/UploadView.vue b/web/src/UploadView.vue index 62d0fa1..76c169d 100644 --- a/web/src/UploadView.vue +++ b/web/src/UploadView.vue @@ -1,6 +1,6 @@ @@ -28,7 +26,6 @@ import { mapState, mapGetters } from 'vuex' import Upload from './Upload.vue'; import Pay from './Pay.vue'; -import DownloadLink from './DownloadLink.vue'; import GetWorker from './workerInterface.js'; import Api from './api.js'; @@ -46,7 +43,7 @@ export default { } }, props: ['uploadId'], - mounted (){ + created (){ let self = this; this.worker = Worker; @@ -55,9 +52,10 @@ export default { Api.checkUploadStatus(this.uploadId) .then((data)=>{ - // Set upload metadata this.$store.commit('setInvoice', data.invoice) - this.$store.commit('setStatus', data.status) + + // Set upload metadata + this.$store.commit('setUpStatus', data.status) // if payment required, poll for invoice paid if (data.status.pay_status === 'waiting') { @@ -65,7 +63,7 @@ export default { .then((data)=>{ console.log(data) this.$store.commit('setInvoice', data.invoice) - this.$store.commit('setStatus', data.status) + this.$store.commit('setUpStatus', data.status) // if paid we get the admin/dl link if (data.status.pay_status == 'paid' ){ @@ -87,7 +85,7 @@ export default { }, methods:{ - selectItem(ev){ + selectCopy(ev){ ev.target.select() document.execCommand('copy'); } @@ -95,7 +93,7 @@ export default { computed: { ...mapState({ status: state => state.upload.status, - invoice: state => state.upload.invoice, + invoice: state => state.base.invoice, }), downloadLink(){ let loc = window.location; @@ -105,7 +103,6 @@ export default { components: { Upload, Pay, - DownloadLink, } } diff --git a/web/src/api.js b/web/src/api.js index 94ade41..be6d351 100644 --- a/web/src/api.js +++ b/web/src/api.js @@ -8,11 +8,22 @@ const endPoints = { upload: '/api/v1/u', session: '/api/v1/session', pollstatus: '/api/v1/u/poll', - checkstatus: '/api/v1/u/check' + checkstatus: '/api/v1/u/check', + download: '/api/v1/d' } +export async function download(dlId){ + + let req = new Request(endPoints.download + '/' + dlId,{ + method: 'GET', + credentials: 'same-origin' + }) + + + return fetch(req).catch((e)=>{console.error(e)}) +} export async function lastSession(){ @@ -35,16 +46,8 @@ export async function checkUploadStatus(uploadId){ credentials: 'same-origin' }) - let res = await fetch(req) - if (!res.ok && res.status != 402){ - let json = await res.json() - .catch((e)=>{ - console.error(res.text()) - }) - console.error(`${res.status}: ` + json.error) - } - return res.json().catch((e)=>{console.error(e)}) + return fetch(req).catch((e)=>{console.error(e)}) } export async function pollUploadStatus(uploadId){ @@ -131,5 +134,6 @@ export default { endPoints: endPoints, Upload: Upload, pollUploadStatus: pollUploadStatus, - checkUploadStatus: checkUploadStatus + checkUploadStatus: checkUploadStatus, + download: download, } diff --git a/web/src/index.js b/web/src/index.js index e6d46bf..4e544c0 100644 --- a/web/src/index.js +++ b/web/src/index.js @@ -8,6 +8,7 @@ import store from './store.js' import Home from './Home.vue' import App from './App.vue' import UploadView from './UploadView.vue' +import DownloadView from './DownloadView.vue' import GetWorker from './workerInterface.js' import Router from 'vue-router' import Api from './api.js' @@ -29,6 +30,12 @@ const router = new Router({ component: UploadView, props: true, }, + { + path:'/d/:dlId', + name: 'download', + component: DownloadView, + props: true, + }, { path:'/a/:adminToken', name: 'admin', diff --git a/web/src/store.js b/web/src/store.js index b5c03c9..d45f345 100644 --- a/web/src/store.js +++ b/web/src/store.js @@ -6,42 +6,65 @@ Vue.use(Vuex) const upload ={ state: { - invoice: {}, status: {}, options: { request_payment: true, - request_payment_amount: 10, + request_payment_amount: 1, payment_currency: 'SAT' }, }, - getters: { - paid(state) { - return state.status.pay_status == 'paid' + mutations:{ + setPayAmount (state, val){ + state.options.request_payment_amount = val }, - expired (state){ - return state.status.pay_status == 'expired' + setUpStatus (state, status) { + state.status = status }, - unpaid (state){ - return state.status.pay_status == 'waiting' + setCurrency(state, val){ + state.options.payment_currency = val + }, + setRequestPay(state, val){ + state.options.request_payment = val } + } +} + +const down = { + state: { + files: [], }, - mutations:{ - setInvoice (state, invoice) { - state.invoice = invoice - }, - setStatus (state, status) { - state.status = status + mutations: { + setFiles(state, val){ + state.files = val } } } const base = { state: { + invoice: { + settled: false, + }, uploadId: 0 }, mutations: { setUploadId (state, id) { state.uploadId = id + }, + setInvoice (state, invoice) { + state.invoice = invoice + } + }, + getters: { + paid(state) { + return state.invoice.settled + }, + expired (state){ + return ((new Date(state.invoice.expires_at*1000) - new Date() <= 0) && + !state.paid) + }, + unpaid (state){ + return !state.invoice.settled } } } @@ -49,6 +72,8 @@ const base = { export default new Vuex.Store({ modules: { upload, + down, base - } + }, + strict: true })