From 76534c2296444a1e6ea6f7ddb26e9003ea12175d Mon Sep 17 00:00:00 2001 From: George Knee Date: Wed, 5 Jul 2023 11:35:38 +0100 Subject: [PATCH 1/6] add basic http server supporting range requests --- packages/boost-demo/go.mod | 3 +++ packages/boost-demo/main.go | 29 +++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 packages/boost-demo/go.mod create mode 100644 packages/boost-demo/main.go diff --git a/packages/boost-demo/go.mod b/packages/boost-demo/go.mod new file mode 100644 index 0000000..c5d2e1f --- /dev/null +++ b/packages/boost-demo/go.mod @@ -0,0 +1,3 @@ +module test-server + +go 1.20 diff --git a/packages/boost-demo/main.go b/packages/boost-demo/main.go new file mode 100644 index 0000000..f418677 --- /dev/null +++ b/packages/boost-demo/main.go @@ -0,0 +1,29 @@ +package main + +import ( + "bytes" + "io" + "log" + "net/http" + "time" +) + +func main() { + + resp, err := http.Get("http://magmo.com/nitro-protocol.pdf") + if err != nil { + panic(err) + } + + file, err := io.ReadAll(resp.Body) + if err != nil { + panic(err) + } + + http.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) { + http.ServeContent(w, r, "test.pdf", time.Now(), io.NewSectionReader(bytes.NewReader(file), 0, int64(len(file)))) + }) + + log.Fatal(http.ListenAndServe(":8081", nil)) + +} From 9065d34c81af1d081ebbacbd2421c709d1240133 Mon Sep 17 00:00:00 2001 From: George Knee Date: Wed, 5 Jul 2023 11:40:08 +0100 Subject: [PATCH 2/6] simplify UI --- packages/boost-demo/main.go | 4 +- packages/boost-demo/src/App.tsx | 166 ++------------------------------ 2 files changed, 12 insertions(+), 158 deletions(-) diff --git a/packages/boost-demo/main.go b/packages/boost-demo/main.go index f418677..920f08d 100644 --- a/packages/boost-demo/main.go +++ b/packages/boost-demo/main.go @@ -20,8 +20,8 @@ func main() { panic(err) } - http.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) { - http.ServeContent(w, r, "test.pdf", time.Now(), io.NewSectionReader(bytes.NewReader(file), 0, int64(len(file)))) + http.HandleFunc("/nitro-protocol.pdf", func(w http.ResponseWriter, r *http.Request) { + http.ServeContent(w, r, "nitro-protocol.pdf", time.Now(), io.NewSectionReader(bytes.NewReader(file), 0, int64(len(file)))) }) log.Fatal(http.ListenAndServe(":8081", nil)) diff --git a/packages/boost-demo/src/App.tsx b/packages/boost-demo/src/App.tsx index aa468f9..9f8486e 100644 --- a/packages/boost-demo/src/App.tsx +++ b/packages/boost-demo/src/App.tsx @@ -1,111 +1,22 @@ -import { ChangeEvent, useEffect, useState } from "react"; -import { NitroRpcClient } from "@statechannels/nitro-rpc-client"; -import { PaymentChannelInfo } from "@statechannels/nitro-rpc-client/src/types"; -import { - Select, - MenuItem, - SelectChangeEvent, - Button, - TextField, - Box, - Table, - TableRow, - TableCell, - TableBody, -} from "@mui/material"; +import { useState } from "react"; +import { Box, Button } from "@mui/material"; import axios, { isAxiosError } from "axios"; -const QUERY_KEY = "rpcUrl"; - import "./App.css"; function App() { - const retrievalProvider = "0xbbb676f9cff8d242e9eac39d063848807d3d1d94"; - const hub = "0x111a00868581f73ab42feef67d235ca09ca1e8db"; - const defaultUrl = "localhost:4005"; - - const url = - new URLSearchParams(window.location.search).get(QUERY_KEY) ?? defaultUrl; - - const [nitroClient, setNitroClient] = useState(null); - const [paymentChannels, setPaymentChannels] = useState( - [] - ); - const [selectedChannel, setSelectedChannel] = useState(""); - const [selectedChannelInfo, setSelectedChannelInfo] = useState< - PaymentChannelInfo | undefined - >(); - - // TODO: For now the default is a hardcoded value based on a local file - // If you're running this locally you'll need to override this value - // Ideally we should just query boost/lotus for the list of available payloads> - const [payloadId, setPayloadId] = useState( - "bafk2bzaceapnitekx4sp3mtitqatm5zpxn6nvjicwtltomttrlof65wlcfjpa" - ); - const [errorText, setErrorText] = useState(""); - useEffect(() => { - NitroRpcClient.CreateHttpNitroClient(url).then((c) => setNitroClient(c)); - }, [url]); - - // Fetch all the payment channels for the retrieval provider - useEffect(() => { - if (nitroClient) { - // TODO: We should consider adding a API function so this ins't as painful - nitroClient.GetAllLedgerChannels().then((ledgers) => { - for (const l of ledgers) { - if (l.Balance.Hub != hub) continue; - - nitroClient.GetPaymentChannelsByLedger(l.ID).then((payChs) => { - const withProvider = payChs.filter( - (p) => p.Balance.Payee == retrievalProvider - ); - setPaymentChannels(withProvider); - }); - } - }); - } - }, [nitroClient]); - - const updateChannelInfo = async (channelId: string) => { - const paymentChannel = await nitroClient?.GetPaymentChannel(channelId); - setSelectedChannelInfo(paymentChannel); - }; - - const handleSelectedChannelChanged = async (event: SelectChangeEvent) => { - setSelectedChannel(event.target.value); - updateChannelInfo(event.target.value); - }; - - const updatePayloadId = (e: ChangeEvent) => { - setPayloadId(e.target.value); - }; - - const makePayment = () => { - setErrorText(""); - if (nitroClient && selectedChannel) { - nitroClient.Pay(selectedChannel, 100); - // TODO: Slightly hacky but we wait a beat before querying so we see the updated balance - setTimeout(() => { - updateChannelInfo(selectedChannel); - }, 50); - } - }; - const fetchFile = () => { setErrorText(""); axios - .get( - `http://localhost:7777/ipfs/${payloadId}?channelId=${selectedChannel}`, - { - responseType: "blob", // This lets us download the file - headers: { - Accept: "*/*", // TODO: Do we need to specify this? - }, - } - ) + .get(`http://localhost:8081/nitro-protocol.pdf`, { + responseType: "blob", // This lets us download the file + headers: { + Accept: "*/*", // TODO: Do we need to specify this? + }, + }) .then((result) => { // This will prompt the browser to download the file @@ -129,65 +40,8 @@ function App() { return ( - - - - - - - Paid so far - - {selectedChannelInfo && - // TODO: We shouldn't have to cast to a BigInt here, the client should return a BigInt - BigInt(selectedChannelInfo?.Balance.PaidSoFar).toString(10)} - - - - Remaining funds - - {selectedChannelInfo && - // TODO: We shouldn't have to cast to a BigInt here, the client should return a BigInt - BigInt(selectedChannelInfo?.Balance.RemainingFunds).toString( - 10 - )} - - - - Payee - - {selectedChannelInfo && selectedChannelInfo.Balance.Payee} - - - - Payer - - {selectedChannelInfo && selectedChannelInfo.Balance.Payer} - - - -
-
- - - - - - - {errorText} - + + {errorText}
); } From 067717d11ad0cf0ad376539d35a2deec052cfe43 Mon Sep 17 00:00:00 2001 From: George Knee Date: Wed, 5 Jul 2023 11:44:05 +0100 Subject: [PATCH 3/6] set CORS headers --- packages/boost-demo/main.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/boost-demo/main.go b/packages/boost-demo/main.go index 920f08d..53202be 100644 --- a/packages/boost-demo/main.go +++ b/packages/boost-demo/main.go @@ -21,6 +21,15 @@ func main() { } http.HandleFunc("/nitro-protocol.pdf", func(w http.ResponseWriter, r *http.Request) { + + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PUT") + w.Header().Set("Access-Control-Allow-Headers", "*") + if r.Method == "OPTIONS" { + _, _ = w.Write([]byte("OK")) + return + } + http.ServeContent(w, r, "nitro-protocol.pdf", time.Now(), io.NewSectionReader(bytes.NewReader(file), 0, int64(len(file)))) }) From 979850d22d5ba64e9e0cc4da668e836b3f41c149 Mon Sep 17 00:00:00 2001 From: George Knee Date: Wed, 5 Jul 2023 13:51:03 +0100 Subject: [PATCH 4/6] make it work --- packages/boost-demo/src/App.tsx | 85 ++++++++++++++++++++++++--------- 1 file changed, 62 insertions(+), 23 deletions(-) diff --git a/packages/boost-demo/src/App.tsx b/packages/boost-demo/src/App.tsx index 9f8486e..1e52fd6 100644 --- a/packages/boost-demo/src/App.tsx +++ b/packages/boost-demo/src/App.tsx @@ -5,41 +5,80 @@ import axios, { isAxiosError } from "axios"; import "./App.css"; function App() { + const numChunks = 100; + const length = 403507; + + const [gotChunks, setGotChunks] = useState(new Array(numChunks).fill(false)); + + function setChunkGot(index: number) { + const gotChunksCopy = [...gotChunks]; + gotChunksCopy[index] = true; + setGotChunks(gotChunksCopy); + } + const [errorText, setErrorText] = useState(""); - const fetchFile = () => { - setErrorText(""); + async function getChunk(index: number, file: Int8Array[]) { + if (index >= numChunks) { + throw Error; + } + const chunkSize = Math.floor(length / numChunks); + const startByte = index * chunkSize; + const endByte = Math.min(length, (index + 1) * chunkSize); + const range = "bytes=" + startByte + "-" + endByte; - axios - .get(`http://localhost:8081/nitro-protocol.pdf`, { - responseType: "blob", // This lets us download the file - headers: { - Accept: "*/*", // TODO: Do we need to specify this? - }, - }) - - .then((result) => { - // This will prompt the browser to download the file - const blob = result.data; - const blobUrl = URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = blobUrl; - a.download = "fetched-file-from-ipfs"; - a.click(); - a.remove(); - window.URL.revokeObjectURL(blobUrl); - }) - .catch((e) => { + try { + const result = await axios.get( + `http://localhost:8081/nitro-protocol.pdf`, + { + responseType: "blob", // This lets us download the file + headers: { + Accept: "*/*", // TODO: Do we need to specify this? + Range: range, + }, + } + ); + setChunkGot(index); + file[index] = result.data; + } catch { + (e: any) => { if (isAxiosError(e)) { setErrorText(`${e.message}: ${e.response?.statusText}`); } else { setErrorText(JSON.stringify(e)); } - }); + }; + } + } + + function compileFile(file: Int8Array[]): File { + const f = new File(file, "nitro-protocol.pdf"); + return f; + } + + function triggerCompleteFileDownload(file: Int8Array[]) { + const fileUrl = URL.createObjectURL(compileFile(file)); + const a = document.createElement("a"); + a.href = fileUrl; + a.download = "nitro-protocol.pdf"; + a.click(); + a.remove(); + window.URL.revokeObjectURL(fileUrl); + } + + const fetchFile = async () => { + setErrorText(""); + const file = new Array(numChunks); + for (let i = 0; i < numChunks; i++) { + await getChunk(i, file); + } + + triggerCompleteFileDownload(file); }; return ( + {gotChunks.map((c) => (c ? "X" : "_"))} {errorText} From cb01bb7f8e94be9d6675e899763c3c34ac29cfe7 Mon Sep 17 00:00:00 2001 From: George Knee Date: Wed, 5 Jul 2023 14:03:42 +0100 Subject: [PATCH 5/6] support various download methods --- packages/boost-demo/src/App.tsx | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/packages/boost-demo/src/App.tsx b/packages/boost-demo/src/App.tsx index 1e52fd6..27b4fc9 100644 --- a/packages/boost-demo/src/App.tsx +++ b/packages/boost-demo/src/App.tsx @@ -5,7 +5,7 @@ import axios, { isAxiosError } from "axios"; import "./App.css"; function App() { - const numChunks = 100; + const numChunks = 10; const length = 403507; const [gotChunks, setGotChunks] = useState(new Array(numChunks).fill(false)); @@ -66,7 +66,7 @@ function App() { window.URL.revokeObjectURL(fileUrl); } - const fetchFile = async () => { + const fetchFileChunks = async () => { setErrorText(""); const file = new Array(numChunks); for (let i = 0; i < numChunks; i++) { @@ -76,10 +76,36 @@ function App() { triggerCompleteFileDownload(file); }; + const fetchFileConcurrently = async () => { + setErrorText(""); + const file = new Array(numChunks); + const promises = []; + for (let i = 0; i < numChunks; i++) { + promises.push(getChunk(i, file)); + } + await Promise.all(promises); + triggerCompleteFileDownload(file); + }; + + const fetchFileFull = async () => { + setErrorText(""); + const file = new Array(numChunks); + const result = await axios.get(`http://localhost:8081/nitro-protocol.pdf`, { + responseType: "blob", // This lets us download the file + headers: { + Accept: "*/*", // TODO: Do we need to specify this? + }, + }); + file[0] = result.data; + + triggerCompleteFileDownload(file); + }; return ( {gotChunks.map((c) => (c ? "X" : "_"))} - + + + {errorText} ); From 2d3645f0fc79bdfd5b0ce618b3b69dd1467437e6 Mon Sep 17 00:00:00 2001 From: George Knee Date: Wed, 5 Jul 2023 18:20:27 +0100 Subject: [PATCH 6/6] add comment and change chunk size --- packages/boost-demo/src/App.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/boost-demo/src/App.tsx b/packages/boost-demo/src/App.tsx index 27b4fc9..169e8f7 100644 --- a/packages/boost-demo/src/App.tsx +++ b/packages/boost-demo/src/App.tsx @@ -5,7 +5,7 @@ import axios, { isAxiosError } from "axios"; import "./App.css"; function App() { - const numChunks = 10; + const numChunks = 3; const length = 403507; const [gotChunks, setGotChunks] = useState(new Array(numChunks).fill(false)); @@ -41,6 +41,8 @@ function App() { setChunkGot(index); file[index] = result.data; } catch { + // TODO check that the status code is 206 Partial Content + // It could be 200 OK -- but then we may have the entire file already! (e: any) => { if (isAxiosError(e)) { setErrorText(`${e.message}: ${e.response?.statusText}`);