2017/06/22

Recent entries from same category

  1. Golang で HTTP コンテンツの charset 判定をするには
  2. Golang の archive/zip でタイムゾーンの問題とファイル名の問題が解決した。
  3. Golang で優先度を変えてプロセスを起動する。
  4. net/http でレスポンスの内容を確認したいなら io.TeeReader を使おう
  5. Golang で物理ファイルの操作に path/filepath でなく path を使うと爆発します。

要求仕様から工数を出す側から言うと「ブラウザのダウンロード画面に進捗出てるから要らないでしょ」と言いたい所でしたが「出来ないのか」と言われると「出来るもん」と言わざると得ないエンジニア魂。

JavaScript - ブラウザから、ファイルをダウンロードしている途中で、プログレスバーを実装したい。完了したら、プログレスバーを閉じたい。(81363)|teratail

前提・実現したいこと javaScript/HTML/CSSを利用しております。 目的は、ブラウザから、ファイルをダウンロードしている途中で、プログレスバーを実装したい。完了したら、プログレスバーを閉...

https://teratail.com/questions/81363

通常、ブラウザからファイルをダウンロードする際は javascript からは制御できません。サーバからバイト列を JSON で Range っぽく返して最後に data スキームでダウンロードダイアログを出す、といったニッチなテクニックでも事も出来なくないですがブラウザにメモリを保持してしまって大きいファイルだとハングしかねない等の問題が発生します。で、どうやるかというとまずはサーバの処理

package main

import (
    "fmt"
    "log"
    "net/http"
    "os"
    "sync"

    "github.com/google/uuid"
    "github.com/labstack/echo"
)

var (
    m = &sync.Map{}
)

// ダウンロードの進捗を JSON で返す
func stat(c echo.Context) error {
    ck, err := c.Cookie("download-progress")
    if err != nil {
        log.Println(err)
        return err
    }
    progress := 0
    v, ok := m.Load(ck.Value)
    if ok {
        if vi, ok := v.(int); ok {
            progress = vi
        }
    }
    return c.JSON(http.StatusOK, &struct {
        Progress int `json:"progress"`
    }{
        Progress: progress,
    })
}

// クライアントにデータを送信しつつ進捗を更新
func download(c echo.Context) error {
    id := uuid.New().String()
    c.SetCookie(&http.Cookie{
        Name:  "download-progress",
        Value: id,
    })
    f, err := os.Open("ubuntu-17.04-server-amd64.iso")
    if err != nil {
        log.Println(err)
        return err
    }
    defer f.Close()
    st, err := f.Stat()
    if err != nil {
        log.Println(err)
        return err
    }
    total := st.Size()
    rest := total
    m.Store(id, 0)

    w := c.Response().Writer
    w.Header().Set("Content-Disposition""attachment")
    w.Header().Set("Content-Length", fmt.Sprint(total))
    for {
        var b [4098]byte
        n, err := f.Read(b[:])
        if err != nil {
            break
        }
        _, err = w.Write(b[:n])
        if err != nil {
            break
        }
        rest -= int64(n)
        m.Store(id, int((total-rest)*100/total))
        if total <= 0 {
            break
        }
    }
    m.Store(id, nil)
    return nil
}

func main() {
    e := echo.New()

    e.GET("/stat", stat)
    e.GET("/download", download)

    e.Static("/""static")
    e.Logger.Fatal(e.Start(":8989"))
}

そしてクライアント側の処理

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>download</title>
<script>
window.addEventListener('load'function() {
  function progress() {
    fetch("/stat"{
      'credentials'"same-origin"
    }).then(function(response) {
      return response.json();
    }).then(function(json) {
      document.querySelector('#progress').textContent = json.progress + "%";
      if (json.progress < 100) setTimeout(progress, 1000);
    })
  }
  document.querySelector('#download').addEventListener('click'function() {
    progress();
    return true;
  });
}false);
</script>
</head>
<body>
    <p>
        <span id="progress"></span>
    </p>
    <a id="download" href="/download">Download</a>
</body>
</html>

ダウンロードが始まったらランダムIDでクッキーを返送し、そのIDでステータスの要求を受け付ける。ダウンロードは細かい単位で行い都度進捗を更新する。こうすればダウンロードが始まれば進捗がパーセンテージで表示され、終了すればタイマーが止まる。プログレスバー表示やダウンロードをキャンセルした際の処理はめんどくさいので実装してないですが分かりますよね。あとダウンロードが終わったら m から破棄しないと何時かサーバがパンクしますよっと。

ダウンロードの進捗表示は出来なくはない。ただ、これだけは言っておきたい。

実装は、仕事でやるならタダじゃない(575)

blog comments powered by Disqus