blob.rb 5.43 KB
Newer Older
1 2
# Blob is a Rails-specific wrapper around Gitlab::Git::Blob objects
class Blob < SimpleDelegator
Jacob Vosmaer's avatar
Jacob Vosmaer committed
3 4 5
  CACHE_TIME = 60 # Cache raw blobs referred to by a (mutable) ref for 1 minute
  CACHE_TIME_IMMUTABLE = 3600 # Cache blobs referred to by an immutable reference for 1 hour

6 7
  MAXIMUM_TEXT_HIGHLIGHT_SIZE = 1.megabyte

8 9 10 11 12 13 14 15 16 17 18 19 20 21
  # Finding a viewer for a blob happens based only on extension and whether the
  # blob is binary or text, which means 1 blob should only be matched by 1 viewer,
  # and the order of these viewers doesn't really matter.
  #
  # However, when the blob is an LFS pointer, we cannot know for sure whether the
  # file being pointed to is binary or text. In this case, we match only on
  # extension, preferring binary viewers over text ones if both exist, since the
  # large files referred to in "Large File Storage" are much more likely to be
  # binary than text.
  #
  # `.stl` files, for example, exist in both binary and text forms, and are
  # handled by different viewers (`BinarySTL` and `TextSTL`) depending on blob
  # type. LFS pointers to `.stl` files are assumed to always be the binary kind,
  # and use the `BinarySTL` viewer.
22
  RICH_VIEWERS = [
23 24 25 26
    BlobViewer::Markup,
    BlobViewer::Notebook,
    BlobViewer::SVG,

Douwe Maan's avatar
Douwe Maan committed
27 28
    BlobViewer::Image,
    BlobViewer::Sketch,
29
    BlobViewer::Balsamiq,
30

31
    BlobViewer::Video,
32

33 34
    BlobViewer::PDF,

Douwe Maan's avatar
Douwe Maan committed
35
    BlobViewer::BinarySTL,
36
    BlobViewer::TextSTL
37
  ].sort_by { |v| v.binary? ? 0 : 1 }.freeze
38

39 40 41
  AUXILIARY_VIEWERS = [
    BlobViewer::GitlabCiYml,
    BlobViewer::RouteMap,
42

43
    BlobViewer::Readme,
44
    BlobViewer::License,
45
    BlobViewer::Contributing,
46 47 48 49 50 51 52 53 54 55 56 57 58
    BlobViewer::Changelog,

    BlobViewer::Cartfile,
    BlobViewer::ComposerJson,
    BlobViewer::Gemfile,
    BlobViewer::Gemspec,
    BlobViewer::GodepsJson,
    BlobViewer::PackageJson,
    BlobViewer::Podfile,
    BlobViewer::Podspec,
    BlobViewer::PodspecJson,
    BlobViewer::RequirementsTxt,
    BlobViewer::YarnLock
59
  ].freeze
60

Douwe Maan's avatar
Douwe Maan committed
61 62
  attr_reader :project

63 64 65 66 67 68 69 70 71 72
  # Wrap a Gitlab::Git::Blob object, or return nil when given nil
  #
  # This method prevents the decorated object from evaluating to "truthy" when
  # given a nil value. For example:
  #
  #     blob = Blob.new(nil)
  #     puts "truthy" if blob # => "truthy"
  #
  #     blob = Blob.decorate(nil)
  #     puts "truthy" if blob # No output
Douwe Maan's avatar
Douwe Maan committed
73
  def self.decorate(blob, project = nil)
74 75
    return if blob.nil?

Douwe Maan's avatar
Douwe Maan committed
76 77 78
    new(blob, project)
  end

Douwe Maan's avatar
Douwe Maan committed
79
  def initialize(blob, project = nil)
Douwe Maan's avatar
Douwe Maan committed
80 81 82
    @project = project

    super(blob)
83 84
  end

85 86 87 88 89 90 91 92 93 94 95 96
  # Returns the data of the blob.
  #
  # If the blob is a text based blob the content is converted to UTF-8 and any
  # invalid byte sequences are replaced.
  def data
    if binary?
      super
    else
      @data ||= super.encode(Encoding::UTF_8, invalid: :replace, undef: :replace)
    end
  end

97 98 99 100
  def load_all_data!
    super(project.repository) if project
  end

101
  def no_highlighting?
102 103 104 105 106
    raw_size && raw_size > MAXIMUM_TEXT_HIGHLIGHT_SIZE
  end

  def empty?
    raw_size == 0
107 108
  end

109 110 111 112 113 114 115 116 117 118 119 120 121 122
  def external_storage_error?
    if external_storage == :lfs
      !project&.lfs_enabled?
    else
      false
    end
  end

  def stored_externally?
    return @stored_externally if defined?(@stored_externally)

    @stored_externally = external_storage && !external_storage_error?
  end

Douwe Maan's avatar
Douwe Maan committed
123 124 125
  # Returns the size of the file that this blob represents. If this blob is an
  # LFS pointer, this is the size of the file stored in LFS. Otherwise, this is
  # the size of the blob itself.
126
  def raw_size
127 128
    if stored_externally?
      external_size
129 130 131
    else
      size
    end
132 133
  end

Douwe Maan's avatar
Douwe Maan committed
134 135 136 137
  # Returns whether the file that this blob represents is binary. If this blob is
  # an LFS pointer, we assume the file stored in LFS is binary, unless a
  # text-based rich blob viewer matched on the file's extension. Otherwise, this
  # depends on the type of the blob itself.
138
  def raw_binary?
139
    if stored_externally?
140 141
      if rich_viewer
        rich_viewer.binary?
142 143 144 145
      elsif Linguist::Language.find_by_filename(name).any?
        false
      elsif _mime_type
        _mime_type.binary?
146 147 148
      else
        true
      end
149 150 151
    else
      binary?
    end
152 153
  end

154 155
  def extension
    @extension ||= extname.downcase.delete('.')
156 157
  end

158 159
  def video?
    UploaderHelper::VIDEO_EXT.include?(extension)
160 161
  end

162
  def readable_text?
163
    text? && !stored_externally? && !truncated?
Phil Hughes's avatar
Phil Hughes committed
164 165
  end

166 167 168 169 170 171 172
  def simple_viewer
    @simple_viewer ||= simple_viewer_class.new(self)
  end

  def rich_viewer
    return @rich_viewer if defined?(@rich_viewer)

Douwe Maan's avatar
Douwe Maan committed
173
    @rich_viewer = rich_viewer_class&.new(self)
174 175
  end

176 177 178 179 180 181
  def auxiliary_viewer
    return @auxiliary_viewer if defined?(@auxiliary_viewer)

    @auxiliary_viewer = auxiliary_viewer_class&.new(self)
  end

Douwe Maan's avatar
Douwe Maan committed
182
  def rendered_as_text?(ignore_errors: true)
183
    simple_viewer.text? && (ignore_errors || simple_viewer.render_error.nil?)
184 185 186
  end

  def show_viewer_switcher?
Douwe Maan's avatar
Douwe Maan committed
187 188 189
    rendered_as_text? && rich_viewer
  end

190 191 192
  def expand!
    simple_viewer&.expanded = true
    rich_viewer&.expanded = true
193 194 195 196
  end

  private

197 198 199 200 201 202 203 204 205 206 207
  def simple_viewer_class
    if empty?
      BlobViewer::Empty
    elsif raw_binary?
      BlobViewer::Download
    else # text
      BlobViewer::Text
    end
  end

  def rich_viewer_class
208 209 210 211 212 213 214 215
    viewer_class_from(RICH_VIEWERS)
  end

  def auxiliary_viewer_class
    viewer_class_from(AUXILIARY_VIEWERS)
  end

  def viewer_class_from(classes)
216
    return if empty? || external_storage_error?
217

218
    verify_binary = !stored_externally?
Douwe Maan's avatar
Douwe Maan committed
219

220
    classes.find { |viewer_class| viewer_class.can_render?(self, verify_binary: verify_binary) }
221
  end
222
end