file_uploader.rb 3.7 KB
Newer Older
1 2 3 4 5 6 7 8
# 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.
#
J
Jacob Vosmaer 已提交
9
class FileUploader < GitlabUploader
10
  include UploaderHelper
11
  include RecordsUploads::Concern
12

13
  MARKDOWN_PATTERN = %r{\!?\[.*?\]\(/uploads/(?<secret>[0-9a-f]{32})/(?<file>.*?)\)}
14
  DYNAMIC_PATH_PATTERN = %r{(?<secret>\h{32})/(?<identifier>.*)}
15

16 17
  storage :file

18 19
  after :remove, :prune_store_dir

20 21 22 23 24
  def self.root
    File.join(options.storage_path, 'uploads')
  end

  def self.absolute_path(upload)
25
    File.join(
26 27
      absolute_base_dir(upload.model),
      upload.path # already contain the dynamic_segment, see #upload_path
28 29 30
    )
  end

31 32 33 34 35 36 37
  def self.base_dir(model)
    model_path_segment(model)
  end

  # used in migrations and import/exports
  def self.absolute_base_dir(model)
    File.join(root, base_dir(model))
38 39
  end

40 41 42 43 44 45
  # 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.
  #
46
  # model - Object that responds to `full_path` and `disk_path`
47 48
  #
  # Returns a String without a trailing slash
49
  def self.model_path_segment(model)
J
Jarka Kadlecova 已提交
50
    if model.hashed_storage?(:attachments)
51
      model.disk_path
52
    else
53
      model.full_path
54
    end
55 56
  end

57 58 59 60 61 62
  def self.upload_path(secret, identifier)
    File.join(secret, identifier)
  end

  def self.generate_secret
    SecureRandom.hex
63 64
  end

65
  attr_accessor :model
66

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

70
    @model = model
71
    apply_context!(uploader_context)
72 73
  end

74 75
  def base_dir
    self.class.base_dir(@model)
76 77
  end

78 79 80 81
  # 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
  def absolute_path
    self.class.absolute_path(@upload)
82 83
  end

84 85
  def upload_path
    self.class.upload_path(dynamic_segment, identifier)
86 87
  end

88 89 90 91 92 93 94
  def model_path_segment
    self.class.model_path_segment(@model)
  end

  def store_dir
    File.join(base_dir, dynamic_segment)
  end
D
Douwe Maan 已提交
95

96 97
  def markdown_link
    markdown = "[#{markdown_name}](#{secure_url})"
98
    markdown.prepend("!") if image_or_video? || dangerous?
99 100
    markdown
  end
D
Douwe Maan 已提交
101

102
  def to_h
D
Douwe Maan 已提交
103
    {
104
      alt:      markdown_name,
R
Robert Speicher 已提交
105
      url:      secure_url,
106
      markdown: markdown_link
D
Douwe Maan 已提交
107 108
    }
  end
109

110 111 112 113 114 115
  def filename
    self.file.filename
  end

  def upload=(value)
    super
M
Micaël Bergeron 已提交
116 117 118 119 120 121 122 123 124

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

    # fallback to the regex based extraction
    if matches = DYNAMIC_PATH_PATTERN.match(value.path)
      @secret = matches[:secret]
      @identifier = matches[:identifier]
    end
125 126 127 128 129 130
  end

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

R
Robert Speicher 已提交
131 132
  private

133 134 135 136 137 138 139 140 141 142 143 144
  def apply_context!(uploader_context)
    @secret, @identifier = uploader_context.values_at(:secret, :identifier)

    !!(@secret && @identifier)
  end

  def build_upload
    super.tap do |upload|
      upload.secret = secret
    end
  end

145 146 147 148
  def prune_store_dir
    storage.delete_dir!(store_dir) # only remove when empty
  end

149 150
  def markdown_name
    (image_or_video? ? File.basename(filename, File.extname(filename)) : filename).gsub("]", "\\]")
151 152
  end

153 154 155 156 157 158
  def identifier
    @identifier ||= filename
  end

  def dynamic_segment
    secret
159
  end
R
Robert Speicher 已提交
160 161 162 163

  def secure_url
    File.join('/uploads', @secret, file.filename)
  end
164
end