summaryrefslogtreecommitdiff
path: root/app/models/upload.rb
blob: 9bffdcdb2e7c3ca5dff8dee31164451f571283c2 (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
# frozen_string_literal: true

class Upload < ApplicationRecord
  # Upper limit for foreground checksum processing
  CHECKSUM_THRESHOLD = 100.megabytes

  belongs_to :model, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations

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

  scope :with_files_stored_locally, -> { where(store: ObjectStorage::Store::LOCAL) }
  scope :with_files_stored_remotely, -> { where(store: ObjectStorage::Store::REMOTE) }

  before_save  :calculate_checksum!, if: :foreground_checksummable?
  after_commit :schedule_checksum,   if: :checksummable?

  # as the FileUploader is not mounted, the default CarrierWave ActiveRecord
  # hooks are not executed and the file will not be deleted
  after_destroy :delete_file!, if: -> { uploader_class <= FileUploader }

  def self.hexdigest(path)
    Digest::SHA256.file(path).hexdigest
  end

  class << self
    ##
    # 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

  def absolute_path
    raise ObjectStorage::RemoteStoreError, "Remote object has no absolute path." unless local?
    return path unless relative_path?

    uploader_class.absolute_path(self)
  end

  def calculate_checksum!
    self.checksum = nil
    return unless checksummable?

    self.checksum = Digest::SHA256.file(absolute_path).hexdigest
  end

  def build_uploader(mounted_as = nil)
    uploader_class.new(model, mounted_as || mount_point).tap do |uploader|
      uploader.upload = self
      uploader.retrieve_from_store!(identifier)
    end
  end

  def exist?
    exist = File.exist?(absolute_path)

    # Help sysadmins find missing upload files
    if persisted? && !exist
      if Gitlab::Sentry.enabled?
        Raven.capture_message("Upload file does not exist", extra: self.attributes)
      end

      Gitlab::Metrics.counter(:upload_file_does_not_exist_total, 'The number of times an upload record could not find its file').increment
    end

    exist
  end

  def uploader_context
    {
      identifier: identifier,
      secret: secret
    }.compact
  end

  def local?
    store == ObjectStorage::Store::LOCAL
  end

  private

  def delete_file!
    build_uploader.remove!
  end

  def checksummable?
    checksum.nil? && local? && exist?
  end

  def foreground_checksummable?
    checksummable? && size <= CHECKSUM_THRESHOLD
  end

  def schedule_checksum
    UploadChecksumWorker.perform_async(id)
  end

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

  def uploader_class
    Object.const_get(uploader)
  end

  def identifier
    File.basename(path)
  end

  def mount_point
    super&.to_sym
  end
end