u-ryo's blog

various information for coding...

Author: U-ryo

Cut Pdf and Convert to Png

| Comments

「pdf fileの10ページ目を切り取ってpngにconvertする」やり方。 pdftk10ページ目をcatして標準出力に渡し、それをpipeで繋げてpdftoppmで標準入力から読み込みredirectして-pngで変換してfileへ。-pngがないと.ppmになるので注意。

1
pdftk nenga2021.pdf cat 10 output -|pdftoppm -png - > nenga2021.png

Learned on Ruby and Vue, Angularjs

| Comments

お仕事でRuby on RailsとVueをやっております。 これまではずっとJavaやAngularな人生だったので、Duck Typingと格闘中です。 新規機能開発で様々な山を超えてきた(知見を得た)ので、忘れないうちに。

  • 破壊的method(Bang Method)って、その響きから何かあんまり良くないのかと無意識に思っていたのですけれども、調べてみると特に忌避すべきというものは見つかりませんでした。内部logicを考えると中で変数を付け替えていると思われますが、bangしないと両方分memoryに持ってなきゃならないから?→そうしたらrubocopに、result[:key]=... if ...と書けと言われました。ナルホドです!
  • 手元ではyarn test:vue通るのにCircleCIでは7 failsと言われて通らないので悩みました。どう目を凝らしてもerrorらしき出力はなく。よく見るとCircleCIの最後にToo long with no output (exceeded 10m0s): context deadline exceededとあり、かかった時間が10:57とか。えー!?そんなにかかっているの?? 手元でtimeで計測するも、1分ちょっと。CircleCI再実行して観察すると、test自体は1分程で終わり、その後ずっとだんまりで、10分程してTimeoutします。何だろう? 手元では再現しないのでとにかく厄介です。考えてみたら、チェックじゃなくて「1)」とか「2)」になっているところが失敗したところなんですね。確かに7)までありますわ。でも、そこがどう間違っているのかが全然出力されないので皆目わかりません。一箇所、describe('...', async () => {ってなっていたから、これか! と消して勇躍試してみたものの、症状変わらず。mountshallowにしてみたり削れるだけのasync/awaitを削っても。何故かdescribeの囲みを解くとtestが失敗します。期待してるobjectのpropsがないと。でももう一つ別のpropsはある。もぅわけわかんない!ですよ。CircleCIにsshで入ったりして色々と調べてみると、特定の2 filesが先にあると失敗する様子。何それ。どちらかがない、もしくはどちらかより先に実行されれば上手く行く。もっと詳しく見てみると、具体的には、shallowした直後のwrapper.html()の中身が全然違うという。どうして? そんなとこどうにもなんないじゃん。...あぁ、ぃゃ、test対象Vue Classできちんとcomponentを定義すると(e.g.components: { 'el-button': Button, 'el-dialog': Dialog })warningが消えてcomponentとして見えるようになることがありますが(特にElementUIはel-...とclass nameが合わないので...(- -;)、そういうわけでもなく。何せ2 filesが組み合わさるとtestにコケるというのが謎です。試しにダメにするspec fileを一つ消してから全体的にnpm run test:vueしてみると、またコケるので、その2 filesはat leastであってもっと他にも組み合わせの悪いのがあるということです。i18nかなぁ? 確かに一方はenで他方はjaでしたが、大して使ってないのでcomment outしても変わらないですし... localでは全体的に通しても通っている、CircleCI上でも単体や相性の悪いものの後でなければ動く、でも全体的に通すとtestに失敗する、というのはとても悔しく、厄介です。結局心折れて、componentが見つからなければtest codeをthroughするようにしました。端からdescribe.skip(...よりはマシでしょう。それにしても何でやねん。大したtestしてるわけでもないclasses(methodがcallされたかどうかをassertしてる程度)に阻まれて、ちゃんと作ったspecのtest(条件がどうならどういう表示状態、input要素をclickしたらどういう文言のdialogが出て、等)を実行できないというのは何とも残念でなりません。
  • Vueのtest、ムズカシイです。child componentの扱い、ですね。何気なく使っているであろうel-tableとかel-dialog、そのままだとそんなcomponent知らないよとwarningが出まくります([Vue warn]: Unknown custom element: <el-table-column> - did you register the component correctly? For recursive components, make sure to provide the "name" option.)。wrapperからはHTML Tagとして扱えるので最後はそうするのですけど、出来ればちゃんとcomponentとして扱いたいところです(でないとtable等でdataをloopさせて表示を作るcodeだった時のdata部分がそっくりないのでtest出来ない)。spec classでshallow(ormount)する時にstubs:するのかと思ってた(前それで頑張った気がした)んですけどそうではなく、spec対象元classでちゃんと定義されていれば?、否、ElementUIなんかは丸っと読み込まれているのでいいのかなぁ?ともあれ、spec classでlocalVue.use(Table)としたlocalVueshallow(ormount)時に読み込んでやる必要がありました。でそのcomponentをfindして.props()して中身をexpectしていくという流れですね。最初はconsole.log(wrapper.html())で何が捕れるのかよく観察すると。でも上記のように、何かが変わるとすぐ取れるcomponentが変わるようなので、怖いです。その辺りの機微がよくわからないので...
  • Vue testにおけるtickの待ち方。it(..., async () => { ... await Vue.nextTick(); ...}); 2 ticks待たないと値が変わらないことも。
  • wrapper.findAll('.cell')で取って来た値はElement Objectなので、見た目上は空でもexpect(cell).to.be.emptyとは出来ません。expect(cell.isEmpty()).to.be.true
  • Vue componentって、初期値propsdataは分けねばならない? 当初、dataに初期値を渡そうとしてどうしても出来ず、途方に暮れました。dataの初期値はdata部に書いた最初に返す値でそれは何でもよく、tagの方で渡す値で上書きされるのかと思ったら違うんですね。ではdataではなくpropsを使うのかと思うとそうでもなく、propsを後から変更しようとすると怒られますし。Stackoverflowにあった解決策は、初期値propsdataを使い分けること。無駄な変数増えて何だかなぁとは思いますれど、そういうもの?
  • slot-scopeって、="scope"ではなく={row}とやればいいんですね。本で知りました。
  • ...mapGettersがなかなか効かなくて苦労しました。computed:に入れる、storeのmodule構造に合わせてmodule(file)名/method名にする、とやったんですが、undefinedって言われて。store経由でthis.store.value_nameとかってやると取れるのに。でもState を直接参照せず、getter 経由で参照することを強く推奨するというので頑張りました。一回は諦めたんですけど、その後retryしたらうまくいくように。結局どうしてうまくようになったのか、決め手が何だったのかよく分からずじまいです。
  • 確認dialogを出すというんですが、非同期のJavascript/Vueにあってはこれが意外とムズカシイです。確認dialogというからには処理を止めなければなりません。よくやるように「flagで表示を制御」ではmain処理止まらないんですよね。そもそもsleepだとて簡単には行かないですよねJavascriptで。とても苦労しました。async/awaitを導入してやっと止めました。要は「dialogを作っているところをnew Promiseでくるんでreturn、それをawait、しているfunctionをasync」ですかね。
  • dialogって自分で$destory()出来るんですね。生成と削除は外からやらないとダメかな、と思っていた(ので$emitして親で受けてdialog.$destory()してた)んですけど、実際生成は外から$mount()呼ばないとならないですけど(created:mounted:はcallbackですよね)、「ボタン押したら消滅」で自己$destroy()は出来るんですねやってみたら。いちいち面倒くさいけどやってみないとわからんもんですね。
  • dialogを出す必要に迫られたんですけど、よくあるようにdialogを予めDOMに仕込んでflagでappear/disposeとするには、2階層上のVue componentに書かないと、loopで大量生産されてしまうことが判明。Vue component間は親との対話だったら$emitと、戻しは何だ??を使えば出来ますが、2階層上は面倒です。情報渡すだけならまだしも、その後の処理を孫でやるならdialogの結果を2階層戻してもらわないとならないですし、じゃぁ処理もgrandparentに渡す? 処理に必要な情報も渡さないとだし、grandparentで知らなくてもいいことを知らなきゃいけないなんて。ということで、何かないかなーと探すと、揮発性dialogというものが(揮発性の高いコンポーネントを作る話)。探すと似たようなことしてる人割と多く。これを理解して、そのままではなく2 filesで済むようにして自分のにapplyするのも頭を使いました。これ結構いいような。Vueのdialogも一般的にこれでいいような。初からDOMについているのではなくって。
  • 既述のように、awaitもうまく行ってよし、じゃぁtestを書くぞ、と試したら、いきなりわけのわからないerrorに見舞われてそもそもtestが動かなくなってしまいました。なんでだろー?! 自分以前では確かに動いていたので、自分のせいです。でもどのfileが悪いとかも出て来ないのでまるで取っ掛かりがなく、途方に暮れました。error messageで検索すると、どうやらなんとasync/awaitを使った場合に出るbable/polyfill系のerrorだとか。えー!! 折角苦労してasync/awaitでdialog止めたのに! 代替手段なんて思い付かないよー! ということで、何とか突破すべく頑張りました。babelの線で色々と調べてみると、こうしたら直った、ああしたら、とか種々書いてあるんですね。新たなgem install以外は全部試しましたけど、うまく行かず。でもこの記事を参考にvueのsetup.js冒頭にrequire("babel-polifill");と書いたら!! 動いてくれました!! 省みれば僅か1行の追加ですけど、この1行には数時間もの汗と涙が詰まっているのです。
  • APIの試験をするのに、curlやPostmanを使うのはよくありますけど、cookieが何か長くてcopyが面倒だなーと思ったので、Chrome Developer toolのConsoleにてfetchでやる手法を。fetch(`/apis/endpoint/${id}`,{method:method,credentials:'include',headers:{'X-CSRF-Token':token}}).then((response)=>console.log(response))とすればBrowserの持っている認証情報をそのまま使ってくれます。credentials:'include'が肝です。どうせ使い捨てcommandですから。
  • Railsは自動的にCSRF対策がなされている、ということで、何してるんだろーと思いきや、session毎に予測困難なX-CSRF-Tokenが付くというものでした。request毎に違うものでなくてもいいのか? いいのか...予測困難なら...
  • Railsの日付、Timezone気を付けないといけないのですけど(AWSはUTCなので)、Time.currentDate.currentのように.current methodを使えばTimezoneを意識してくれる模様。.nowより.currentがということでやはり「ナウい」は死語ということですか。
  • Ruby、共通処理は継承関係作って親クラスに、と思っていたんですけどそれってJava的発想? OOP的というか。RubyではむしろMixin? MixinってAOPかと思ってたんですけど。Duck Typingだからそもそもclass的には無関係でいいんですね。Java屋さんとしては何だかなぁと。
  • Rubyのstream(mapselectrejecteach等)、もうfoolproofとしてlazy...forceを付けるのがいいんじゃないか、と思ったんですけど、そうでもないんですねこれ!! ってかそもそもどうしてlazyがdefaultじゃないんだろう? ってJava屋さんとしては思ってたんですけど、銀の弾丸ではないという話があって衝撃でした何を今更ですけど(古い話のようで今は多少performance上がったそう)。というわけで、lazyは「省メモリ」か「後でかなりの数takeすることがわかっている場合」でないと効果なさそうなんですね。元ネタ
  • 最近何か安易に大量のlogをslackに投げていたので、そうじゃないでしょ基本的にはlog fileでしょ、それもdefaultのfileでいいよね、ということで、Logger.new(...)ってやったんですけどどうして毎回file nameを指定しなきゃいけないのか、しかもfile名指定してnewすると都度file作りやがる、のでappend modeでopenして、とかってやらないとならないの?! なんか面倒臭くね? と思ってたら、Rails.loggerでいいんですね。なぁんだ。@logger ||= Rails.logger
  • controllerで最終的?に全てのerrorを拾うにはrescue_from error_class名, with: :method名、事前validationを定義するにはbefore_action :method名
  • 決まった名前をclass methodにするか定数にするか、は、定数の方が良いようです。ref. Class method vs constant in Ruby/Rails
  • Rubyで「objectの同一性」とは、Javaと違って==eql?hashの3 methodあるんですね。うち==はArrayで、残る2つはSetで使われる模様。Arrayでは比較通るのにSetでは違うと判定され悩みました。Setではobject hashを取っているからなんですね。でも結局、.to_jsonすれば同一性methods定義しなくても行けました。何だかなぁですけどまぁunit testなのでいっかー。
  • すっかり忘れてましたけどroutesresourcesを使えばendpoint定義をまとめられますね。path parameterも積極的に使うべきでしょう(その存在を強制できるので)。parameter nameは基本idですけど変えられるんですね。
  • ActiveRecordで、MySQLみたいに「あれば取得、なければ作る(recordも入れる)」なんてあるかなーと思ったら.find_or_create_byがありました。更にforeign key制約をつけて、その制約に反したらerrorにして欲しい場合は、.find_or_create_by!でした。
  • RubyでVO(Value Object)を作るのは、どうしたらいいんですかね。object = Struct.new(:variables, ...)とobjectとして使えば良い、という記述があったので、今度試してみます。←ここでいうobjectってclasなんですね。
  • RubyにもJavadocのようなcomment流儀あるんですね。YARDですか。@param [引数のクラス] 引数名 引数の説明@return [戻り値のクラス] 戻り値の説明
  • 配列からhash mapを作る手法、色々ありますよね。よくある「[key, value]の配列の配列にしてから.to_set」はloopを2回回すので何だかなぁと思っていました。Rubyにもあるreduce(inject)を使えば出来る!とあったのでやったのですけど、そうしたらrubocopに「そういう時はeach_with_objectを使え」と言われました。ナルホド、それなら確かにblockの最後にhashをreturnせずともよいわけですね! というわけで、list.each_with_object({}){|value,hash|hash[key]=value}、更に一気にhashのhashを作るには、list.each_with_object(Hash.new{|hash,key|hash[key]={}){|value,hash|hash[key1][key2]=value}
  • 同様に、一気にSetを作るには、list.each_with_object(Set.new){|value,set|set<<value}の方が、最後に.to_setよりよいでしょう。
  • reekに「log.debugを複数回使っているからまとめよ」と言われて弱りました。単純にまとめると、毎回引数を評価しちゃうじゃないですか。でもloggingなので遅延評価が必要でしょう。というわけで、blockを残せるようにmethod定義しました。def debug(&block) Rails.logger.debug(&block); end使う時はdebug{...}です。
  • rescue=> eとせずとも$!でerrorを参照できるんですね。rescueretryとすればbeginからまたやってくれるんですね! ただ、上限設けないとずっとretryし続けるので注意ですかね。
  • Railsでmail送信は、configが終わっているのであとは、ApplicationMailerを継承したclassを作成、app/views/class名/method名.html.hamlといったtemplateを用意、という感じでした。html mailだけだとspam判定されやすいとのことなので、app/views/class名/method名.txt.erbも用意しました。classのinstance fieldをmail template中で参照できます。
  • ActiveRecordでの取得はなるべく.pluckにした方が速いし軽い、ということでそうしてますが、でもそうすると何のためのDTOなのかと。model定義の意義が少し薄れるような。modelがfatなのが悪い? .pluckでない場合はせめて.selectで選択するfieldを減らしてやって生成されるobjectを少しでも軽くすると。ただActiveRecordからObjectを生成するcostが高いとのことなので、.pluckの方が推奨されています。....selectって、modelにないfieldもselectできるんですね。そしてdynamicにmethodが生えると。なんかそこまで行くと気持ち悪い気がするのは、やはりJava出身だから??
  • yieldはよく使いました。if block_given?も併せて。
  • instanceからclass variableへの参照はinstance.class::VARIABLEでした。
  • rspecでallow(Rails).to receive(:logger).and_return(spy('logger'))とするとloggingが一律空回しになります。
  • method chainをallowするにはreceive_message_chain(e.g.expect(SomeClass).to receive_message_chain(:joins, :distinct, :where, :pluck)) .with(...)を複数書いても、それぞれのmethodの引数指定ではなく、最後の一つしか評価されていない?
  • rspecで、違ったparameterで同じobjectのmethodをcallするのをstubするには、allow(...).to receive(:same_method).with(...)を並列で何度も書けばよいです。with(...)をmethod chainさせるとかではないんですね。
  • rspecでinstance doubleに引数を連ねるとmethod stubになります。e.g. double('user', id: 1)user.id==1(doubleの最初の引数は任意名)
  • 調べればすぐ出てくることではありますが、Railsのmigrationのcreate_table時にprimary keyを自動で出来るid以外にしたい場合はcreate_tableの行にprimary_key: key名と書きます。更にforeign key constraintを付け加えたい場合は、その下にadd_foreign_key :table名, :foreign_table名と書けば大丈夫でした。
  • RubyでTime format時、.strftime('%X')とすると一文字で15:37:21に、.strftime('%F')とすると2020-03-01になるので便利です。また、.iso8601とするとTを挟んでTimezone付きで2020-03-01T15:39:10+09:00というようにしてくれるのでこれも便利です。
  • 嫁の顔忘れてもTimecop.returnは忘れないでねという話にあるように、Timecop.travel(Time.parse('2020/3/1 0:01:03')) do ... endで囲うのはいいですね。Timecop.travel(特定時刻にしてから時を刻む)と.freeze(特定時刻にして時を止める)では違うので注意です。
  • Rubyで文字列の連結は<<いいみたいですね。C++みたいですね。
  • ActiveRecordでN+1問題を回避するには、なるべくjoinsした方が良いようです。が、図に乗って巨大tableをjoinするとそれはcostがでかいので、分割して引いた方がいいんでしょう。joinsするにはmodelにhas_manyとかの関係が書いてないとダメでした。2つ先の関連も書けて、has_many :second_table, through: :first_tablethrough:で繋げられました。
  • と思ったのは幻想でした。書けちゃいますけど、joinされたものはkeyで結びついたものではなく、雑に合わさったもので、whereで引いてもそうでないものまでついてきていて、bugを引き起こしていました。ちゃんとやらないとダメですね。
  • 「ちゃんとやる」とは、Railsでモデルを4段階joinする方法で、もう一度理解するjoinsとmergeにあるように、ActiveRecordのjoinsで繋ぐ時にjoins(first:[second: :third])とすること。これでちゃんとjoinされました。
  • controllerで、APIっぽくheaderだけ返して中身は不要という場合、head :created(201)とかhead :unauthorized(401)と書けます。
  • Rubyのhere documentはもう<<~でよくないすか? <<だと左端にひっつくので。
  • Rubyのreduceeach_with_objectに取って代わられるのですが、reduce(&:method)で済むところに活路があるようです。使えたのはSetのmergeですね。[Set.new([1,2,3]),Set.new([2,3,4])].reduce(&:merge)==Set.new([1,2,3,4])
  • 複数回.include?するなら絶対Setにすべきですよ。
  • 定数文字列もなるべく.freezeすべきでしょうか。magic comment # frozen_string_literal: trueで(が?)よいのでしょうか。
  • controllerのrspecで、before_actionのtestを書くことになったのですが、色々と悩みました。どこに書くか? 書いたclass? 使うclass? Javaでabstract classのtestだとconcrete classに書きますが、controllerは使う側に書くと多岐にわたるので、application_controller_spec.rbに書きました。
  • curlでtest clientとしてJSONを-dでPOSTしたところ、どうもうまく行かなくて、調べるとserver側に"{\"key\":\"value\"}"なんて渡っていました。どうしてー?! HTTP Request HeaderにContetnt-Type: application/jsonが必要なんですね! そういえば。
  • 今更ですがAngularJSのtestで、window.location.hrefをassertionしようとして四苦八苦しました。How to mock $window.location.replace in AngularJS unit test?等から得たことは、window.location.hrefだとtest(jasmin)で拾えないので$window.location.hrefにする、responseがemptyだとどうしてもtestの方でcatchしてくれないのでdummy値をresponse代入する、testでは$windowがemptyで悩みました。$provide.value('$window',でprovideしてbeforeEach(inject(function($injector, $rootScope, $httpBackend, $window) {でinjectしつつwindow=$window;と付け替えて、assertionのところでwindowを使う、という。
  • diffで相違は出ますが、共通行を抽出するには? comm -1 -2 file1 file2(但しfilesはsort済であること) ref.2つのファイルの共通行を抽出する方法
  • MySQLでindexがついたかどうかを確認するには、describe table名ではMulと付くだけでよくわからず。show create table table名でCREATE文を出すことで。
  • controllerのrspecで、sessionってどう表すのかと。get :index,params:{aaa:'AA'}だからget :index,params{...},session:{...}かと思いきや、実際そう書いてあるところもあるんですが、それではうまく行かず、get :index,{param:'something'},{session:'something'}と。ref. RSpec set session object
  • rspecでprivate variableにsetするには、some_instance.instance_variable_set('variable_name', 'some value')、getするには`someinstance.instancevariableget('variablename')
  • visitor patternを適用しようと思っていたのですけど、諦めました。RubyってDuck Typingだからあんまり効果を見出だせず。Ruby で Visitor パターンをあまり使わない理由,Ruby で Visitor パターンを使うときという投稿もあり。

  • upstart で start した job の設定を変更するには一度 stop する必要がある へーそうなんだ! ってぃぅか何のためのrestartっすか? restartあるんだから機能すると思いますよねぇ...orz

  • 1.hour.agoがあるのに.later.afterはないのでどうするんだろう? と思ったら、1.hour.from_nowっていうんですね。

  • controllerのrspec書いていて、やっと単体で通るようになったので、いざ全体的にrspecかけてみると、コケます。確かに自分の書き足したところ。でも通る時もある。どうして?→そういう、rspecの結果が不安定な時は、実行順序依存性があります。これを紐解かねばなりません。それが厄介でした。といいますか、rspecって全体的にかけると、といいますかspec fileを指定しないでdirectoryだけ指定して実行すると、spec filesの実行順序がrandomなんですね。それはその方がいいですね。なので、色んな手を使って順序依存性を解明していきます。ref. RSpecコマンドのオプションまとめ rspecに--order random:NNNNNを付けてrandom seedを固定、-f dとして出力formatにclass名を出すようにし、--no-colorでescape sequenceを抑制します。-o filename.logでfileに出力、それをうまく行ったときと失敗した時で保存して、class名を比較しました。当該classのtest以前だけが大事なので、そこだけ出してsortしてdiff取ります。失敗事例が複数取れれば、それらの共通classをcomm -1 -2 <(...) <(...)で抽出するのが良いです。

    1
    2
    3
    4
    
    docker-compose exec rails bash -c 'RAILS_ENV=test bundle exec rspec --order random:32510 -f d -o 32510.log --no-color'
    diff <(grep -e '^[A-Za-z]' -e '^  Apis::' controllers2.log|awk '{if(/ApplicationController/){exit}print}'|sort) <(grep -e '^[A-Za-z]' -e '^  Apis::' 32510.log|awk '{if(/ApplicationController/){exit}print}'|sort)|ag '<'
    comm -1 -2 <(grep -e '^[A-Za-z]' -e '^  Apis::' controllers2.log|awk '{if(/ApplicationController/){exit}print}'|sort) <(grep -e '^[A-Za-z]' -e '^  Apis::' 4627.log|awk '{if(/ApplicationController/){exit}print}'|sort)
    comm -1 -2 <(grep -e '^[A-Za-z]' -e '^  Apis::' controllers2.log|awk '{if(/ApplicationController/){exit}print}'|sort) <(grep -e '^[A-Za-z]' -e '^  Apis::' 48327.log|awk '{if(/ApplicationController/){exit}print}'|sort)|grep -r -l -f /dev/stdin spec/controllers|sort -u|tr -d '\r'
    

  • 単体では通るのに全体だと時々こけるというのはどうしてもよくわからないので、何度も何度も実行して観察してみると、コケる時のFの出る位置がまちまちなんですね。あぁ、これって毎回test順序違うのか、とその時初めて思い至りました。それで、rspecにおけるrandom seedの固定方法や出力形式の指定の仕方を調べ、何とか実行順を成功時と失敗時で比較して、怪しいものを試して、辿り着いたのでした。

  • controllerのtestでは、before_actionに指定したprivate methodのtest且つ利用するchild classでのtestではなくbase classでのtestなので、当初はcontorller.send('method_name')とかやってたんですけどこれは本来的ではないな、ということで、Anonymous Controller使いました。といってもdescribeの中でcontroller do ... endして、そこでbefore_action :method_nameしただけのものです。それでそのdescribe内のcontrollerは定義したものになるという、お手軽な。でもここでエラー(Errorをthrow、じゃないRubyだからraiseですか、する筈が正常に終了してしまう)が出て、悩みました。結局、わかってみればどうということはないのですが、他のspec classで、ApplicationController.skip_before_filter :login_requiredbefore do...でやっていて、でもafter do...で元に戻していなかったから、というのが原因でした。

  • そもそも、ApplicationController.skip_before_filterなんてせずに、単にallow(controller).to receive(:method)とmockすれば十分です。そうしたらafter do...だって不要ですし。ref. before_filterを分離してテストする方法(Rspec) 別段true返す必要もないんですねこのbefore_actionって。before_filterは古いんだそう。

  • いつもscheduleの確認をするのに。bundle exec whenever

  • AngularJSでのredirect? になるのかな? は、routesの$stateProvider.state('...')に定義されたところへ、$state.go('...')で飛びます。ref. Angularjs UI Routerの使い方 でも結局、AngularJSのrouting範囲外に飛ばしたかったので、$window.location.href = ...で飛ばしました。

  • ActiveRecordのscopeに引数を取る場合には、scope scope_name ->(arg1, arg2) { ... } 矢印とカッコはくっつけないとreekに怒られます。

  • RubyでStringselectを正規表現でするならgrep(/.../)-vの場合はgrep_v(Ruby equivalent to grep -v)

  • AngularJSのroutingってURLについたanchor(#以下)に出るのですが、それってそもそもbrowserがserver側に送信しないんですね...

  • ActiveRecordで選択して保存を1行で。例えばUser.find(1).update(name:'new name')

  • rspecでActiveRecordのtest dataってfixtureやFactoryGirlを使うものかと思っていたのですけど、普通に.createとか.updateとかして大丈夫なんですね。大丈夫、というのは、後腐れがない、んですね。

  • RSpecでallowでmockした時に、.and_returnだけでなく処理(sleep(1))を入れたかったんですけどどうしたら? と思ったら、単にblockでいいんですね。blockの最終評価値がreturnになる、というなら.and_returnって不要なの?

Add Swap File

| Comments

年末にideapad 310のmemoryが壊れてしまい、2度買い直してもmemoryを認識しなかったので、これは本体側が壊れたのだろうと。これを直すには本体のmother board交換になる、くらいならまだ2年程しか使ってませんが新しいのを買う? と考え、物色していたところ、新しいものでもmemoryって搭載容量4GBとか程度で、何故? と思ったんですけど、今はSSDだからそれでいいってことなんですね! そっか、今はSSDが廉価になったからswapでいいのか! なら310もSSDに換装すれば、ということで、SSD 1TBにしました。1.11万円でSamsungの1TBをGET。ubuntu 19.10を入れて... 最初、折角だからZFSにしてみたのですが、SSDと相性悪いんですねこれ。swap partitionは、位置が固定されるためSSDには良くないと。XFSもSSDではjournalingを止めた方が良いとのことで、それだと何のためのXFSなの?! ということなのでつまらないですけどext4で再installします。しかし、partitioningをお任せにすると、今はdefaultでswap partitionではなく2GBのswap fileになるというのは驚きですね。 ということで、本題です。よく、ddでswap fileを作る例が載っていますけれども、スワップ領域の追加方法|server-memo.netにはfallocateを使う方法があったので、メモです。

1
2
3
4
5
6
sudo fallocate -l 2G /swapfile1
sudo mkswap /swapfile1
Setting up swapspace version 1, size = 2097148 KiB
no label, UUID=89a19a54-42c9-4847-a482-1971c388fa95
sudo chmod 600 /swapfile1
sudo swapon /swapfile1

Google Photo Api

| Comments

一日千枚とか写真撮る人だと写真がすぐ溜まっちゃうんですよね。 backupは無限のGoogle Photosに、ということで、前はPicasaのAPI、upload_gphotsを使ってたんですけど、もう無くなっちゃっていて。どうしよう、途方に暮れていました。暫くぶりに探すと、丁度1年程前からGoogle Photo APIが整備されたようで、良かったです。ずっと待っていました。 [追記あり] Google Photos APIsでアルバム作成と写真のアップロードGoogle Photoを業務システムのクラウドストレージとして使った結果本家API Documentを参考に早速使ってみます。

ACCESS_TOKENの取得

  • APIの有効化
  • Google Developer Consoleから「認証情報」→「OAuth2.0クライアントID」無ければ上の「認証情報を作成」pulldown menuから「OAuthクライアントID」(「ウェブアプリケーションの種類」は「その他」)で作成
  • 上記「クライアントID」「クライアント シークレット」をメモ
  • 次のURLに$CLIENT_IDを入れてbrowserでaccess、AUTHORIZATION_CODEを取得 (https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=$CLIENT_ID&redirect_uri=urn:ietf:wg:oauth:2.0:oob&scope=https://www.googleapis.com/auth/photoslibrary&access_type=offline (SCOPEはGoogle PhotoでのR/W accessの場合はhttps://www.googleapis.com/auth/photoslibrary)
  • 以下のようにして、ACCESS_TOKEN及びREFRESH_TOKENを得る
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
    $ AUTHORIZATION_CODE=4/wnmGpTh__1zdrgdjmPWyetUI7C1mvsjRrA_IyZmwY7aSeYppD9X_9iB
    $ CLIENT_ID=952391557281-s8b8ditnocfu590fi0ntsfk76rbmkm80.apps.googleusercontent.com
    $ CLIENT_SECRET=k6XPLuryMWUtKDKmS1cYgW0r
    $ REDIRECT_URI=urn:ietf:wg:oauth:2.0:oob
    
    $ curl --data "code=$AUTHORIZATION_CODE" --data "client_id=$CLIENT_ID" --data "client_secret=$CLIENT_SECRET" --data "redirect_uri=$REDIRECT_URI" --data "grant_type=authorization_code" --data "access_type=offline" https://www.googleapis.com/oauth2/v4/token
    {
      "access_token": "ya29.GlsOB-ebr6NrI78UemOPHcm1-jdw0XkxD8iiSqE-Bh5xB_Sx8bhKsRhRyz7gqJy45A-HIF6s6GF0j5wz0dmNppVqEMhtUurAwfbe-xgEsR5MZFjoIY3ONOx8zd4Q",
      "expires_in": 3600,
      "refresh_token": "1/8LrGRLdBaFJYHlOr0rEAyZcgC9yDl2PcZZyrbqoxc7c",
      "scope": "https://www.googleapis.com/auth/photoslibrary",
      "token_type": "Bearer"
    }
    
  • ACCESS_TOKENは1時間しか有効でないので、適宜REFRESH_TOKENを使って更新
    1
    2
    3
    4
    5
    
    $ REFRESH_TOKEN=1/8LrGRLdBaFJYHlOr0rEAyZcgC9yDl2PcZZyrbqoxc7c
    $ CLIENT_ID=952391557281-s8b8ditnocfu590fi0ntsfk76rbmkm80.apps.googleusercontent.com
    $ CLIENT_SECRET=k6XPLuryMWUtKDKmS1cYgW0r
    
    $ ACCESS_TOKEN=`curl -s --data "refresh_token=$REFRESH_TOKEN" --data "client_id=$CLIENT_ID" --data "client_secret=$CLIENT_SECRET" --data "grant_type=refresh_token" https://www.googleapis.com/oauth2/v4/token|jq .access_token -r`
    

REFRESH_TOKENを取得すれば、あとCLIENT_IDCLIENT_SECRETが分かればACCESS_TOKENは更新できます。

ALBUMの作成

  • 既存のAlbumの確認
    1
    
    $ curl -s -H "Authorization: Bearer $ACCESS_TOKEN" https://photoslibrary.googleapis.com/v1/albums?pageSize=50
    
  • 既存のAlbumの確認(nextPageTokenがある場合)
    1
    
    $ curl -s -H "Authorization: Bearer $ACCESS_TOKEN" https://photoslibrary.googleapis.com/v1/albums?pageSize=50&pageToken=...
    
  • 新規Albumの作成
    1
    2
    3
    4
    5
    6
    7
    8
    
    $ DIR=20190428
    $ curl -s -X POST -H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-type: application/json" -d '{ "album": { "title":"'$DIR'" } }' https://photoslibrary.googleapis.com/v1/albums
    {
      "id": "ADIlBkAOcfB64a_Opnwdjgxeq6jhQv4GQ1pZQ-wse2o2hiBIofuhefmFycfTtIcLAG0inLt0FlZn",
      "title": "20190428",
      "productUrl": "https://photos.google.com/lr/album/ADIlBkAOcfB64a_Opnwdjgxeq6jhQv4GQ1pZQ-wse2o2hiBIofuhefmFycfTtIcLAG0inLt0FlZn",
      "isWriteable": true
    }
    

UPLOAD and adding to Album

2段階になっていて、

  1. binary fileをuploadしてUPLOAD_TOKENを得る
  2. UPLOAD_TOKENを元にmediaItems:batchCreateする(ALBUM名はここで渡す。batch処理なので複数のUPLOAD_TOKENを渡せる)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
FILENAME=20190428/img_0699.jpg

$ curl -s -X POST -H "Authorization: Bearer $ACCESS_TOKEN" -H 'Content-type: application/octet-stream' -H 'X-Goog-Upload-Protocol: raw' -H "X-Goog-Upload-File-Name: $FILENAME" --data-binary "@$FILENAME" https://photoslibrary.googleapis.com/v1/uploads
CAIS+QIASsy4zFbu3IKGgbDXA5XshOGvHPOTLuqbqTN9MQxKRVCsxp3YHbus+qsDgA0GCjuqXdmGpv1uWFxKvf8GYa/8VJQ1S6FUcmGWgw6Hdj14QNYtBRVbXU/cdq/Jkx3ZblG5co3hnY6+yMxih26kB0vTWfWp9GwIE904y5yXEE1pm/V0bFduzA/CZvdlAU9EvWfqKnNO7c3nozWUalm5WUZHHatVQZT+H5+jD0Bq3YwMUdfC5KF048AxFa9auW1HpQGdboalYyXBCJksfzteWtU53wZ8rFnZgHwrui9uA2ptnTuDlin2m+WXU+HqaVRuKX1ou5BzalI4P0gVfWql41Af6nuvvEdMNZ39tEvK2EARUX0CUd8veDznZiWjtPcRqpJnvjDRCxaSgr/cn+JXf9k7SnD0DYVWOdM64lngcAuXxsKk6RJJOVxQBUi6XAG04dHnKxDndqjl+fcH9qWAmpXejPx8Kgn6GX7TgatiKHEG4ybvWjStWg1JPg

$ UPLOAD_TOKEN=CAIS+...
$ ALBUM_ID=ADIlBkAOcfB64a_Opnwdjgxeq6jhQv4GQ1pZQ-wse2o2hiBIofuhefmFycfTtIcLAG0inLt0FlZn
$ curl -s -X POST -H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-type: application/json" -d '{ "albumId": "'$ALBUM_ID'", "newMediaItems":[ { "simpleMediaItem": { "uploadToken": "'$UPLOAD_TOKEN'" }} ] }' https://photoslibrary.googleapis.com/v1/mediaItems:batchCreate
{
  "newMediaItemResults": [
    {
      "uploadToken": "CAIS+...",
      "status": {
        "message": "OK"
      },
      "mediaItem": {
        "id": "ADIl...",
        "productUrl": "https://photos.google.com/lr/album/ADIl...",
        "mimeType": "image/jpeg",
        "mediaMetadata": {
          "creationTime": "2019-04-28T02:40:35Z",
          "width": "5184",
          "height": "3456"
        },
        "filename": "20190428/img_0699.jpg"
      }
    }
  ]
}

folderまるっとupload

  • 事前準備
    1
    2
    3
    4
    
    REFRESH_TOKEN=...
    CLIENT_ID=...
    CLIENT_SECRET=...
    ACCESS_TOKEN=`curl -s --data "refresh_token=$REFRESH_TOKEN" --data "client_id=$CLIENT_ID" --data "client_secret=$CLIENT_SECRET" --data "grant_type=refresh_token" https://www.googleapis.com/oauth2/v4/token|jq .access_token -r`
    
  • Album作成
    1
    2
    
    DIR=...
    ALBUM_ID=`curl -s -X POST -H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-type: application/json" -d '{"album":{"title":"'$DIR'"}}' https://photoslibrary.googleapis.com/v1/albums|jq -r .id`
    
  • ~/photo/$DIR以下のimg_*.jpg filesのuploadとalbum登録(約100 files毎にACCESS_TOKENのrefresh)
1
for i in ~/photo/$DIR/img_*.jpg; do if [ ! ${i##*00.jpg} ];then ACCESS_TOKEN=`curl -s --data "refresh_token=$REFRESH_TOKEN" --data "client_id=$CLIENT_ID" --data "client_secret=$CLIENT_SECRET" --data "grant_type=refresh_token" https://www.googleapis.com/oauth2/v4/token|jq .access_token -r`;fi;UPLOAD_TOKEN=`FILENAME=$DIR/${i##*/}; curl -s -X POST -H "Authorization: Bearer $ACCESS_TOKEN" -H 'Content-type: application/octet-stream' -H 'X-Goog-Upload-Protocol: raw' -H "X-Goog-Upload-File-Name: $FILENAME" --data-binary "@$i" https://photoslibrary.googleapis.com/v1/uploads`;curl -s -X POST -H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-type: application/json" -d '{"albumId":"'$ALBUM_ID'","newMediaItems":[{"simpleMediaItem":{"uploadToken":"'$UPLOAD_TOKEN'"}}]}' https://photoslibrary.googleapis.com/v1/mediaItems:batchCreate|tee -a /tmp/upload.log|grep -q error&&echo $i;done|tee /tmp/upload_failed.log

uploadに失敗したfile namesが標準出力と/tmp/upload_failed.logに出てくるので、後刻それらをretry。

1
for i in `cat /tmp/upload_failed.log`; do if [ ! ${i##*00.jpg} ];then ACCESS_TOKEN=`curl -s --data "refresh_token=$REFRESH_TOKEN" --data "client_id=$CLIENT_ID" --data "client_secret=$CLIENT_SECRET" --data "grant_type=refresh_token" https://www.googleapis.com/oauth2/v4/token|jq .access_token -r`;fi;UPLOAD_TOKEN=`FILENAME=$DIR/${i##*/}; curl -s -X POST -H "Authorization: Bearer $ACCESS_TOKEN" -H 'Content-type: application/octet-stream' -H 'X-Goog-Upload-Protocol: raw' -H "X-Goog-Upload-File-Name: $FILENAME" --data-binary "@$i" https://photoslibrary.googleapis.com/v1/uploads`;curl -s -X POST -H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-type: application/json" -d '{"albumId":"'$ALBUM_ID'","newMediaItems":[{"simpleMediaItem":{"uploadToken":"'$UPLOAD_TOKEN'"}}]}' https://photoslibrary.googleapis.com/v1/mediaItems:batchCreate|tee -a /tmp/upload.log|grep -q error&&echo $i;done

これではbatch処理を活かしていない(複数のUPLOAD_TOKENをbatchCreateしていない)のですが、Googleだけに割とすぐ終わること、error handlingがあまりにも複雑になることから、都度batchCreateすることにしました。

私の場合、1000 filesで約3GB弱、を目処に分割してuploadしています。 uploadしたfilesは全て「元のサイズ」で保存されてしまい、Google Driveの容量を消費してしまうので、設定から「容量を解放」しなければなりません。これが「1日1回」となっているものの、だからといって24時間後に再度実行しても「ファイルを圧縮できませんでした。ストレージを復元できるのは 1 日 1 回だけです。」と言われて出来ず、困っています。実際に再度実行できるまでには1.5日〜2日かかるようで、これが最大のneckになっています。

新規Albumへの既存files追加

これはダメでした。 何度試してもダメだったので、調べてみると、公式Documentに、 Note that you can only add media items that have been uploaded by your application to albums that your application has created.とあります。 なんでやねん! 何で既存の画像とAPI経由の画像とを区別するのか、わけわかりません。 それじゃぁ、っていうんで、既にGoogle Photos上にある写真も改めてuploadしてalbumにaddしたら、それは出来ました。しかし、「元のサイズ」になってしまって容量を食ってしまいます。これについても「容量を解放」しなければなりません。 全く七面倒臭いものです。

ちなみに、以下のようにやりました。paginationが発生しない程度のAlbum限定で、

1
2
3
4
5
6
7
8
9
10
11
12
13
$ DIR=20171005
$ ALBUM_ID=`curl -s -X POST -H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-type: application/json" -d '{ "album": { "title":"'$DIR'" } }' https://photoslibrary.googleapis.com/v1/albums|jq -r .id`

$ MEDIA_ITEMS=`curl -s -X POST -H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-type: application/json" -d '{"pageSize":"100","filters":{"dateFilter":{"dates":[{"year":2017,"month":10,"day":5}]}}}' https://photoslibrary.googleapis.com/v1/mediaItems:search|jq .mediaItems[].id|sed -z 's/\n/,/g'`

$ curl -s -X POST -H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-type: application/json" -d '{"mediaItemIds":['$MEDIA_ITEMS']}' https://photoslibrary.googleapis.com/v1/albums/${ALBUM_ID}:batchAddMediaItems
{
  "error": {
    "code": 400,
    "message": "Request contains an invalid media item id.",
    "status": "INVALID_ARGUMENT"
  }
}
なら既存のものは手でやればいいではないか? やってみたのですが、微妙に手元のfilesと数が合わなかったりするので、困難です。手元に3282枚あってGoogle PhotosのAlbumに3281枚あった時、どうやって差分をあぶり出したらいいのですか?! 全downloadはなしで。もう一つでは、手元に5221枚、Google Photosに5230枚と増えてます! Manuallyでは限界を感じました。

Album中の全file名取得

自己解決しました。 NEXT_PAGE_TOKENあると面倒くさいんですけど、これで何とか。

Album探し

最初のpageに目的のalbumがあるかをこれ↓で探す

1
curl -s -H "Authorization: Bearer $ACCESS_TOKEN" 'https://photoslibrary.googleapis.com/v1/albums?pageSize=50'|jq -r .albums[].title,.nextPageToken
なければ次のpageへ。

1
curl -s -H "Authorization: Bearer $ACCESS_TOKEN" 'https://photoslibrary.googleapis.com/v1/albums?pageSize=50&pageToken=Ck...'|jq -r .albums[].title,.nextPageToken

見付かれば、ALBUM_IDを同定。

1
2
3
4
curl -s -H "Authorization: Bearer $ACCESS_TOKEN" 'https://photoslibrary.googleapis.com/v1/albums?pageSize=50&pageToken=Ck...'|grep -1 20080318
      "id": "ADI...",
      "title": "20080318",
      "productUrl": "https://photos.google.com/lr/album/ADI...",

そうしてから徐に、

1
2
ALBUM_ID=...
NEXT_PAGE_TOKEN=`curl -s -X POST -H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-type: application/json" -d '{"pageSize":"100","albumId":"'$ALBUM_ID'"}' https://photoslibrary.googleapis.com/v1/mediaItems:search|jq -r '.mediaItems[].filename,.nextPageToken'|tee /tmp/files.txt|tail -1`;while [ "$NEXT_PAGE_TOKEN" != null ];do NEXT_PAGE_TOKEN=`curl -s -X POST -H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-type: application/json" -d '{"pageSize":"100","albumId":"'$ALBUM_ID'","pageToken":"'$NEXT_PAGE_TOKEN'"}' https://photoslibrary.googleapis.com/v1/mediaItems:search|jq -r '.mediaItems[].filename,.nextPageToken'|tee -a /tmp/files.txt|tail -1`;done

その後、

1
grep -vE -e '.{300,}' -e null /tmp/files.txt

で取り出せます。

それで比較(diff -y --suppress-common-lines <(cd ~/photo;ls .../img_*|sort) <(grep -vE -e '.{300,}' -e null /tmp/files.txt|sort))したところ、足りないものはわかりました。 ですが、何故かAPIで取ると3282個なのに 「コンテンツ 3283個」と表示されていたり... よく精査すると、なるほど、Google Photosが勝手に?作った、 アシスタントにある MOVIE.mp4(ムービー)や...-EFFECTS.jpg(スタイルを適用した写真)、 ...-PANO.jpg(パノラマ)が含まれているから? いや、一つ(5230個と表示)はそれが原因で9個多く数が表示されていたのですが、 もう一つ、「コンテンツ 3283個」は、APIで取得するといくら見ても 3282個しかない、自動生成物もない、です。謎です。

それと、EnrichmentとかいってTextやLocationとMapを入れられるんですけど、 それらを取得する術がなく、Textに書いたことを検索するとかも出来ず。 GUIから入れてみましたが、要素を追加する度にいちいち先頭に戻される、 移動すると他の要素もどこかに行ってしまうことがある、 等何だかなぁ、というものでした。 Googleならこんなもんじゃないだろー!

3 Good Things in This Week

| Comments

また幸せになるため?に、 先週3つ良かったことを並べてみます。

  • バイトで発表を何とか凌げたこと(でも本番のreportを書き上げる筈のこの土日、またなぁんにもしませんでした...orz)
  • 職場で定期本番リリースを無事?終えたこと(完全に付きっきりでしたけど。まぁ最初だから仕方ありませんよね。けど次のリリース、一人で出来るかな?)
  • Paiza S Rank、CodeIQ S Rankを持っていたこと(でもこんな役に立たないもの自慢?という程のものではないにしても、何だか、ですが。みみっちぃなぁ...)

あと、12月半月分の給料もいっぺんに出たので、 今回の総額が多かったこと、とか?

良くないことは沢山思い付くんですけど、 それじゃぁいけないんですよね。 しかしまぁ、総体的には健気に生きています。