Class: FileUploader

Inherits:
GitlabUploader show all
Includes:
ObjectStorage::Concern, 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 =
%r{\!?\[.*?\]\(/uploads/(?<secret>[0-9a-f]{32})/(?<file>.*?)\)}.freeze
DYNAMIC_PATH_PATTERN =
%r{.*(?<secret>\b(\h{10}|\h{32}))\/(?<identifier>.*)}.freeze
VALID_SECRET_PATTERN =
%r{\A\h{10,32}\z}.freeze
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

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, #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?, #schedule_background_upload, #store!, #store_dir, #use_file

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 inherited from GitlabUploader

#cache_dir, #cached_size, #exists?, #file_cache_storage?, file_storage?, #filename, #local_url, #model_valid?, #move_to_cache, #move_to_store, #open, #relative_path, #replace_file_without_saving!, storage_options, #work_dir

Constructor Details

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

Returns a new instance of FileUploader.


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

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


84
85
86
# File 'app/uploaders/file_uploader.rb', line 84

def model
  @model
end

Class Method Details

.absolute_base_dir(model) ⇒ Object

used in migrations and import/exports


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

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

.absolute_path(upload) ⇒ Object


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

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

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


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

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


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

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


73
74
75
# File 'app/uploaders/file_uploader.rb', line 73

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

.generate_secretObject


69
70
71
# File 'app/uploaders/file_uploader.rb', line 69

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


61
62
63
64
65
66
67
# File 'app/uploaders/file_uploader.rb', line 61

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

.rootObject


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

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


108
109
110
# File 'app/uploaders/file_uploader.rb', line 108

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


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

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

#copy_file(file) ⇒ Object


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

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


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

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


121
122
123
# File 'app/uploaders/file_uploader.rb', line 121

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

#remote_storage_path(file_identifier) ⇒ Object


125
126
127
# File 'app/uploaders/file_uploader.rb', line 125

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

#secretObject

Raises:


157
158
159
160
161
162
163
# File 'app/uploaders/file_uploader.rb', line 157

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

  raise InvalidSecret unless @secret =~ VALID_SECRET_PATTERN

  @secret
end

#store_dirsObject


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

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


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

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

#upload=(value) ⇒ Object


144
145
146
147
148
149
150
151
152
153
154
155
# File 'app/uploaders/file_uploader.rb', line 144

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


112
113
114
115
116
117
118
119
# File 'app/uploaders/file_uploader.rb', line 112

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


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

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