aboutsummaryrefslogtreecommitdiffstats
path: root/activestorage/lib/active_storage/service/disk_service.rb
blob: 35b09092975af7e8016e14da0750288a0cf38046 (plain) (blame)
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
require "fileutils"
require "pathname"
require "digest/md5"
require "active_support/core_ext/numeric/bytes"

# Wraps a local disk path as a Active Storage service. See `ActiveStorage::Service` for the generic API
# documentation that applies to all services.
class ActiveStorage::Service::DiskService < ActiveStorage::Service
  attr_reader :root

  def initialize(root:)
    @root = root
  end

  def upload(key, io, checksum: nil)
    instrument :upload, key, checksum: checksum do
      IO.copy_stream(io, make_path_for(key))
      ensure_integrity_of(key, checksum) if checksum
    end
  end

  def download(key)
    if block_given?
      instrument :streaming_download, key do
        File.open(path_for(key), "rb") do |file|
          while data = file.read(64.kilobytes)
            yield data
          end
        end
      end
    else
      instrument :download, key do
        File.binread path_for(key)
      end
    end
  end

  def delete(key)
    instrument :delete, key do
      begin
        File.delete path_for(key)
      rescue Errno::ENOENT
        # Ignore files already deleted
      end
    end
  end

  def exist?(key)
    instrument :exist, key do |payload|
      answer = File.exist? path_for(key)
      payload[:exist] = answer
      answer
    end
  end

  def url(key, expires_in:, disposition:, filename:, content_type:)
    instrument :url, key do |payload|
      verified_key_with_expiration = ActiveStorage.verifier.generate(key, expires_in: expires_in, purpose: :blob_key)

      generated_url =
        if defined?(Rails.application)
          Rails.application.routes.url_helpers.rails_disk_service_path \
            verified_key_with_expiration,
            disposition: disposition, filename: filename, content_type: content_type
        else
          "/rails/active_storage/disk/#{verified_key_with_expiration}/#{filename}?disposition=#{disposition}&content_type=#{content_type}"
        end

      payload[:url] = generated_url

      generated_url
    end
  end

  def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:)
    instrument :url, key do |payload|
      verified_token_with_expiration = ActiveStorage.verifier.generate(
        {
          key: key,
          content_type: content_type,
          content_length: content_length,
          checksum: checksum
        },
        expires_in: expires_in,
        purpose: :blob_token
      )

      generated_url =
        if defined?(Rails.application)
          Rails.application.routes.url_helpers.update_rails_disk_service_path verified_token_with_expiration
        else
          "/rails/active_storage/disk/#{verified_token_with_expiration}"
        end

      payload[:url] = generated_url

      generated_url
    end
  end

  def headers_for_direct_upload(key, content_type:, **)
    { "Content-Type" => content_type }
  end

  private
    def path_for(key)
      File.join root, folder_for(key), key
    end

    def folder_for(key)
      [ key[0..1], key[2..3] ].join("/")
    end

    def make_path_for(key)
      path_for(key).tap { |path| FileUtils.mkdir_p File.dirname(path) }
    end

    def ensure_integrity_of(key, checksum)
      unless Digest::MD5.file(path_for(key)).base64digest == checksum
        delete key
        raise ActiveStorage::IntegrityError
      end
    end
end