summaryrefslogtreecommitdiff
path: root/app/uploaders/gitlab_uploader.rb
blob: 62024bff4c080d12936117b3d79de2b675c40bb5 (plain)
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
# frozen_string_literal: true

class GitlabUploader < CarrierWave::Uploader::Base
  include ContentTypeWhitelist::Concern

  class_attribute :options

  PROTECTED_METHODS = %i(filename cache_dir work_dir store_dir).freeze

  ObjectNotReadyError = Class.new(StandardError)

  class << self
    # DSL setter
    def storage_options(options)
      self.options = options
    end

    def root
      options.storage_path
    end

    # represent the directory namespacing at the class level
    def base_dir
      options.fetch('base_dir', '')
    end

    def file_storage?
      storage == CarrierWave::Storage::File
    end

    def absolute_path(upload_record)
      File.join(root, upload_record.path)
    end

    def version(*args, **kwargs, &block)
      # CarrierWave uploaded file "versions" are not tracked in the uploads
      # table. Because of this they won't get replicated to Geo secondaries.
      # If we ever want to use versions, we need to fix that first. Also see
      # https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/1757.
      raise "using CarrierWave alternate file version is not supported"
    end
  end

  storage_options Gitlab.config.uploads

  delegate :base_dir, :file_storage?, to: :class

  before :cache, :protect_from_path_traversal!

  def initialize(model, mounted_as = nil, **uploader_context)
    super(model, mounted_as)
  end

  def file_cache_storage?
    cache_storage.is_a?(CarrierWave::Storage::File)
  end

  def move_to_cache
    file_storage?
  end

  def move_to_store
    file_storage?
  end

  def exists?
    file.present?
  end

  def cache_dir
    File.join(root, base_dir, 'tmp/cache')
  end

  def work_dir
    File.join(root, base_dir, 'tmp/work')
  end

  def filename
    super || file&.filename
  end

  def relative_path
    return path if pathname.relative?

    pathname.relative_path_from(Pathname.new(root))
  end

  def model_valid?
    !!model
  end

  def local_url
    File.join('/', self.class.base_dir, dynamic_segment, filename)
  end

  def cached_size
    size
  end

  def open
    stream =
      if file_storage?
        File.open(path, "rb") if path
      elsif url
        ::Gitlab::HttpIO.new(url, cached_size)
      end

    return unless stream
    return stream unless block_given?

    begin
      yield(stream)
    ensure
      stream.close
    end
  end

  def multi_read(offsets)
    open do |stream|
      offsets.map do |start_offset, end_offset|
        stream.seek(start_offset)
        stream.read(end_offset - start_offset + 1)
      end
    end
  end

  # Used to replace an existing upload with another +file+ without modifying stored metadata
  # Use this method only to repair/replace an existing upload, or to upload to a Geo secondary node
  #
  # @param [CarrierWave::SanitizedFile] file that will replace existing upload
  # @return CarrierWave::SanitizedFile
  def replace_file_without_saving!(file)
    raise ArgumentError, 'should be a CarrierWave::SanitizedFile' unless file.is_a? CarrierWave::SanitizedFile

    storage.store!(file)
  end

  def url_or_file_path(url_options = {})
    if file_storage?
      'file://' + path
    else
      url(url_options)
    end
  end

  private

  # Designed to be overridden by child uploaders that have a dynamic path
  # segment -- that is, a path that changes based on mutable attributes of its
  # associated model
  #
  # For example, `FileUploader` builds the storage path based on the associated
  # project model's `path_with_namespace` value, which can change when the
  # project or its containing namespace is moved or renamed.
  #
  # When implementing this method, raise `ObjectNotReadyError` if the model
  # does not yet exist, as it will be tested in `#protect_from_path_traversal!`
  def dynamic_segment
    raise(NotImplementedError)
  end

  # To prevent files from moving across filesystems, override the default
  # implementation:
  # http://github.com/carrierwaveuploader/carrierwave/blob/v1.0.0/lib/carrierwave/uploader/cache.rb#L181-L183
  def workfile_path(for_file = original_filename)
    # To be safe, keep this directory outside of the the cache directory
    # because calling CarrierWave.clean_cache_files! will remove any files in
    # the cache directory.
    File.join(work_dir, cache_id, version_name.to_s, for_file)
  end

  def pathname
    @pathname ||= Pathname.new(path)
  end

  # Protect against path traversal attacks
  # This takes a list of methods to test for path traversal, e.g. ../../
  # and checks each of them. This uses `.send` so that any potential errors
  # don't block the entire set from being tested.
  #
  # @param [CarrierWave::SanitizedFile]
  # @return [Nil]
  # @raise [Gitlab::Utils::PathTraversalAttackError]
  def protect_from_path_traversal!(file)
    PROTECTED_METHODS.each do |method|
      Gitlab::Utils.check_path_traversal!(self.send(method)) # rubocop: disable GitlabSecurity/PublicSend

    rescue ObjectNotReadyError
      # Do nothing. This test was attempted before the file was ready for that method
    end
  end
end