Class: ActiveStorage::Service::DiskService

Inherits:
Service
  • Object
show all
Defined in:
lib/active_storage/service/disk_service.rb

Overview

Active Storage Disk Service

Wraps a local disk path as an Active Storage service. See ActiveStorage::Service for the generic API documentation that applies to all services.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(root:, public: false, **options) ⇒ DiskService

Returns a new instance of DiskService.



16
17
18
19
# File 'lib/active_storage/service/disk_service.rb', line 16

def initialize(root:, public: false, **options)
  @root = root
  @public = public
end

Instance Attribute Details

#rootObject

Returns the value of attribute root.



14
15
16
# File 'lib/active_storage/service/disk_service.rb', line 14

def root
  @root
end

Instance Method Details

#compose(source_keys, destination_key) ⇒ Object



145
146
147
148
149
150
151
152
153
# File 'lib/active_storage/service/disk_service.rb', line 145

def compose(source_keys, destination_key, **)
  File.open(make_path_for(destination_key), "w") do |destination_file|
    source_keys.each do |source_key|
      File.open(path_for(source_key), "rb") do |source_file|
        IO.copy_stream(source_file, destination_file)
      end
    end
  end
end

#delete(key) ⇒ Object



53
54
55
56
57
58
59
# File 'lib/active_storage/service/disk_service.rb', line 53

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

#delete_prefixed(prefix) ⇒ Object



61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/active_storage/service/disk_service.rb', line 61

def delete_prefixed(prefix)
  instrument :delete_prefixed, prefix: prefix do
    prefix_path = path_for(prefix)

    # File.expand_path (called within path_for) strips trailing slashes.
    # Restore trailing separator if the original prefix had one, so that
    # the glob "prefix/*" matches files inside the directory, not siblings
    # whose names start with the prefix string.
    prefix_path += "/" if prefix.end_with?("/")

    escaped = escape_glob_metacharacters(prefix_path)
    Dir.glob("#{escaped}*").each do |path|
      FileUtils.rm_rf(path)
    end
  end
end

#download(key, &block) ⇒ Object



28
29
30
31
32
33
34
35
36
37
38
39
40
# File 'lib/active_storage/service/disk_service.rb', line 28

def download(key, &block)
  if block_given?
    instrument :streaming_download, key: key do
      stream key, &block
    end
  else
    instrument :download, key: key do
      File.binread path_for(key)
    rescue Errno::ENOENT
      raise ActiveStorage::FileNotFoundError
    end
  end
end

#download_chunk(key, range) ⇒ Object



42
43
44
45
46
47
48
49
50
51
# File 'lib/active_storage/service/disk_service.rb', line 42

def download_chunk(key, range)
  instrument :download_chunk, key: key, range: range do
    File.open(path_for(key), "rb") do |file|
      file.seek range.begin
      file.read range.size
    end
  rescue Errno::ENOENT
    raise ActiveStorage::FileNotFoundError
  end
end

#exist?(key) ⇒ Boolean

Returns:

  • (Boolean)


78
79
80
81
82
83
84
# File 'lib/active_storage/service/disk_service.rb', line 78

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

#headers_for_direct_upload(key, content_type:) ⇒ Object



106
107
108
# File 'lib/active_storage/service/disk_service.rb', line 106

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

#path_for(key) ⇒ Object

Every filesystem operation in DiskService resolves paths through this method (or through make_path_for, which delegates here). This is the primary filesystem security check: all path-traversal protection is enforced here. New methods that touch the filesystem MUST use path_for or make_path_for -- never construct paths from root directly.



114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# File 'lib/active_storage/service/disk_service.rb', line 114

def path_for(key) # :nodoc:
  if key.blank?
    raise ActiveStorage::InvalidKeyError, "key is blank"
  end

  # Reject keys with dot segments as defense in depth. This prevents path traversal both outside
  # and within the storage root. The root containment check below is a more fundamental check on
  # path traversal outside of the disk service root.
  begin
    if key.split("/").intersect?(%w[. ..])
      raise ActiveStorage::InvalidKeyError, "key has path traversal segments"
    end
  rescue Encoding::CompatibilityError
    raise ActiveStorage::InvalidKeyError, "key has incompatible encoding"
  end

  begin
    path = File.expand_path(File.join(root, folder_for(key), key))
  rescue ArgumentError
    # ArgumentError catches null bytes
    raise ActiveStorage::InvalidKeyError, "key is an invalid string"
  end

  # The resolved path must be inside the root directory.
  unless path.start_with?(File.expand_path(root) + "/")
    raise ActiveStorage::InvalidKeyError, "key is outside of disk service root"
  end

  path
end

#upload(key, io, checksum: nil) ⇒ Object



21
22
23
24
25
26
# File 'lib/active_storage/service/disk_service.rb', line 21

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

#url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:, custom_metadata: {}) ⇒ Object



86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
# File 'lib/active_storage/service/disk_service.rb', line 86

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

    url_helpers.update_rails_disk_service_url(verified_token_with_expiration, url_options).tap do |generated_url|
      payload[:url] = generated_url
    end
  end
end