I am new to Go
and trying to make a cross-browser app which can download multiple url's with progress bar. The Grab
package does the job nicely as shown in the example below. Now, I want to have a self-contained/portable/single-executable Web-UI which can display the download progress from the below code in a web-browser?
package main
import (
"fmt"
"github.com/cavaliercoder/grab"
"os"
"time"
)
func main() {
// get URL to download from command args
if len(os.Args) < 2 {
fmt.Fprintf(os.Stderr, "usage: %s url [url]...
", os.Args[0])
os.Exit(1)
}
urls := os.Args[1:]
// start file downloads, 3 at a time
fmt.Printf("Downloading %d files...
", len(urls))
respch, err := grab.GetBatch(3, ".", urls...)
if err != nil {
fmt.Fprintf(os.Stderr, "%v
", err)
os.Exit(1)
}
// start a ticker to update progress every 200ms
t := time.NewTicker(200 * time.Millisecond)
// monitor downloads
completed := 0
inProgress := 0
responses := make([]*grab.Response, 0)
for completed < len(urls) {
select {
case resp := <-respch:
// a new response has been received and has started downloading
// (nil is received once, when the channel is closed by grab)
if resp != nil {
responses = append(responses, resp)
}
case <-t.C:
// clear lines
if inProgress > 0 {
fmt.Printf("\033[%dA\033[K", inProgress)
}
// update completed downloads
for i, resp := range responses {
if resp != nil && resp.IsComplete() {
// print final result
if resp.Error != nil {
fmt.Fprintf(os.Stderr, "Error downloading %s: %v
", resp.Request.URL(), resp.Error)
} else {
fmt.Printf("Finished %s %d / %d bytes (%d%%)
", resp.Filename, resp.BytesTransferred(), resp.Size, int(100*resp.Progress()))
}
// mark completed
responses[i] = nil
completed++
}
}
// update downloads in progress
inProgress = 0
for _, resp := range responses {
if resp != nil {
inProgress++
fmt.Printf("Downloading %s %d / %d bytes (%d%%)\033[K
", resp.Filename, resp.BytesTransferred(), resp.Size, int(100*resp.Progress()))
}
}
}
}
t.Stop()
fmt.Printf("%d files successfully downloaded.
", len(urls))
}
One kind of solution would be to use websocket, because to establish a connection it uses a special kind of header that reduces the number of handshakes required between browser and server to only one, which means the client server communication will not block.
This connection will remain active throughout its lifetime, and you can use JavaScript to write or read data from this connection, as in the case of a conventional TCP sockets.
As a matter of implementation you can encode the resulting information composed by grab
package as a json
string then you can transmit this string with websocket.JSON.Send
.
func (cd Codec) Send(ws *Conn, v interface{}) (err error)
And on the client part you can obtain the composed json object which later you can parse and use as you wish, depending on your purpose of use and the technology prefered (canvas, DOM etc.).
Below is a snippet of websocket server side:
package main
import (
"fmt"
"golang.org/x/net/websocket"
"net/http"
)
type FileInformation struct {
BytesTransferred int
Size string
Filename string
Progress int
}
func Echo(ws *websocket.Conn) {
info := FileInformation{
BytesTransferred: 70,
Size: "1.7MB",
Filename: "test.txt",
Progress: 60,
}
for {
if err := websocket.JSON.Send(ws, info); err != nil {
fmt.Println("Error sending message")
break
}
// if BytesTransferred == 100 break
}
}
func main() {
http.Handle("/", websocket.Handler(Echo))
if err := http.ListenAndServe(":1234", nil); err != nil {
fmt.Println("Cannot iniatiate a websocket connection")
}
}
Of course the values are hard coded and you can use a timer tick if you wish to alter the transfer speed.
The client side written in go:
package main
import (
"fmt"
"golang.org/x/net/websocket"
"io"
"os"
)
func main() {
type FileInformation struct {
BytesTransferred int
Size string
Filename string
Progress int
}
if len(os.Args) > 1 && (os.Args[1] == "--help" || os.Args[1] == "-h") {
fmt.Println("Usage : " + os.Args[0] + " ws://localhost:port")
os.Exit(1)
}
conn, err := websocket.Dial(os.Args[1], "", "http://127.0.0.1")
checkError(err)
var info FileInformation
for {
err := websocket.JSON.Receive(conn, &info)
if err != nil {
if err == io.EOF {
break
}
fmt.Println("Couldn't receive msg " + err.Error())
break
}
fmt.Printf("%s", info)
if err := websocket.JSON.Send(conn, info); err != nil {
checkError(err)
}
}
}
func checkError(err error) {
if err != nil {
fmt.Println("Fatal error: " + err.Error())
os.Exit(1)
}
}
In javascript you can connect to websocket and receive the data as:
<script type="text/javascript">
var sock = null;
var wsuri = "ws://127.0.0.1:1234";
window.onload = function() {
console.log("onload");
sock = new WebSocket(wsuri);
sock.onopen = function() {
console.log("connected to " + wsuri);
}
sock.onclose = function(e) {
console.log("connection closed (" + e.code + ")");
}
sock.onmessage = function(e) {
console.log("message received: " + e.data);
// deserialize json data
var json = JSON.parse(e.data);
}
};
function send() {
var msg = document.getElementById('message').value;
sock.send(msg);
};
</script>