Class: FileUploader

Inherits:
GitlabUploader show all
Includes:
ObjectStorage::Concern, ObjectStorage::Extension::RecordsUploads, RecordsUploads::Concern, UploaderHelper
Defined in:
app/uploaders/file_uploader.rb

Overview

This class breaks the actual CarrierWave concept. Every uploader should use a base_dir that is model agnostic so we can build back URLs from base_dir-relative paths saved in the ‘Upload` model.

As the ‘.base_dir` is model dependent and not saved in the upload model (see #upload_path) there is no way to build back the correct file path without the model, which defies CarrierWave way of storing files.

Direct Known Subclasses

NamespaceFileUploader, PersonalFileUploader

Constant Summary collapse

MARKDOWN_PATTERN =

This pattern is vulnerable to malicious inputs, so use Gitlab::UntrustedRegexp to place bounds on execution time

Gitlab::UntrustedRegexp.new(
  '!?\[.*?\]\(/uploads/(?P<secret>[0-9a-f]{32})/(?P<file>.*?)\)'
)
DYNAMIC_PATH_PATTERN =
%r{.*(?<secret>\b(\h{10}|\h{32}))\/(?<identifier>.*)}
VALID_SECRET_PATTERN =
%r{\A\h{10,32}\z}
InvalidSecret =
Class.new(StandardError)

Constants included from Gitlab::FileTypeDetection

Gitlab::FileTypeDetection::DANGEROUS_AUDIO_EXT, Gitlab::FileTypeDetection::DANGEROUS_IMAGE_EXT, Gitlab::FileTypeDetection::DANGEROUS_VIDEO_EXT, Gitlab::FileTypeDetection::PDF_EXT, Gitlab::FileTypeDetection::SAFE_AUDIO_EXT, Gitlab::FileTypeDetection::SAFE_IMAGE_EXT, Gitlab::FileTypeDetection::SAFE_IMAGE_FOR_SCALING_EXT, Gitlab::FileTypeDetection::SAFE_VIDEO_EXT

Constants inherited from GitlabUploader

GitlabUploader::ObjectNotReadyError, GitlabUploader::PROTECTED_METHODS

Instance Attribute Summary collapse

Attributes included from RecordsUploads::Concern

#upload

Class Method Summary collapse

Instance Method Summary collapse

Methods included from ObjectStorage::Concern

#cache!, #delete_migrated_file, #delete_tmp_file_after_storage, #exclusive_lease_key, #exists?, #file_cache_storage?, #file_storage?, #filename, #filename=, #fog_attributes, #fog_credentials, #fog_directory, #fog_public, #migrate!, #object_store, #object_store=, #persist_object_store!, #persist_object_store?, #retrieve_from_store!, #store!, #store_dir, #store_path, #use_file, #use_open_file

Methods included from Gitlab::Utils::Override

#extended, extensions, #included, #method_added, #override, #prepended, #queue_verification, verify!

Methods included from RecordsUploads::Concern

#filename, #readd_upload, #record_upload

Methods included from Gitlab::FileMarkdownLinkBuilder

#markdown_link, #markdown_name

Methods included from Gitlab::FileTypeDetection

#audio?, #dangerous_audio?, #dangerous_embeddable?, #dangerous_image?, #dangerous_video?, #embeddable?, extension_match?, #image?, #image_safe_for_scaling?, #pdf?, #video?

Methods included from ObjectStorage::Extension::RecordsUploads

#exclusive_lease_key, #retrieve_from_store!

Methods inherited from GitlabUploader

#cache_dir, #cached_size, #exists?, #file_cache_storage?, file_storage?, #filename, #local_url, #model_valid?, #move_to_cache, #move_to_store, #multi_read, #open, #options, options, #relative_path, #replace_file_without_saving!, storage_location, #url_or_file_path, version, #work_dir

Constructor Details

#initialize(model, mounted_as = nil, **uploader_context) ⇒ FileUploader

Returns a new instance of FileUploader.



94
95
96
97
98
99
# File 'app/uploaders/file_uploader.rb', line 94

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

  @model = model
  apply_context!(uploader_context)
end

Instance Attribute Details

#modelObject

Returns the value of attribute model.



92
93
94
# File 'app/uploaders/file_uploader.rb', line 92

def model
  @model
end

Class Method Details

.absolute_base_dir(model) ⇒ Object

used in migrations and import/exports



56
57
58
# File 'app/uploaders/file_uploader.rb', line 56

def self.absolute_base_dir(model)
  File.join(root, base_dir(model))
end

.absolute_path(upload) ⇒ Object



34
35
36
37
38
39
# File 'app/uploaders/file_uploader.rb', line 34

def self.absolute_path(upload)
  File.join(
    root,
    relative_path(upload)
  )
end

.base_dir(model, store = Store::LOCAL) ⇒ Object



48
49
50
51
52
53
# File 'app/uploaders/file_uploader.rb', line 48

def self.base_dir(model, store = Store::LOCAL)
  decorated_model = model
  decorated_model = Storage::Hashed.new(model) if store == Store::REMOTE

  model_path_segment(decorated_model)
end

.copy_to(uploader, to_project) ⇒ Object

return a new uploader with a file copy on another project



174
175
176
177
178
179
180
181
# File 'app/uploaders/file_uploader.rb', line 174

def self.copy_to(uploader, to_project)
  moved = self.new(to_project)
  moved.object_store = uploader.object_store
  moved.filename = uploader.filename

  moved.copy_file(uploader.file)
  moved
end

.extract_dynamic_path(path) ⇒ Object



81
82
83
# File 'app/uploaders/file_uploader.rb', line 81

def self.extract_dynamic_path(path)
  DYNAMIC_PATH_PATTERN.match(path)
end

.generate_secretObject



77
78
79
# File 'app/uploaders/file_uploader.rb', line 77

def self.generate_secret
  SecureRandom.hex
end

.model_path_segment(model) ⇒ Object

Returns the part of ‘store_dir` that can change based on the model’s current path

This is used to build Upload paths dynamically based on the model’s current namespace and path, allowing us to ignore renames or transfers.

model - Object that responds to ‘full_path` and `disk_path`

Returns a String without a trailing slash



69
70
71
72
73
74
75
# File 'app/uploaders/file_uploader.rb', line 69

def self.model_path_segment(model)
  case model
  when Storage::Hashed then model.disk_path
  else
    model.hashed_storage?(:attachments) ? model.disk_path : model.full_path
  end
end

.relative_path(upload) ⇒ Object



41
42
43
44
45
46
# File 'app/uploaders/file_uploader.rb', line 41

def self.relative_path(upload)
  File.join(
    base_dir(upload.model),
    upload.path # already contain the dynamic_segment, see #upload_path
  )
end

.rootObject



30
31
32
# File 'app/uploaders/file_uploader.rb', line 30

def self.root
  File.join(options.storage_path, 'uploads')
end

Instance Method Details

#absolute_pathObject

we don’t need to know the actual path, an uploader instance should be able to yield the file content on demand, so we should build the digest



116
117
118
# File 'app/uploaders/file_uploader.rb', line 116

def absolute_path
  self.class.absolute_path(@upload)
end

#base_dir(store = nil) ⇒ Object

enforce the usage of Hashed storage when storing to remote store as the FileMover doesn’t support OS



110
111
112
# File 'app/uploaders/file_uploader.rb', line 110

def base_dir(store = nil)
  self.class.base_dir(@model, store || object_store)
end

#copy_file(file) ⇒ Object



183
184
185
186
187
188
189
190
191
192
# File 'app/uploaders/file_uploader.rb', line 183

def copy_file(file)
  to_path = if file_storage?
              File.join(self.class.root, store_path)
            else
              store_path
            end

  self.file = file.copy_to(to_path)
  record_upload # after_store is not triggered
end

#initialize_copy(from) ⇒ Object



101
102
103
104
105
106
# File 'app/uploaders/file_uploader.rb', line 101

def initialize_copy(from)
  super

  @secret = self.class.generate_secret
  @upload = nil # calling record_upload would delete the old upload if set
end

#local_storage_path(file_identifier) ⇒ Object



129
130
131
# File 'app/uploaders/file_uploader.rb', line 129

def local_storage_path(file_identifier)
  File.join(dynamic_segment, file_identifier)
end

#remote_storage_path(file_identifier) ⇒ Object



133
134
135
# File 'app/uploaders/file_uploader.rb', line 133

def remote_storage_path(file_identifier)
  File.join(store_dir, file_identifier)
end

#secretObject

Raises:



165
166
167
168
169
170
171
# File 'app/uploaders/file_uploader.rb', line 165

def secret
  @secret ||= self.class.generate_secret

  raise InvalidSecret unless VALID_SECRET_PATTERN.match?(@secret)

  @secret
end

#store_dirsObject



137
138
139
140
141
142
# File 'app/uploaders/file_uploader.rb', line 137

def store_dirs
  {
    Store::LOCAL => File.join(base_dir, dynamic_segment),
    Store::REMOTE => File.join(base_dir(ObjectStorage::Store::REMOTE), dynamic_segment)
  }
end

#to_hObject



144
145
146
147
148
149
150
# File 'app/uploaders/file_uploader.rb', line 144

def to_h
  {
    alt: markdown_name,
    url: secure_url,
    markdown: markdown_link
  }
end

#upload=(value) ⇒ Object



152
153
154
155
156
157
158
159
160
161
162
163
# File 'app/uploaders/file_uploader.rb', line 152

def upload=(value)
  super

  return unless value
  return if apply_context!(value.uploader_context)

  # fallback to the regex based extraction
  if matches = self.class.extract_dynamic_path(value.path)
    @secret = matches[:secret]
    @identifier = matches[:identifier]
  end
end

#upload_pathObject



120
121
122
123
124
125
126
127
# File 'app/uploaders/file_uploader.rb', line 120

def upload_path
  if file_storage?
    # Legacy path relative to project.full_path
    local_storage_path(identifier)
  else
    remote_storage_path(identifier)
  end
end

#upload_paths(identifier) ⇒ Object



85
86
87
88
89
90
# File 'app/uploaders/file_uploader.rb', line 85

def upload_paths(identifier)
  [
    File.join(secret, identifier),
    File.join(base_dir(Store::REMOTE), secret, identifier)
  ]
end