File: photo_mixin.rb

package info (click to toggle)
mikutter 5.0.4%2Bdfsg1-2
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 9,700 kB
  • sloc: ruby: 21,307; sh: 181; makefile: 19
file content (217 lines) | stat: -rw-r--r-- 6,620 bytes parent folder | download | duplicates (3)
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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
# -*- coding: utf-8 -*-

require 'serialthread'

=begin rdoc
画像リソースを扱うModelのためのmix-in。
これをincludeすると、画像データを保存するblobフィールドが追加される。

このmoduleをincludeしたクラスは、必要に応じて _download_routine_ をオーバライドする
=end
module Diva::Model::PhotoMixin
  DownloadThread = SerialThreadGroup.new(max_threads: 4, deferred: Delayer::Deferred)
  PARTIAL_READ_BYTESIZE = 1024 ** 2

  include Diva::Model::PhotoInterface

  def self.included(klass)
    klass.field.string :blob
  end

  def initialize(*rest)
    super
    @read_count = 0
    @cached = false
    @forget = nil
  end

  # 画像をダウンロードする。
  # partialを指定すると、ダウンロードの進捗があれば、前回呼び出されたときから
  # ダウンロードできた内容を引数に呼び出される。
  # 既にダウンロードが終わっていれば、 _blob_ の戻り値がそのまま渡される。
  # このメソッドは、複数回呼び出されても画像のダウンロードを一度しか行わない。
  # widthとheightは、画像のサイズが複数ある場合に、ダウンロードする画像を決めるために使う。リサイズされるわけではない。
  # ==== Args
  # [width:] ヒントとして提供する幅(px)
  # [height:] ヒントとして提供する高さ(px)
  # [&partial_callback] 現在ダウンロードできたデータの一部(String)
  # ==== Return
  # [Delayer::Deferred::Deferredable] ダウンロードが完了したらselfを引数に呼び出される
  def download(width: nil, height: nil, &partial_callback)      # :yield: part
    increase_read_count
    case @state
    when :complete
      partial_callback.(blob) if block_given?
      Delayer::Deferred.new.next{ self }
    when :download
      append_download_queue(&partial_callback)
    else
      download!(&partial_callback)
    end
  end

  # 画像のダウンロードが終わっていれば真を返す。
  # 真を返す時、 _blob_ には完全な画像の情報が存在している
  def completed?
    @state == :complete
  end

  # 画像をダウロード中なら真
  def downloading?
    @state == :download
  end

  # ダウンロードが始まっていなければ真
  def ready?
    !@state
  end

  def inspect
    if @state == :complete
      "#<#{self.class}: #{uri} (state: #{@state}, #{self.blob.size} bytes cached)>"
    else
      "#<#{self.class}: #{uri} (state: #{@state})>"
    end
  end

  private

  def download!(&partial_callback)
    atomic do
      return download(&partial_callback) unless ready?
      promise = initialize_download(&partial_callback)
      DownloadThread.new(&method(:cache_read_or_download)).next{|success|
        if success
          finalize_download_as_success
        else
          Delayer::Deferred.fail false
        end
      }.trap{|exception|
        finalize_download_as_fail(exception)
      }.terminate('error')
      promise
    end
  end

  def append_download_queue(&partial_callback)
    atomic do
      return download(&partial_callback) unless downloading?
      register_partial_callback(partial_callback)
      register_promise
    end
  end

  def register_promise
    promise = Delayer::Deferred.new(true)
    (@promises ||= Set.new) << promise
    promise
  end

  def register_partial_callback(cb)
    @partials ||= Set.new
    if cb
      @partials  << cb
      cb.(@buffer) if !@buffer.empty?
    end
  end

  def cache_read_or_download
    cache_read_routine || download_routine
  end

  def cache_read_routine
    raw = Plugin.filtering(:image_cache, uri.to_s, nil)[1]
    if raw.is_a?(String)
      @buffer = raw.freeze
      atomic{ @partials.each{|c|c.(raw)} }
      true
    end
  end

  def download_routine
    if uri.scheme == 'file'
      File.open(uri.path, &method(:download_mainloop))
    else
      URI.open(uri.to_s, &method(:download_mainloop))
    end
  rescue EOFError
    true
  end

  # _input_stream_ から、画像をダウンロードし、 _@buffer_ に格納する。
  # このインスタンスの _download_ メソッドが既に呼ばれていて、ブロックが渡されている場合、
  # そのブロックに一定の間隔でダウンロードしたデータを渡す。
  # PhotoMixinをincludeしたクラスでオーバライドされた _download_routine_ から呼ばれることを想定している。
  # ==== Args
  # [input_stream] 画像データがどんどん出てくる IO のインスタンス
  def download_mainloop(input_stream)
    loop do
      Thread.pass
      partial = input_stream.readpartial(PARTIAL_READ_BYTESIZE)
      if @buffer
        @buffer << partial
      else
        @buffer = +partial
      end
      atomic{ @partials.each{|c|c.(partial)} }
    end
  end

  def initialize_download(&partial_callback)
    @state = :download
    @buffer = nil
    register_partial_callback(partial_callback)
    register_promise
  end

  def finalize_download_as_success
    atomic do
      self.blob = @buffer.freeze
      @state = :complete
      @promises.each{|p| p.call(self) }
      @buffer = @promises = @partials = nil
    end
  end

  def finalize_download_as_fail(exception)
    atomic do
      @state = nil
      @promises.each{|p| p.fail(exception) }
      @buffer = @promises = @partials = nil
    end
  end

  # 画像が読まれた回数をインクリメントする。
  # 読み込まれた回数が規定値を超えたら、blobを引数に image_file_cache_photo イベントを発生させて、ストレージにキャッシュさせる
  def increase_read_count
    @read_count += 1
    if !@cached and @read_count >= appear_limit
      @cached = true
      Plugin.call(:image_file_cache_photo, self)
    end
    set_forget_timer
  end

  # blobのメモリキャッシュ消滅タイマーをリセットする。
  # 既に動いているタイマーがあればそれをキャンセルする。
  def set_forget_timer
    @forget.cancel if @forget
    @forget = Delayer.new(:destroy_cache, delay: forget_time){ forget! }
  end

  # 覚えておりません
  def forget!
    @forget = @state = self.blob = nil
  end

  # キャッシュする出現回数のしきい値を返す
  def appear_limit
    UserConfig[:image_file_cache_appear_limit] || 32
  end

  # 画像をメモリキャッシュする時間(秒)
  # Pixbufが生成されてしまえば基本的にblobにはアクセスされないので、短くて良いと思う
  def forget_time
    (UserConfig[:photo_forget_time] || 60)
  end
end