React+canvasで別ドメインの画像ファイルをローカルに保存する
Reactでcanvasを使用して「ボタンを押したら画像をダウンロードする機能」を実装しようとしたら詰まったので備忘録 かなりニッチなニーズ
やりたいこと
ボタンのクリックで画像をダウンロードする機能を作ります 別ドメインだと厄介だったので残しておきます
aタグによる画像のダウンロード
通常aタグにdownload属性をつければファイルは簡単にダウンロードできますが
<a href="https://hoge.com/sample.png" download="saved.png">ダウンロード</a>
のように別ドメインのサーバーに存在する画像を指定すると別タブで開くだけです
aタグのdownload属性は同一オリジンでのみ動作するので別ドメインの画像はダウンロードできません
canvasを使用してダウンロード
canvasを使用して画像をblobに変換してダウンロードする方法
const c = document.getElementById('canvas'); c.toBlob((blob) => { const url = URL.createObjectURL(blob); const a = document.createElement("a"); document.body.appendChild(a); a.download = 'foo.png'; a.href = url; a.click(); }, 'image/png');
こんな感じでblobに変換してダウンロードすることが可能なのでこちらを試してみます
しかし、、
Tainted canvases may not be exported
こんな感じのエラーが発生
キャンバスは別オリジンの画像を入れると汚染されてしまいtoBlobなどのメソッドが使えなくなるそう
crossOrigin="anonymous"
canvas内のimgタグに crossOrigin="anonymous"
をつけると別オリジン間でのダウンロードが可能になります
余計な画像のキャッシュ
crossOrigin="anonymous"
をつけたらダウンロードできるようになる場合とならない場合が
前提としてS3に画像を保存している場合、S3は Orgin ヘッダが含まれていないと Access-Control-
系のヘッダを返しません
crossOrigin属性を付ける前のimageタグではOriginヘッダを送信せずにオリジン許可が得られません その画像のキャッシュを保存している可能性があり怒られます
のでキャッシュを消してあげましょう
<img src="https://hoge.com/sample.png?cache=none" crossOrigin="anonymous" alt=""/>
のようにすればキャッシュの問題も解決できる
S3の場合はこれでキャッシュの削除ができたが他のサーバーではできないこともあったので他の方法を検討したほうが良さそう
Reactで画像ダウンロード機能を実装してみる
今回は2枚の画像を同時にダウンロードする機能を作成します
まずはカスタムフック
import { useRef, RefObject } from 'react'; const useEnhancer = () => { const canv = useRef<HTMLCanvasElement>(null); const canv2 = useRef<HTMLCanvasElement>(null); const img = useRef<HTMLImageElement>(null); const img2 = useRef<HTMLImageElement>(null); const downloadImage = ( canvas: RefObject<HTMLCanvasElement>, image: RefObject<HTMLImageElement>, id: string ) => { if (canvas.current !== null) { const currentCnavas = canvas.current; const ctx = currentCnavas.getContext('2d'); if (ctx && image.current !== null) { const currentImage = image.current; currentCnavas.width = currentImage.width; currentCnavas.height = currentImage.height; ctx.drawImage(currentImage, 0, 0, currentImage.width, currentImage.height); } const anchor: HTMLAnchorElement = document.createElement('a'); currentCnavas.toBlob((blob: any) => { if (anchor !== null && blob) { anchor.href = window.URL.createObjectURL(blob); anchor.download = `${id}.png`; anchor.click(); } }); } }; return { downloadImage, canv, canv2, img, img2, }; }; export default useEnhancer;
View
import React from 'react'; import useEnhancer from './enhance'; const DownloadImage = () => { const enhance = useEnhancer(); return ( <div> <button onClick={() => { enhance.downloadImage(enhance.canv, enhance.img, '1'); enhance.downloadImage(enhance.canv2, enhance.img2, '2'); }} type="button" > ダウンロード </button> <canvas ref={enhance.canv} style={{ display: 'none' }}> <img ref={enhance.img} src="https://cdn.qiita.com/assets/qiita-fb-fe28c64039d925349e620ba55091e078.png?cache=none" alt="" crossOrigin="anonymous" /> </canvas> <canvas ref={enhance.canv2} style={{ display: 'none' }}> <img ref={enhance.img2} src="https://cdn.qiita.com/assets/qiita-fb-2887e7b4aad86fd8c25cea84846f2236.png?cache=none" alt="" crossOrigin="anonymous" /> </canvas> </div> ); };
これにて一件落着
参考
ポート開放してたらDB破壊され身代金を請求されかけた話
CA Tech Dojo/Challenge/JOB Advent Calendar 2019 の5日目のエントリーです。
この記事はツッコミどころ満載です。どうぞご自由にツッコんでくださいませ。
大学院休学中の怠け野郎です。
11/7~11/29にAbemaTVのフロントエンドでCA Tech JOBに参加しました。
1ヶ月のインターン終わりました~!ほんと早すぎた…そしてたくさんアベマくんグッズをもらった😂未熟すぎて若干折れかけたけど最後まで楽しめました!#catechjob pic.twitter.com/10BLtAainu
— もーりー⛅ (@_mooriii) 2019年11月29日
インターンでは新規ページの仕様策定〜実装までやらせてもらいました。
ディレクターの方やデザイナーの方と連携しながら仕事をさせてもらいとてもいい経験になりました。
今日は油断してたら自作webサービスのDBが破壊され身代金を請求されかけた話をします。
概要
レンタルVPSのDBポートを意図的に開放&外部接続可能にした結果、0.2ビットコインを請求されかけました。
事の発端
僕はYYさんという実況者がめちゃめちゃ好きで毎日見ています。
YYさんの好きなシーンはたくさんあって時々「あのシーンみたいなぁ」ってことがあるのですが、どの動画のどのシーンかわからなくて探すのに時間をかけたり、結局見つからなかったりしてました。
そこで、「みんなの好きなシーン集めちゃえばいいんじゃね?」と思い立ち、YYさんの好きなシーンを投稿するSNSを作成しました。
サイト紹介
システム概要
バックエンドはRails、フロントはReact×Ts、認証にはFirebase Authenticationを使用しています。YYさん以外の動画を弾くためにバックエンドでYoutube Data APIも使用しています。サーバーはさくらのVPSです。
認証はこんな感じ。
機能紹介
ホーム画面にある+ボタンを押すと投稿画面が表示されます。投稿したい動画のURLをコピペして、好きなシーンの開始地点と終了地点でボタンを押して投稿します。
投稿するとホーム画面に追加され、好きなシーンの開始地点から再生が始まり終了地点まで行くと、開始地点に戻り再び再生が始まります。
簡単な検索も可能になってます。
本題
サイトの話はこの辺にしておいて、本題に入ります。
「本番DBが見たい」
これが悪夢の始まりでした。
僕はSQLのGUIクライアントにSequel Proを使っています。
Sequel Proから本番環境に接続するためには外部接続の許可&ポート開放が必要なので、「まあ接続確認だけだし、そんな簡単にハックされることはないだろ。すぐ戻すから大丈夫」と思い普通に
mysql> GRANT ALL PRIVILEGES ON *.* TO root@'%' IDENTIFIED BY 'root' WITH GRANT OPTION; *1
*1: コマンドはイメージです
自分のPCはあらゆるWiFiに接続するのでIP指定することもできず仕方なく全開放しました。
無事Sequel Proから本番DBが見えるようになりました!
数日後、、
「あれ、APIからレスポンスが返ってこない」
「どうせnginxの不調やらRailsサーバーが落ちたとかそういう類でしょ。」と思いながらSequel Proに接続。
すると、、、
もともとあったデータベースが削除されWARNINGというデータベースが作成されていました。
その中にはPLEASE_READという謎のテーブルがただ存在しているだけでした。
「怖」
もうその一言に尽きます。
速攻で検索。(いや、先に権限直せ)
するとこんな記事が。
要約すると、
「お前のDBを乗っ取った。DBのダンプがほしけりゃ金を払え。」
というランサムウェア攻撃。
幸いサイトは開発段階でDBはほぼ空っぽだったので被害はなし。危な。
なんとか事なきを得ました。
まとめ
自作サイトのDBのパスワードをrootのまま外部接続を許可したらデータを消され乗っ取られました。
その後は、rootのパスワードを変え、rootの外部接続を切り、専用のユーザーを作成し権限を与えて解決しました。
今では投稿数は200を超え、ユーザー数は50人を超えるまで増えてくれました。
以上セキュリティは大事だよ。というお話でした。
黙ってポートフォワーディングしとけってことです。
ではまた。
CircleCIでS3にデプロイしようとしたらSyntaxError: invalid syntaxで失敗した
いつもlintでエラー出てたので、どうせlintだろと思ってログを見たら
# aws --version Traceback (most recent call last): File "/bin/aws", line 19, in <module> import awscli.clidriver File "/usr/lib/python2.7/site-packages/awscli/clidriver.py", line 36, in <module> from awscli.help import ProviderHelpCommand File "/usr/lib/python2.7/site-packages/awscli/help.py", line 20, in <module> from docutils.core import publish_string File "/usr/lib/python2.7/site-packages/docutils/core.py", line 246 print('\n::: Runtime settings:', file=self._stderr) ^ SyntaxError: invalid syntax
??!
なんだと思って調べてみたら、こんなissueを発見。
pythonのdocutilのバージョンが0.15に上がったことによる不具合らしい
configのaws cliのインストール後に pip install docutils==0.14
をすると治るとか
pip install awscli --user pip install docutils==0.14
successした。いぇい。
redux-actionsをtypescriptで使いたい人へ
Reactの0→1案件でReduxを導入したんですが Action書くのだるいなぁと思ってたらこんなものを見つけました
これを使うとactionとreducerが完結にかけるらしい
// Actions export const isLoading = (): LoginActionType => ({ type: LOGIN, payload: null, }); const loginSuccess = (result: any): LoginActionType => ({ type: LOGIN_SUCCESS, payload: result, }); const loginFail = (err: any): LoginActionType => ({ type: LOGIN_FAIL, payload: err, }); // Reducer const login = (state = initialState, action: LoginActionType): LoginState => { switch (action.type) { case LOGIN: return { ...state, loading: true, }; case LOGIN_SUCCESS: return { ...state, data: action.payload, loading: false, status: 'success', }; case LOGIN_FAIL: return { ...state, data: action.payload, loading: false, status: 'fail', }; default: return state; } };
例えば、こんなactionとreducerが(interfaceとか色々割愛してます)
// Actions export const loadingAction = createAction(LOGIN); const loginSuccessAction = createAction(LOGIN_SUCCESS, (res: any) => res); const loginFailAction = createAction(LOGIN_FAIL, (err: any) => err); // // Reducer const login = handleActions({ [loadingAction.toString()]: state => ({ ...state, loading: true, }), [loginSuccessAction.toString()]: (state, action) => ({ ...state, data: action.payload, loading: false, status: 'success', }), [loginFailAction.toString()]: (state, action) => ({ ...state, data: action.payload, loading: false, status: 'fail', }), }, initialState);
こんな完結にかけちゃいます
ただ若干ts対応が弱い…
[action.toString()]
にしないとエラーが出ます
(もしくはactionの返り型をanyにする)
色々調べていると
このissueのコメント欄にこんなものがありました
Hello all.
I don't think the pattern of redux-actions can give a full type inferring capability. at least not yet. Also, there is no official support for typescript in redux-actions. All of these makes using redux-actions in typescript > hard and misleading.
That's why I have just published thebrodmann/deox as a good alternative to redux-actions for typescript use cases with full power of type inferring without any type information losing. I hope it makes sense.
「deox
がtypescript使いのredux-actions
の代わりになる」と…
半信半疑で使ってみました
// Actions export const isLoading = createActionCreator(LOGIN); const loginSuccess = createActionCreator(LOGIN_SUCCESS, resolve => (res: object) => resolve(res)); const loginFail = createActionCreator(LOGIN_FAIL, resolve => (err: string) => resolve(err)); // Reducer const login = createReducer(initialState, handleAction => [ handleAction(isLoading, state => ({ ...state, loading: true, })), handleAction(loginSuccess, (state, action) => ({ ...state, data: action.payload, loading: false, status: 'success', })), handleAction(loginFail, (state, action) => ({ ...state, data: action.payload, loading: false, status: 'fail', })), ]);
記述量はそんな変わりませんね あと変なエラーも出なくなりました
The only completely functional type-safe approach to Flux that its main goals are to diminish types verbosity, repetition and complexity without losing any type information (type-safe alternative of redux-actions).
型安全な redux-actions
だよ〜って感じですかね
star数こそ劣っているものの、最終更新はdeoxのほうが新しく最近もサポートされているみたいです
- star
- redux-actions : 6114
- deox : 83
- last commit
- redux-actions : 4 months ago
- deox : 4 days ago
割と問題なく使えたのでtsでredux-actions導入しようと思ってる人は使ってみてはいかがでしょう
xip拡張子とは?
iPadのsidecarを使いたいがためにMacをcatalinaにアップデートしたらxcodeの11betaがインストールできるみたいでインストールしようとしたらXcode_11_Beta_2.xip
という形式でダウンロードされていました。
xip
ってなんだ?と思ったので調べてみました。
man xip
で見てみると
DESCRIPTION The xip tool is used to create a digitally signed archive. As of macOS Sierra, only archives that are signed by Apple are trusted, and the for- mat is deprecated for third party use.
アーカイブが展開される前にデジタル署名が適用されて検証されるみたいですね。
zipのデジタル署名版ってところでしょうかね?
Type-C一本で電源供給と出力してくれるモニターがやばすぎた
ついに、今日念願のモニターが届きました!
購入したモニターはこちら
|
最近主流になっているUSB-Cタイプのモニターです
僕はMacbookを使っているのですが
今までは電源供給にACアダプター、出力にHDMI、HHKBにUSBを使っていて
これが必須でした。。
研究室に持っていくのに電源用のACアダプターをいちいち取り外し、持っていかなきゃいけませんでした。
そんな中、このモニターに出会いました。
type-c一本で電源供給とモニター出力をしてしまうのです!
しかもHHKBが直接モニターにさせる!!
最高かよ!!!
いままでは↓これでした
でも今は
これだけです。
最高にスッキリしました(^^)