Read about our upcoming Code of Conduct on this issue

upload.rb 4.55 KB
Newer Older
1
2
# frozen_string_literal: true

3
class Upload < ApplicationRecord
4
  include Checksummable
5
6
7
  # Upper limit for foreground checksum processing
  CHECKSUM_THRESHOLD = 100.megabytes

8
  belongs_to :model, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
9
10
11
12
13
14

  validates :size, presence: true
  validates :path, presence: true
  validates :model, presence: true
  validates :uploader, presence: true

15
  scope :with_files_stored_locally, -> { where(store: ObjectStorage::Store::LOCAL) }
Toon Claes's avatar
Toon Claes committed
16
  scope :with_files_stored_remotely, -> { where(store: ObjectStorage::Store::REMOTE) }
17

18
  before_save  :calculate_checksum!, if: :foreground_checksummable?
19
  after_commit :schedule_checksum,   if: :needs_checksum?
20

21
22
  # as the FileUploader is not mounted, the default CarrierWave ActiveRecord
  # hooks are not executed and the file will not be deleted
Micaël Bergeron's avatar
Micaël Bergeron committed
23
  after_destroy :delete_file!, if: -> { uploader_class <= FileUploader }
24

25
  class << self
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
    def inner_join_local_uploads_projects
      upload_table = Upload.arel_table
      project_table = Project.arel_table

      join_statement = upload_table.project(upload_table[Arel.star])
                         .join(project_table)
                         .on(
                           upload_table[:model_type].eq('Project')
                             .and(upload_table[:model_id].eq(project_table[:id]))
                             .and(upload_table[:store].eq(ObjectStorage::Store::LOCAL))
                         )

      joins(join_statement.join_sources)
    end

41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
    ##
    # FastDestroyAll concerns
    def begin_fast_destroy
      {
        Uploads::Local => Uploads::Local.new.keys(with_files_stored_locally),
        Uploads::Fog => Uploads::Fog.new.keys(with_files_stored_remotely)
      }
    end

    ##
    # FastDestroyAll concerns
    def finalize_fast_destroy(keys)
      keys.each do |store_class, paths|
        store_class.new.delete_keys_async(paths)
      end
    end
  end

59
  def absolute_path
60
    raise ObjectStorage::RemoteStoreError, _("Remote object has no absolute path.") unless local?
61
62
63
64
65
    return path unless relative_path?

    uploader_class.absolute_path(self)
  end

66
67
  def calculate_checksum!
    self.checksum = nil
68
    return unless needs_checksum?
69

70
    self.checksum = self.class.hexdigest(absolute_path)
71
72
  end

73
74
75
76
  # Initialize the associated Uploader class with current model
  #
  # @param [String] mounted_as
  # @return [GitlabUploader] one of the subclasses, defined at the model's uploader attribute
77
78
  def build_uploader(mounted_as = nil)
    uploader_class.new(model, mounted_as || mount_point).tap do |uploader|
79
      uploader.upload = self
80
81
82
83
84
85
86
87
88
89
    end
  end

  # Initialize the associated Uploader class with current model and
  # retrieve existing file from the store to a local cache
  #
  # @param [String] mounted_as
  # @return [GitlabUploader] one of the subclasses, defined at the model's uploader attribute
  def retrieve_uploader(mounted_as = nil)
    build_uploader(mounted_as).tap do |uploader|
90
91
92
93
      uploader.retrieve_from_store!(identifier)
    end
  end

94
95
96
  # This checks for existence of the upload on storage
  #
  # @return [Boolean] whether upload exists on storage
97
  def exist?
98
99
100
101
102
    exist = if local?
              File.exist?(absolute_path)
            else
              retrieve_uploader.exists?
            end
Toon Claes's avatar
Toon Claes committed
103
104
105
106

    # Help sysadmins find missing upload files
    if persisted? && !exist
      if Gitlab::Sentry.enabled?
107
        Raven.capture_message(_("Upload file does not exist"), extra: self.attributes)
Toon Claes's avatar
Toon Claes committed
108
109
      end

110
      Gitlab::Metrics.counter(:upload_file_does_not_exist_total, _('The number of times an upload record could not find its file')).increment
Toon Claes's avatar
Toon Claes committed
111
112
113
    end

    exist
114
115
  end

116
117
118
119
120
121
122
  def uploader_context
    {
      identifier: identifier,
      secret: secret
    }.compact
  end

123
124
125
126
  def local?
    store == ObjectStorage::Store::LOCAL
  end

127
128
129
130
131
132
133
134
135
136
  # Returns whether generating checksum is needed
  #
  # This takes into account whether file exists, if any checksum exists
  # or if the storage has checksum generation code implemented
  #
  # @return [Boolean] whether generating a checksum is needed
  def needs_checksum?
    checksum.nil? && local? && exist?
  end

137
138
  private

139
  def delete_file!
140
    retrieve_uploader.remove!
141
142
143
  end

  def foreground_checksummable?
144
    needs_checksum? && size <= CHECKSUM_THRESHOLD
145
146
147
148
149
150
151
152
153
154
  end

  def schedule_checksum
    UploadChecksumWorker.perform_async(id)
  end

  def relative_path?
    !path.start_with?('/')
  end

155
  def uploader_class
156
    Object.const_get(uploader, false)
157
158
  end

159
160
161
162
  def identifier
    File.basename(path)
  end

163
164
  def mount_point
    super&.to_sym
165
166
  end
end
167
168

Upload.prepend_if_ee('EE::Upload')