2017/06/22

Recent entries from same category

  1. Go 言語プログラミングエッセンスという本を書きました。
  2. errors.Join が入った。
  3. unsafe.StringData、unsafe.String、unsafe.SliceData が入った。
  4. Re: Go言語で画像ファイルか確認してみる
  5. net/url に JoinPath が入った。

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

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)
Posted at by