WIP download start and keep dl invoice session

master
Chakib Benziane 5 years ago
parent 53f37ff619
commit 4105c33e25

@ -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{})
}

@ -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

@ -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)

@ -15,6 +15,11 @@ proxy /api localhost:8880 {
transparent
}
## Download rewrite
rewrite /d {
to /
}
## test
proxy /t localhost:8880 {
transparent

@ -12,6 +12,9 @@
<script charset="utf-8">
import GetWorker from './workerInterface.js';
const Worker = GetWorker('main');
import { mapState, mapGetters } from 'vuex'
const dlUrlRegex = /d\/(\w+)\/?/
export default {
@ -23,18 +26,27 @@ export default {
this.worker.listenTo('upload-invoice', (e) => {
console.log("received invoice ", e.data)
this.$store.commit('setInvoice', e.data.invoice)
this.$store.commit('setStatus', e.data.status)
this.$store.commit('setUpStatus', e.data.status)
// if we are not on upload route go there
if ( this.$router.currentRoute.name !== 'upload' ) {
this.$router.push({name: 'upload', params: { uploadId: e.data.upload_id }})
}
// self.invoice = e.data.invoice;
// self.uploadId = e.data.upload_id;
// self.status = e.data.status;
})
}
// On load if the url is a download url route to download path
let loc = window.location
if (loc.pathname.match(dlUrlRegex)) {
let dlId = dlUrlRegex.exec(loc.pathname)[1]
history.replaceState("", `download ${dlId}`, '/' )
this.$router.replace({
name: 'download',
replace: true,
params: {dlId}
})
}
},
}
</script>

@ -1,25 +0,0 @@
<template>
<div class="download">
<p>upload id: <span class="last-upload">{{ id }}</span></p>
<p>download: <a :href="link" class="dl-link">{{link}}</a></p>
</div>
</template>
<script>
const baseDlPath = "/d/"
export default {
props: ['id'],
mounted(){
},
computed:{
link(){
let linkTarget = baseDlPath + this.id
return new URL(linkTarget, window.location.toString())
}
}
}
</script>

@ -0,0 +1,69 @@
<template>
<div id="download">
<pay v-if="!error" :objectId="dlId" :invoice="invoice"></pay>
<div v-if="error" class="f4 mv5 light-red ttu">{{errorMsg}}</div>
<upload v-if="error"></upload>
</div>
</template>
<script>
import { mapState, mapGetters } from 'vuex'
import Pay from './Pay.vue';
import Api from './api.js';
import Upload from './Upload.vue';
export default {
name: 'DownloadView',
props: ['dlId'],
data(){
return {
error: false,
errorMsg: "",
}
},
created(){
let self = this
// Query for download invoice
Api.download(this.dlId)
.then((res)=>{
if (res.status === 404){
self.error = true
self.errorMsg = "this link does not exist anymore"
// ask payment
} else if ( res.status === 402) {
res.json()
.then((data)=>{
this.$store.commit('setInvoice', data.invoice)
this.$store.commit('setFiles', data.files)
})
.catch((e)=>{console.log(e)})
}
})
},
computed: {
...mapState({
invoice: state => state.base.invoice,
files: state => state.down.files,
}),
...mapGetters([
'paid',
'unpaid'
]),
downloadLink(){
return 'TODO'
},
},
components: {
Pay,
Upload,
}
}
</script>

@ -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 }

@ -5,7 +5,7 @@
<div class="uid flex justify-between items-center w-100 f5">
<span class="b f5 mid-gray">ID: {{uploadId}}</span>
<span class="b f5 mid-gray">ID: {{objectId}}</span>
</div>
</div>
@ -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: {

@ -13,13 +13,13 @@
<button class="mb3" @click.stop.prevent="chooseFiles" id="fileSelect">pick files</button>
<div class="askFee flex justify-start items-center">
<input id="request_payment" type="checkbox" v-model="options.request_payment">
<input id="request_payment" type="checkbox" v-model="requestPay">
<label class="ph2 pv2" for="request_payment">Ask fee for download ?</label>
</div>
<div class="options w-100 flex flex-row justify-between">
<input :disabled="!options.request_payment" placeholder="ask fee for download" v-model.trim.number="options.request_payment_amount" type="number">
<input :disabled="!options.request_payment" placeholder="ask fee for download" v-model.trim.number="payAmount" type="number">
<select :disabled="!options.request_payment" v-model="options.payment_currency">
<select :disabled="!options.request_payment" v-model="payCurrency">
<option v-for="cur in currencies">{{cur}}</option>
</select>
</div>
@ -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){

@ -1,6 +1,6 @@
<template>
<div id="upload-view" class="flex flex-column items-center justify-center">
<pay :uploadId="uploadId" :status="status" :invoice="invoice"></pay>
<pay :objectId="uploadId" :invoice="invoice"></pay>
<div class="hr"></div>
<form id="accepted" class="flex flex-column mt5 w-100" v-if="accepted" >
@ -8,17 +8,15 @@
<span class="normal b orange">Your admin token:</span>
</label>
<input class="input-reset f6 b ba b--black-20 pa2 mb2 db w-100 mid-gray" @click="selectItem" id="adminToken" type="text" v-model="adminToken">
<input class="input-reset f6 b ba b--black-20 pa2 mb2 db w-100 mid-gray" @click="selectCopy" id="adminToken" type="text" v-model="adminToken">
<label for="adminToken" class="f7 db mb2 mt3">
<label for="downloadLink" class="f7 db mb2 mt3">
<span class="normal b blue">Download link:</span>
</label>
<input class="input-reset f6 b ba b--black-20 pa2 mb2 db w-100 mid-gray" @click="selectItem" id="downloadLink" type="text" v-model="downloadLink">
<input class="input-reset f6 b ba b--black-20 pa2 mb2 db w-100 mid-gray" @click="selectCopy" id="downloadLink" type="text" v-model="downloadLink">
</form>
<!--<upload></upload>-->
<!--<download-link v-if="uploadId && paid" :id="uploadId"></download-link>-->
</div>
</template>
@ -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,
}
}
</script>

@ -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,
}

@ -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',

@ -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
})

Loading…
Cancel
Save