Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
127
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

updated at

マルチデバイス間でファイル転送を手軽にしたい!スマホでもPCでもcurlでも

ネットワーク越しでパイプしたり、あらゆるデバイス間でデータ転送したい! - Qiita」で登場したPiping Serverを使ったWebアプリです。
ネット上でも気に入っている人も見かけ、スマホ<=>PC間などでの活用してる声が多めでした。TypeScript + Vue + Vuetifyで手軽にファイルを転送するWebアプリが出来ました。


Piping UI: https://piping-ui.org と呼んでます。
以下のデモはiPhone => PCへの送信です。
piping-ui-ja-iphone-to-pc-short.mp4.opt.gif

以下で機能の紹介と少し技術的なことに触れていきたいと思います。
GitHubリポジトリ: https://github.com/nwtgck/piping-ui-web

  • アカウント登録なし
  • 事前の設定なし
  • 特別なアプリのインストールなし
  • curl(CLI)との相性の良いPiping Server経由
  • Progressive Web Apps (PWA)
  • ダークテーマ対応
  • 英語/日本語
  • 複数ファイル転送
  • サーバーURLとパスの記憶と自動補完
  • 画像/動画のプレビュー
  • 静的ホスト - Netlifyでビルド/デプロイ
  • パスワードでE2E暗号化

アップロードとダウンロードを同時に

piping-ui-ja-100MB-transfer.mp4.opt.gif

この転送ツールの一番の特徴が、アップロード中にダウンロード出来ることだと思います。そのため送信者のアップロードの完了を待つ必要もなくダウンロードが可能です。特に大きいファイルの転送の際に発揮する機能です。データがサーバーに保存されることはなくデータをずっと流し込むことができ、前回の実験では少なくとも868テラバイト流し込むことができました。

Piping Serverで無限にデータを送り続ける実験を47日前に開始しました。現在も進行中で、その結果では現在868テラバイトのデータが転送できています。

引用元: 「ネットワーク越しでパイプしたり、あらゆるデバイス間でデータ転送したい! - Qiita

Piping ServerのBack pressure(背圧制御)が上手いこと効いて、受信者が遅ければアップロードしすぎないように調節されます。
参考: Piping ServerでBack pressure(背圧制御)が効いてそうなことが分かるデモ動画 - TypeScript版とRust(Hyper)版 - nwtgck / Ryo Ota

Service Workerを使ってクロスオリジンでストリーミングしながらダウンロードする技術に関して後述したいと思います。

ダークテーマ対応

piping-ui-ja-dark-theme.mp4.opt.gif

最近のmacOSやiOSなどではOSでダークモードを優先するようにOSで設定出来るようになってます。デフォルトのテーマはこのマシンの設定をJavaScriptで読み込んでいます。具体的にはwindow.matchMedia('(prefers-color-scheme: dark)').matchesを使っています。Vuetify 2.xだとthis.$vuetify.theme.dark = trueでダークテーマに変更可能で、<v-app :dark="isDark">のようにdark属性は使わなくて良くなったようです。

参考:
- WebアプリでmacOSのテーマ切り替えを取得する方法 - Qiita
- javascript - How to over ride css prefers-color-scheme setting - Stack Overflow
- window.matchMedia - Web API | MDN

PWAのアップデート検知とアップデートボタン

piping-ui-en-pwa-refresh.mp4.opt.gif

Progressive Web App (PWA)の更新があることを検知して更新ボタンを表示します。TweetDeckにこの機能があってずっと実現したかったことでした。ユーザーがインストール不要で使える手軽なPWAのアップデートをスムーズに行えてとても良いです。

Give Users Control Over App Updates in Vue CLI 3 PWAs」を参考にして実現しました。この記事ではGoogleが作っているWorkboxを使ったVueでのPWAの更新ボタンを生やせるようにする方法が紹介されています。

Piping UIでPWAの更新ボタンに対応した変更はこのコミット「https://github.com/nwtgck/piping-ui-web/commit/e17639cb2237d3261136ecaf2b20f43cab69a623」です。上記の記事で書かれている変更すべきファイルをざっと把握するときに使えるかもしれません。

以下のような流れで、更新ボタンが現れます。

  • src/registerServiceWorker.tsupdated()で更新を検知してCustomEventの'swUpdated'を出す =>
  • src/App.vueで'swUpdated'のハンドラーのthis.showRefreshUIが呼ばれる =>
  • showRefreshUI内でthis.pwa.updateExists = trueに更新され、App.vue内の更新ボタンが表示される

複数ファイルを転送

piping-ui-ja-zip-send.mp4.opt.gif

同時に複数のファイルを転送できます。複数のファイルは.zipで一つにまとめられて送信されます。
.zipにする処理もクライントサイドで完結するためにJSZipを使っています。XMLHttpRequestfetchのアップロード時にストリーミングして送信できるようになれば、.zipで圧縮しながら送信するみたいなzipコマンドとcurlコマンドを組み合わせて送信できてたようなことが出来るようになります。

現在のブラウザでは、任意のReadableStreamfetchでアップロードできる方法が用意されていないようです。ですが嬉しいことに「Uploading a Request made from a ReadableStream body by yutakahirano · Pull Request #425 · whatwg/fetch」で、近いうちにChromiumにこの機能が搭載される予定があると中の人がコメントしています。この機能の実現にはすごく期待していて、圧縮や暗号化がストリーミングしながら出来るため時間的にもメモリ的にも効率性が非常に高いと思っています。

URLをクリック可能に

piping-ui-ja-url-send.mp4.opt.gif

1行のURLに限らず、テキスト内に含まれるすべてのURLがクリック可能になります。Linkifyを使っています。この機能も含めPiping UI全体的に「ふぁメモ」さんの「スマホ間でURLや画像の受け渡し (未完成) - ふぁメモ」の内容を取り入れています。この記事ではApple公式のアプリ「ショートカット」を使ってPiping Server経由でファイルやテキストを送る方法も紹介されています。

転送パスの自動補完

piping-ui-ja-auto-complete-secret-path.mp4.opt.gif

転送に使うパス(path)やサーバーのURLはLocal Storageに記憶されて今まで使ったものは補完が効きます。長いパスでも記憶させれば使いやすくなります。

English/日本語対応とTypeScriptの型の活用

piping-ui-en-to-ja.mp4.opt.gif

Androidのstrings.xmlに少し影響を受けた命名になってます。デフォルトの言語にある項目(フィールド)が他の言語にない場合は実行時にコンパイルエラーとして検出したり、パラメータを受け取って文字列を作り上げるときに順番を間違えにくくしたりする工夫をしました。

具体的には以下のように定義しています。

strings.ts
const en = {
  language: 'Language',
  dark_theme: 'Dark Theme',
  pwa_update: 'Update',
  version: `Version: ${VERSION}`,
  ...
  xhr_status_error: (p: {status: number, response: string}) => {
    return `Error (${p.status}): "${p.response}"`;
  },
  ...
};

const defaultStr = en;

const ja: typeof defaultStr = {
  language: '言語 (Language)',
  dark_theme: 'ダークテーマ',
  pwa_update: 'Update',
  version: `バージョン: ${VERSION}`,
  ...
  xhr_status_error: (p: {status: number, response: string}) => {
    return `エラー (${p.status}): "${p.response}"`;
  },
  ...
};

const ja: typeof defaultStrとなっているため、jaenにある項目がなかったり、余計な項目があるとコンパイル時にエラーとして検出可能です。またキー名だけではなく値の型も検査されるため、値が文字列でなくパラメータを受け取る関数であってもシグネチャが異なれば実行前にコンパイルエラーとして検出可能です。

以下のstrings()関数を使って各言語のときの文字列を返すようにします。strings()は純粋関数のため、この言語切替の仕組みはVueなどとは切り離されていて、他のフレームワークへの移行などもしやすいのでないかと思っています。

strings.ts
export function strings(language: string): typeof defaultStr {
  if(language.startsWith("en")) {
    return en;
  } else if(language.startsWith("ja")) {
    return ja;
  } else {
    return defaultStr;
  }
}

Vueの中では以下のようにして使っています。

@Component
export default class MyComponent extends Vue {
  private get strings() {
    return strings(globalStore.language);
  }
}

<template>内で{{ strings['cancel'] }}とすれば、英語なら{{ "Cancel" }}に日本語なら{{ "キャンセル" }}になるイメージです。
this.strings['cancel']はパラメータなしの文字列ですが、引数を与える必要がある文字列も以下のように使うことが出来ます。

this.strings['xhr_status_error']({
  status: this.xhr.status,
  response: this.xhr.responseText
});

<script lang="ts">内に書いた場合はきちっとコンパイルエラーがあれば検出出来るので安全なのではないかと思っています。

"https://"を自動補完

piping-ui-ja-scheme-auto-complete.mp4.opt.gif
地味ですが便利です。中央集権を避け、手軽に他のサーバーURLに追加したり切り替えられるようにしたいです。

OSSライセンス一覧

一覧はsrc/license.jsonとして生成されます。なるべく自動生成されるものをGit管理するのは避けたいため、npmスクリプトの"posntinstall"license-checkerを使ってsrc/license.jsonを生成しています。TypeScriptのコンパイラオプションで"resolveJsonModule": trueを指定するとimport licenses from '@/licenses.jsonできるようになるため、これでライセンス情報のJSONを読み込みます。

参考: How to Import json into TypeScript - By

転送の仕組み

Web標準で使えるfetch() (fetch)を使って手軽にPiping Serverを経由してデータを転送できます。

送信者
// 送信
fetch("https://ppng.ml/mypath1", {
  method: 'POST',
  body: 'hello!'
});
受信者
// 受信
fetch("https://ppng.ml/mypath1")
  .then(res => res.text())
  .then(text => console.log(text));

送信者の'hello!'の代わりにBlobにしたり、<input type="file">からのFileを入れたりすれば、テキストに限らず色んなデータをHTTPのPOSTとGETで手軽に送ることが出来ます。

受信側もres.text()の代わりにres.blob()res.arrayBuffer()でバイナリを受け取れ、res.bodyReadableStreamを返す)を使うことでストリーミングしながらのバイナリデータを読み取ることもできます。

クロスオリジンでストリーミングしながらダウンロードする

やりたいことは、同一オリジンでないAccess-Control-Allow-Origin: *なファイルをブラウザで表示させずに保存させたいです。<a download>を動的生成してダウンロードできそうですが、"download"属性は同一オリジンでないとダウンロード出来ないようです。
参考: https://developer.mozilla.org/ja/docs/Web/HTML/Element/a#Attributes

そこでこのブラウザの制限を回避するためにService Workerを使います。StreamSaver.jsで使われているテクニックです。Firefox Sendでも使われていた思います。最小構成で再現した方法は「ファイルのストリーミング強制保存をクロスオリジンでも実現させるService Workerの裏技ぽい使い方 - nwtgck / Ryo Ota」にまとめました。

Piping UIでのこの実装はこのコミット「https://github.com/nwtgck/piping-ui-web/commit/31a8f7331d61aa0981337fe5df58fb731ac62f52」です。上記の記事との違いはPiping UIではこのService Workerを使った方法に対応しているか確認するためのパスを用意したり、PWAのWorkboxを使ったものに適用したところです。

このストリーミングしながらのダウンロードに対応できるブラウザだと、以下の画像のようにダウンロードのURLがhttps://piping-ui.org/sw-download?url=...&filename=...のようになります。

Piping UIはNetlifyにデプロイされた静的なファイルだけで構成されるWebアプリですが、/sw-downloadはService Workerがリバースプロキシのようになってリクエストをさばいて適切にPiping Server経由でデータをダウンロードします。

vue-cli-service serveだとService Workerが働かないので(webpack-dev-serverも同じかも)、ファイル監視してビルドする仕組みを作ってそれを使ってます (https://github.com/nwtgck/watch-build-serve-node)。検索しても見つからなかったので、こういう用途を満たすものがあったらそれに移行したいと思っています。

curlユーザーとの互換性

以下はターミナル => Piping UIです。
piping-ui-ja-terminal-to-ui-big.mp4.opt.gif

以下はPiping UI => ターミナルです。
piping-ui-ja-ui-to-terminal-big.mp4.opt.gif

Piping UIではこの互換性をすごく大事にしました。curlwgetなどが使える環境とブラウザを繋ぎやすくしたかったからです。送信は... | curl -T - <URL>, 受信はcurl <URL>でシンプルなままです。
curlコマンドでの活用法は「ネットワーク越しでパイプしたり、あらゆるデバイス間でデータ転送したい! - Qiita」にまとめています。

curlとの互換 - チャット/通話/画面共有/エンドツーエンド暗号化

上記のcurlとの互換性の話です。

HTTPはボディをストリーミング可能です。ですが、残念ながら現在のブラウザ上で動くJavaScriptはストリームに弱いと感じてます。任意のReadableStreamがアップロード出来なかったり、前述したストリーミングしながらダウンロードする機能の話にも関連して弱いと感じてます。繰り返しになりますが、嬉しいことに「Uploading a Request made from a ReadableStream body by yutakahirano · Pull Request #425 · whatwg/fetch」で、近いうちにChromiumにこの機能が搭載される予定があると中の人がコメントしていてストリーミングしながらのアップロードも近い将来にブラウザ上で実現できそうです。

以下のWebアプリたちは、ブラウザ上で小さなチャンクに分けてPiping Serverを経由することで擬似的にストリーミングして実現しています。

Webアプリ間でエンドツーエンド暗号化してファイルを転送したり、チャットしたり、画面共有します。将来、fetchで任意のReadableStreamがアップロードできるようになると、Webアプリ間だけではなくてcurlなどのCLIとの連携が可能になるのではないかと思っています。

おわりに

Piping Serverの実装は前の記事の時と比べてほとんど同じです。Piping ServerをPiping UIや途中で出てきたWebアプリを実装するために特殊な実装をしたりはしていません。あらゆるデータをストリーミングできれば、ファイル転送に限らず、チャットも通話も画面共有も実現できそうです。
より少ない機能で、より多くのことを実現」出来ればいいなと思っています。

おまけ: gifのプログレスバー表示

最後におまけです。以下を使ってプログレスバー付きのgifを生成してます。
https://github.com/nwtgck/gif-progress

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
127
Help us understand the problem. What are the problem?