aboutsummaryrefslogtreecommitdiffstats
path: root/activestorage/lib/active_storage/service.rb
blob: aac1e62e7f1bde382903f3c38d4c8eed1f90d08d (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
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# frozen_string_literal: true

require "active_storage/log_subscriber"
require "action_dispatch"
require "action_dispatch/http/content_disposition"

module ActiveStorage
  # Abstract class serving as an interface for concrete services.
  #
  # The available services are:
  #
  # * +Disk+, to manage attachments saved directly on the hard drive.
  # * +GCS+, to manage attachments through Google Cloud Storage.
  # * +S3+, to manage attachments through Amazon S3.
  # * +AzureStorage+, to manage attachments through Microsoft Azure Storage.
  # * +Mirror+, to be able to use several services to manage attachments.
  #
  # Inside a Rails application, you can set-up your services through the
  # generated <tt>config/storage.yml</tt> file and reference one
  # of the aforementioned constant under the +service+ key. For example:
  #
  #   local:
  #     service: Disk
  #     root: <%= Rails.root.join("storage") %>
  #
  # You can checkout the service's constructor to know which keys are required.
  #
  # Then, in your application's configuration, you can specify the service to
  # use like this:
  #
  #   config.active_storage.service = :local
  #
  # If you are using Active Storage outside of a Ruby on Rails application, you
  # can configure the service to use like this:
  #
  #   ActiveStorage::Blob.service = ActiveStorage::Service.configure(
  #     :Disk,
  #     root: Pathname("/foo/bar/storage")
  #   )
  class Service
    extend ActiveSupport::Autoload
    autoload :Configurator

    class << self
      # Configure an Active Storage service by name from a set of configurations,
      # typically loaded from a YAML file. The Active Storage engine uses this
      # to set the global Active Storage service when the app boots.
      def configure(service_name, configurations)
        Configurator.build(service_name, configurations)
      end

      # Override in subclasses that stitch together multiple services and hence
      # need to build additional services using the configurator.
      #
      # Passes the configurator and all of the service's config as keyword args.
      #
      # See MirrorService for an example.
      def build(configurator:, service: nil, **service_config) #:nodoc:
        new(**service_config)
      end
    end

    # Upload the +io+ to the +key+ specified. If a +checksum+ is provided, the service will
    # ensure a match when the upload has completed or raise an ActiveStorage::IntegrityError.
    def upload(key, io, checksum: nil, **options)
      raise NotImplementedError
    end

    # Update metadata for the file identified by +key+ in the service.
    # Override in subclasses only if the service needs to store specific
    # metadata that has to be updated upon identification.
    def update_metadata(key, **metadata)
    end

    # Return the content of the file at the +key+.
    def download(key)
      raise NotImplementedError
    end

    # Return the partial content in the byte +range+ of the file at the +key+.
    def download_chunk(key, range)
      raise NotImplementedError
    end

    def open(*args, &block)
      ActiveStorage::Downloader.new(self).open(*args, &block)
    end

    # Delete the file at the +key+.
    def delete(key)
      raise NotImplementedError
    end

    # Delete files at keys starting with the +prefix+.
    def delete_prefixed(prefix)
      raise NotImplementedError
    end

    # Return +true+ if a file exists at the +key+.
    def exist?(key)
      raise NotImplementedError
    end

    # Returns a signed, temporary URL for the file at the +key+. The URL will be valid for the amount
    # of seconds specified in +expires_in+. You must also provide the +disposition+ (+:inline+ or +:attachment+),
    # +filename+, and +content_type+ that you wish the file to be served with on request.
    def url(key, expires_in:, disposition:, filename:, content_type:)
      raise NotImplementedError
    end

    # Returns a signed, temporary URL that a direct upload file can be PUT to on the +key+.
    # The URL will be valid for the amount of seconds specified in +expires_in+.
    # You must also provide the +content_type+, +content_length+, and +checksum+ of the file
    # that will be uploaded. All these attributes will be validated by the service upon upload.
    def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:)
      raise NotImplementedError
    end

    # Returns a Hash of headers for +url_for_direct_upload+ requests.
    def headers_for_direct_upload(key, filename:, content_type:, content_length:, checksum:)
      {}
    end

    private
      def instrument(operation, payload = {}, &block)
        ActiveSupport::Notifications.instrument(
          "service_#{operation}.active_storage",
          payload.merge(service: service_name), &block)
      end

      def service_name
        # ActiveStorage::Service::DiskService => Disk
        self.class.name.split("::").third.remove("Service")
      end

      def content_disposition_with(type: "inline", filename:)
        disposition = (type.to_s.presence_in(%w( attachment inline )) || "inline")
        ActionDispatch::Http::ContentDisposition.format(disposition: disposition, filename: filename.sanitized)
      end
  end
end