activestorage_legacy 0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. checksums.yaml +7 -0
  2. data/.babelrc +5 -0
  3. data/.codeclimate.yml +7 -0
  4. data/.eslintrc +19 -0
  5. data/.github/workflows/gem-push.yml +29 -0
  6. data/.github/workflows/ruby-tests.yml +37 -0
  7. data/.gitignore +9 -0
  8. data/.rubocop.yml +125 -0
  9. data/.travis.yml +25 -0
  10. data/Gemfile +33 -0
  11. data/Gemfile.lock +271 -0
  12. data/MIT-LICENSE +20 -0
  13. data/README.md +160 -0
  14. data/Rakefile +12 -0
  15. data/activestorage.gemspec +27 -0
  16. data/app/assets/javascripts/activestorage.js +1 -0
  17. data/app/controllers/active_storage/blobs_controller.rb +22 -0
  18. data/app/controllers/active_storage/direct_uploads_controller.rb +21 -0
  19. data/app/controllers/active_storage/disk_controller.rb +52 -0
  20. data/app/controllers/active_storage/variants_controller.rb +28 -0
  21. data/app/helpers/active_storage/file_field_with_direct_upload_helper.rb +18 -0
  22. data/app/javascript/activestorage/blob_record.js +54 -0
  23. data/app/javascript/activestorage/blob_upload.js +34 -0
  24. data/app/javascript/activestorage/direct_upload.js +42 -0
  25. data/app/javascript/activestorage/direct_upload_controller.js +67 -0
  26. data/app/javascript/activestorage/direct_uploads_controller.js +50 -0
  27. data/app/javascript/activestorage/file_checksum.js +53 -0
  28. data/app/javascript/activestorage/helpers.js +42 -0
  29. data/app/javascript/activestorage/index.js +11 -0
  30. data/app/javascript/activestorage/ujs.js +74 -0
  31. data/app/jobs/active_storage/purge_attachment_worker.rb +9 -0
  32. data/app/jobs/active_storage/purge_blob_worker.rb +9 -0
  33. data/app/models/active_storage/attachment.rb +33 -0
  34. data/app/models/active_storage/blob.rb +198 -0
  35. data/app/models/active_storage/filename.rb +49 -0
  36. data/app/models/active_storage/variant.rb +82 -0
  37. data/app/models/active_storage/variation.rb +53 -0
  38. data/config/routes.rb +9 -0
  39. data/config/storage_services.yml +34 -0
  40. data/lib/active_storage/attached/macros.rb +86 -0
  41. data/lib/active_storage/attached/many.rb +51 -0
  42. data/lib/active_storage/attached/one.rb +56 -0
  43. data/lib/active_storage/attached.rb +38 -0
  44. data/lib/active_storage/engine.rb +81 -0
  45. data/lib/active_storage/gem_version.rb +15 -0
  46. data/lib/active_storage/log_subscriber.rb +48 -0
  47. data/lib/active_storage/messages_metadata.rb +64 -0
  48. data/lib/active_storage/migration.rb +27 -0
  49. data/lib/active_storage/patches/active_record.rb +19 -0
  50. data/lib/active_storage/patches/delegation.rb +98 -0
  51. data/lib/active_storage/patches/secure_random.rb +26 -0
  52. data/lib/active_storage/patches.rb +4 -0
  53. data/lib/active_storage/service/azure_service.rb +115 -0
  54. data/lib/active_storage/service/configurator.rb +28 -0
  55. data/lib/active_storage/service/disk_service.rb +124 -0
  56. data/lib/active_storage/service/gcs_service.rb +79 -0
  57. data/lib/active_storage/service/mirror_service.rb +46 -0
  58. data/lib/active_storage/service/s3_service.rb +96 -0
  59. data/lib/active_storage/service.rb +113 -0
  60. data/lib/active_storage/verifier.rb +113 -0
  61. data/lib/active_storage/version.rb +8 -0
  62. data/lib/active_storage.rb +34 -0
  63. data/lib/tasks/activestorage.rake +20 -0
  64. data/package.json +33 -0
  65. data/test/controllers/direct_uploads_controller_test.rb +123 -0
  66. data/test/controllers/disk_controller_test.rb +57 -0
  67. data/test/controllers/variants_controller_test.rb +21 -0
  68. data/test/database/create_users_migration.rb +7 -0
  69. data/test/database/setup.rb +6 -0
  70. data/test/dummy/Rakefile +3 -0
  71. data/test/dummy/app/assets/config/manifest.js +5 -0
  72. data/test/dummy/app/assets/images/.keep +0 -0
  73. data/test/dummy/app/assets/javascripts/application.js +13 -0
  74. data/test/dummy/app/assets/stylesheets/application.css +15 -0
  75. data/test/dummy/app/controllers/application_controller.rb +3 -0
  76. data/test/dummy/app/controllers/concerns/.keep +0 -0
  77. data/test/dummy/app/helpers/application_helper.rb +2 -0
  78. data/test/dummy/app/jobs/application_job.rb +2 -0
  79. data/test/dummy/app/models/application_record.rb +3 -0
  80. data/test/dummy/app/models/concerns/.keep +0 -0
  81. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  82. data/test/dummy/bin/bundle +3 -0
  83. data/test/dummy/bin/rails +4 -0
  84. data/test/dummy/bin/rake +4 -0
  85. data/test/dummy/bin/yarn +11 -0
  86. data/test/dummy/config/application.rb +22 -0
  87. data/test/dummy/config/boot.rb +5 -0
  88. data/test/dummy/config/database.yml +25 -0
  89. data/test/dummy/config/environment.rb +5 -0
  90. data/test/dummy/config/environments/development.rb +49 -0
  91. data/test/dummy/config/environments/production.rb +82 -0
  92. data/test/dummy/config/environments/test.rb +33 -0
  93. data/test/dummy/config/initializers/application_controller_renderer.rb +6 -0
  94. data/test/dummy/config/initializers/assets.rb +14 -0
  95. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  96. data/test/dummy/config/initializers/cookies_serializer.rb +5 -0
  97. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  98. data/test/dummy/config/initializers/inflections.rb +16 -0
  99. data/test/dummy/config/initializers/mime_types.rb +4 -0
  100. data/test/dummy/config/initializers/secret_key.rb +3 -0
  101. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  102. data/test/dummy/config/routes.rb +2 -0
  103. data/test/dummy/config/secrets.yml +32 -0
  104. data/test/dummy/config/spring.rb +6 -0
  105. data/test/dummy/config/storage_services.yml +3 -0
  106. data/test/dummy/config.ru +5 -0
  107. data/test/dummy/db/.keep +0 -0
  108. data/test/dummy/lib/assets/.keep +0 -0
  109. data/test/dummy/log/.keep +0 -0
  110. data/test/dummy/package.json +5 -0
  111. data/test/dummy/public/404.html +67 -0
  112. data/test/dummy/public/422.html +67 -0
  113. data/test/dummy/public/500.html +66 -0
  114. data/test/dummy/public/apple-touch-icon-precomposed.png +0 -0
  115. data/test/dummy/public/apple-touch-icon.png +0 -0
  116. data/test/dummy/public/favicon.ico +0 -0
  117. data/test/filename_test.rb +36 -0
  118. data/test/fixtures/files/racecar.jpg +0 -0
  119. data/test/models/attachments_test.rb +122 -0
  120. data/test/models/blob_test.rb +47 -0
  121. data/test/models/variant_test.rb +27 -0
  122. data/test/service/.gitignore +1 -0
  123. data/test/service/azure_service_test.rb +14 -0
  124. data/test/service/configurations-example.yml +31 -0
  125. data/test/service/configurator_test.rb +14 -0
  126. data/test/service/disk_service_test.rb +12 -0
  127. data/test/service/gcs_service_test.rb +42 -0
  128. data/test/service/mirror_service_test.rb +62 -0
  129. data/test/service/s3_service_test.rb +52 -0
  130. data/test/service/shared_service_tests.rb +66 -0
  131. data/test/sidekiq/minitest_support.rb +6 -0
  132. data/test/support/assertions.rb +20 -0
  133. data/test/test_helper.rb +69 -0
  134. data/webpack.config.js +27 -0
  135. data/yarn.lock +3164 -0
  136. metadata +330 -0
@@ -0,0 +1,115 @@
1
+ require "active_support/core_ext/numeric/bytes"
2
+ require "azure/storage"
3
+ require "azure/storage/core/auth/shared_access_signature"
4
+
5
+ # Wraps the Microsoft Azure Storage Blob Service as a Active Storage service.
6
+ # See `ActiveStorage::Service` for the generic API documentation that applies to all services.
7
+ class ActiveStorage::Service::AzureService < ActiveStorage::Service
8
+ attr_reader :client, :path, :blobs, :container, :signer
9
+
10
+ def initialize(path:, storage_account_name:, storage_access_key:, container:)
11
+ @client = Azure::Storage::Client.create(storage_account_name: storage_account_name, storage_access_key: storage_access_key)
12
+ @signer = Azure::Storage::Core::Auth::SharedAccessSignature.new(storage_account_name, storage_access_key)
13
+ @blobs = client.blob_client
14
+ @container = container
15
+ @path = path
16
+ end
17
+
18
+ def upload(key, io, checksum: nil)
19
+ instrument :upload, key, checksum: checksum do
20
+ begin
21
+ blobs.create_block_blob(container, key, io, content_md5: checksum)
22
+ rescue Azure::Core::Http::HTTPError => e
23
+ raise ActiveStorage::IntegrityError
24
+ end
25
+ end
26
+ end
27
+
28
+ def download(key)
29
+ if block_given?
30
+ instrument :streaming_download, key do
31
+ stream(key, &block)
32
+ end
33
+ else
34
+ instrument :download, key do
35
+ _, io = blobs.get_blob(container, key)
36
+ io.force_encoding(Encoding::BINARY)
37
+ end
38
+ end
39
+ end
40
+
41
+ def delete(key)
42
+ instrument :delete, key do
43
+ begin
44
+ blobs.delete_blob(container, key)
45
+ rescue Azure::Core::Http::HTTPError
46
+ false
47
+ end
48
+ end
49
+ end
50
+
51
+ def exist?(key)
52
+ instrument :exist, key do |payload|
53
+ answer = blob_for(key).present?
54
+ payload[:exist] = answer
55
+ answer
56
+ end
57
+ end
58
+
59
+ def url(key, expires_in:, disposition:, filename:)
60
+ instrument :url, key do |payload|
61
+ base_url = url_for(key)
62
+ generated_url = signer.signed_uri(URI(base_url), false, permissions: "r",
63
+ expiry: format_expiry(expires_in), content_disposition: "#{disposition}; filename=\"#{filename}\"").to_s
64
+
65
+ payload[:url] = generated_url
66
+
67
+ generated_url
68
+ end
69
+ end
70
+
71
+ def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:)
72
+ instrument :url, key do |payload|
73
+ base_url = url_for(key)
74
+ generated_url = signer.signed_uri(URI(base_url), false, permissions: "rw",
75
+ expiry: format_expiry(expires_in)).to_s
76
+
77
+ payload[:url] = generated_url
78
+
79
+ generated_url
80
+ end
81
+ end
82
+
83
+ def headers_for_direct_upload(key, content_type:, checksum:, **)
84
+ { "Content-Type" => content_type, "Content-MD5" => checksum, "x-ms-blob-type" => "BlockBlob" }
85
+ end
86
+
87
+ private
88
+ def url_for(key)
89
+ "#{path}/#{container}/#{key}"
90
+ end
91
+
92
+ def blob_for(key)
93
+ blobs.get_blob_properties(container, key)
94
+ rescue Azure::Core::Http::HTTPError
95
+ false
96
+ end
97
+
98
+ def format_expiry(expires_in)
99
+ expires_in ? Time.now.utc.advance(seconds: expires_in).iso8601 : nil
100
+ end
101
+
102
+ # Reads the object for the given key in chunks, yielding each to the block.
103
+ def stream(key, options = {}, &block)
104
+ blob = blob_for(key)
105
+
106
+ chunk_size = 5.megabytes
107
+ offset = 0
108
+
109
+ while offset < blob.properties[:content_length]
110
+ _, io = blobs.get_blob(container, key, start_range: offset, end_range: offset + chunk_size - 1)
111
+ yield io
112
+ offset += chunk_size
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,28 @@
1
+ class ActiveStorage::Service::Configurator #:nodoc:
2
+ attr_reader :configurations
3
+
4
+ def self.build(service_name, configurations)
5
+ new(configurations).build(service_name)
6
+ end
7
+
8
+ def initialize(configurations)
9
+ @configurations = configurations.deep_symbolize_keys
10
+ end
11
+
12
+ def build(service_name)
13
+ config = config_for(service_name.to_sym)
14
+ resolve(config.fetch(:service)).build(**config, configurator: self)
15
+ end
16
+
17
+ private
18
+ def config_for(name)
19
+ configurations.fetch name do
20
+ raise "Missing configuration for the #{name.inspect} Active Storage service. Configurations available for #{configurations.keys.inspect}"
21
+ end
22
+ end
23
+
24
+ def resolve(class_name)
25
+ require "active_storage/service/#{class_name.to_s.downcase}_service"
26
+ ActiveStorage::Service.const_get(:"#{class_name}Service")
27
+ end
28
+ end
@@ -0,0 +1,124 @@
1
+ require "fileutils"
2
+ require "pathname"
3
+ require "digest/md5"
4
+ require "active_support/core_ext/numeric/bytes"
5
+
6
+ # Wraps a local disk path as a Active Storage service. See `ActiveStorage::Service` for the generic API
7
+ # documentation that applies to all services.
8
+ class ActiveStorage::Service::DiskService < ActiveStorage::Service
9
+ attr_reader :root
10
+
11
+ def initialize(root:)
12
+ @root = root
13
+ end
14
+
15
+ def upload(key, io, checksum: nil)
16
+ instrument :upload, key, checksum: checksum do
17
+ IO.copy_stream(io, make_path_for(key))
18
+ ensure_integrity_of(key, checksum) if checksum
19
+ end
20
+ end
21
+
22
+ def download(key)
23
+ if block_given?
24
+ instrument :streaming_download, key do
25
+ File.open(path_for(key), "rb") do |file|
26
+ while data = file.read(64.kilobytes)
27
+ yield data
28
+ end
29
+ end
30
+ end
31
+ else
32
+ instrument :download, key do
33
+ File.binread path_for(key)
34
+ end
35
+ end
36
+ end
37
+
38
+ def delete(key)
39
+ instrument :delete, key do
40
+ begin
41
+ File.delete path_for(key)
42
+ rescue Errno::ENOENT
43
+ # Ignore files already deleted
44
+ end
45
+ end
46
+ end
47
+
48
+ def exist?(key)
49
+ instrument :exist, key do |payload|
50
+ answer = File.exist? path_for(key)
51
+ payload[:exist] = answer
52
+ answer
53
+ end
54
+ end
55
+
56
+ def url(key, expires_in:, disposition:, filename:, content_type:)
57
+ instrument :url, key do |payload|
58
+ verified_key_with_expiration = ActiveStorage.verifier.generate(key, expires_in: expires_in, purpose: :blob_key)
59
+
60
+ generated_url =
61
+ if defined?(Rails.application)
62
+ Rails.application.routes.url_helpers.rails_disk_service_path \
63
+ verified_key_with_expiration,
64
+ disposition: disposition, filename: filename, content_type: content_type
65
+ else
66
+ "/rails/active_storage/disk/#{verified_key_with_expiration}/#{filename}?disposition=#{disposition}&content_type=#{content_type}"
67
+ end
68
+
69
+ payload[:url] = generated_url
70
+
71
+ generated_url
72
+ end
73
+ end
74
+
75
+ def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:)
76
+ instrument :url, key do |payload|
77
+ verified_token_with_expiration = ActiveStorage.verifier.generate(
78
+ {
79
+ key: key,
80
+ content_type: content_type,
81
+ content_length: content_length,
82
+ checksum: checksum
83
+ },
84
+ expires_in: expires_in,
85
+ purpose: :blob_token
86
+ )
87
+
88
+ generated_url =
89
+ if defined?(Rails.application)
90
+ Rails.application.routes.url_helpers.update_rails_disk_service_path verified_token_with_expiration
91
+ else
92
+ "/rails/active_storage/disk/#{verified_token_with_expiration}"
93
+ end
94
+
95
+ payload[:url] = generated_url
96
+
97
+ generated_url
98
+ end
99
+ end
100
+
101
+ def headers_for_direct_upload(key, content_type:, **)
102
+ { "Content-Type" => content_type }
103
+ end
104
+
105
+ private
106
+ def path_for(key)
107
+ File.join root, folder_for(key), key
108
+ end
109
+
110
+ def folder_for(key)
111
+ [ key[0..1], key[2..3] ].join("/")
112
+ end
113
+
114
+ def make_path_for(key)
115
+ path_for(key).tap { |path| FileUtils.mkdir_p File.dirname(path) }
116
+ end
117
+
118
+ def ensure_integrity_of(key, checksum)
119
+ unless Digest::MD5.file(path_for(key)).base64digest == checksum
120
+ delete key
121
+ raise ActiveStorage::IntegrityError
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,79 @@
1
+ require "google/cloud/storage"
2
+ require "active_support/core_ext/object/to_query"
3
+
4
+ # Wraps the Google Cloud Storage as a Active Storage service. See `ActiveStorage::Service` for the generic API
5
+ # documentation that applies to all services.
6
+ class ActiveStorage::Service::GCSService < ActiveStorage::Service
7
+ attr_reader :client, :bucket
8
+
9
+ def initialize(project:, keyfile:, bucket:)
10
+ @client = Google::Cloud::Storage.new(project: project, keyfile: keyfile)
11
+ @bucket = @client.bucket(bucket)
12
+ end
13
+
14
+ def upload(key, io, checksum: nil)
15
+ instrument :upload, key, checksum: checksum do
16
+ begin
17
+ bucket.create_file(io, key, md5: checksum)
18
+ rescue Google::Cloud::InvalidArgumentError
19
+ raise ActiveStorage::IntegrityError
20
+ end
21
+ end
22
+ end
23
+
24
+ # FIXME: Add streaming when given a block
25
+ def download(key)
26
+ instrument :download, key do
27
+ io = file_for(key).download
28
+ io.rewind
29
+ io.read
30
+ end
31
+ end
32
+
33
+ def delete(key)
34
+ instrument :delete, key do
35
+ file_for(key).try(:delete)
36
+ end
37
+ end
38
+
39
+ def exist?(key)
40
+ instrument :exist, key do |payload|
41
+ answer = file_for(key).present?
42
+ payload[:exist] = answer
43
+ answer
44
+ end
45
+ end
46
+
47
+ def url(key, expires_in:, disposition:, filename:, content_type:)
48
+ instrument :url, key do |payload|
49
+ generated_url = file_for(key).signed_url expires: expires_in, query: {
50
+ "response-content-disposition" => "#{disposition}; filename=\"#{filename}\"",
51
+ "response-content-type" => content_type
52
+ }
53
+
54
+ payload[:url] = generated_url
55
+
56
+ generated_url
57
+ end
58
+ end
59
+
60
+ def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:)
61
+ instrument :url, key do |payload|
62
+ generated_url = bucket.signed_url key, method: "PUT", expires: expires_in,
63
+ content_type: content_type, content_md5: checksum
64
+
65
+ payload[:url] = generated_url
66
+
67
+ generated_url
68
+ end
69
+ end
70
+
71
+ def headers_for_direct_upload(key, content_type:, checksum:, **)
72
+ { "Content-Type" => content_type, "Content-MD5" => checksum }
73
+ end
74
+
75
+ private
76
+ def file_for(key)
77
+ bucket.file(key)
78
+ end
79
+ end
@@ -0,0 +1,46 @@
1
+ require "active_storage/patches/delegation"
2
+
3
+ # Wraps a set of mirror services and provides a single `ActiveStorage::Service` object that will all
4
+ # have the files uploaded to them. A `primary` service is designated to answer calls to `download`, `exists?`,
5
+ # and `url`.
6
+ class ActiveStorage::Service::MirrorService < ActiveStorage::Service
7
+ attr_reader :primary, :mirrors
8
+
9
+ delegate :download, :exist?, :url, to: :primary
10
+
11
+ # Stitch together from named services.
12
+ def self.build(primary:, mirrors:, configurator:, **options) #:nodoc:
13
+ new \
14
+ primary: configurator.build(primary),
15
+ mirrors: mirrors.collect { |name| configurator.build name }
16
+ end
17
+
18
+ def initialize(primary:, mirrors:)
19
+ @primary, @mirrors = primary, mirrors
20
+ end
21
+
22
+ # Upload the `io` to the `key` specified to all services. If a `checksum` is provided, all services will
23
+ # ensure a match when the upload has completed or raise an `ActiveStorage::IntegrityError`.
24
+ def upload(key, io, checksum: nil)
25
+ each_service.collect do |service|
26
+ service.upload key, io.tap(&:rewind), checksum: checksum
27
+ end
28
+ end
29
+
30
+ # Delete the file at the `key` on all services.
31
+ def delete(key)
32
+ perform_across_services :delete, key
33
+ end
34
+
35
+ private
36
+ def each_service(&block)
37
+ [ primary, *mirrors ].each(&block)
38
+ end
39
+
40
+ def perform_across_services(method, *args)
41
+ # FIXME: Convert to be threaded
42
+ each_service.collect do |service|
43
+ service.public_send method, *args
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,96 @@
1
+ require "aws-sdk"
2
+ require "active_support/core_ext/numeric/bytes"
3
+
4
+ # Wraps the Amazon Simple Storage Service (S3) as a Active Storage service.
5
+ # See `ActiveStorage::Service` for the generic API documentation that applies to all services.
6
+ class ActiveStorage::Service::S3Service < ActiveStorage::Service
7
+ attr_reader :client, :bucket, :upload_options
8
+
9
+ def initialize(access_key_id:, secret_access_key:, region:, bucket:, upload: {}, **options)
10
+ @client = Aws::S3::Resource.new(access_key_id: access_key_id, secret_access_key: secret_access_key, region: region, **options)
11
+ @bucket = @client.bucket(bucket)
12
+
13
+ @upload_options = upload
14
+ end
15
+
16
+ def upload(key, io, checksum: nil)
17
+ instrument :upload, key, checksum: checksum do
18
+ begin
19
+ object_for(key).put(upload_options.merge(body: io, content_md5: checksum))
20
+ rescue Aws::S3::Errors::BadDigest
21
+ raise ActiveStorage::IntegrityError
22
+ end
23
+ end
24
+ end
25
+
26
+ def download(key)
27
+ if block_given?
28
+ instrument :streaming_download, key do
29
+ stream(key, &block)
30
+ end
31
+ else
32
+ instrument :download, key do
33
+ object_for(key).get.body.read.force_encoding(Encoding::BINARY)
34
+ end
35
+ end
36
+ end
37
+
38
+ def delete(key)
39
+ instrument :delete, key do
40
+ object_for(key).delete
41
+ end
42
+ end
43
+
44
+ def exist?(key)
45
+ instrument :exist, key do |payload|
46
+ answer = object_for(key).exists?
47
+ payload[:exist] = answer
48
+ answer
49
+ end
50
+ end
51
+
52
+ def url(key, expires_in:, disposition:, filename:, content_type:)
53
+ instrument :url, key do |payload|
54
+ generated_url = object_for(key).presigned_url :get, expires_in: expires_in,
55
+ response_content_disposition: "#{disposition}; filename=\"#{filename}\"",
56
+ response_content_type: content_type
57
+
58
+ payload[:url] = generated_url
59
+
60
+ generated_url
61
+ end
62
+ end
63
+
64
+ def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:)
65
+ instrument :url, key do |payload|
66
+ generated_url = object_for(key).presigned_url :put, expires_in: expires_in,
67
+ content_type: content_type, content_length: content_length, content_md5: checksum
68
+
69
+ payload[:url] = generated_url
70
+
71
+ generated_url
72
+ end
73
+ end
74
+
75
+ def headers_for_direct_upload(key, content_type:, checksum:, **)
76
+ { "Content-Type" => content_type, "Content-MD5" => checksum }
77
+ end
78
+
79
+ private
80
+ def object_for(key)
81
+ bucket.object(key)
82
+ end
83
+
84
+ # Reads the object for the given key in chunks, yielding each to the block.
85
+ def stream(key, options = {}, &block)
86
+ object = object_for(key)
87
+
88
+ chunk_size = 5.megabytes
89
+ offset = 0
90
+
91
+ while offset < object.content_length
92
+ yield object.read(options.merge(range: "bytes=#{offset}-#{offset + chunk_size - 1}"))
93
+ offset += chunk_size
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,113 @@
1
+ require "active_storage/log_subscriber"
2
+
3
+ # Abstract class serving as an interface for concrete services.
4
+ #
5
+ # The available services are:
6
+ #
7
+ # * +Disk+, to manage attachments saved directly on the hard drive.
8
+ # * +GCS+, to manage attachments through Google Cloud Storage.
9
+ # * +S3+, to manage attachments through Amazon S3.
10
+ # * +Mirror+, to be able to use several services to manage attachments.
11
+ #
12
+ # Inside a Rails application, you can set-up your services through the
13
+ # generated <tt>config/storage_services.yml</tt> file and reference one
14
+ # of the aforementioned constant under the +service+ key. For example:
15
+ #
16
+ # local:
17
+ # service: Disk
18
+ # root: <%= Rails.root.join("storage") %>
19
+ #
20
+ # You can checkout the service's constructor to know which keys are required.
21
+ #
22
+ # Then, in your application's configuration, you can specify the service to
23
+ # use like this:
24
+ #
25
+ # config.active_storage.service = :local
26
+ #
27
+ # If you are using Active Storage outside of a Ruby on Rails application, you
28
+ # can configure the service to use like this:
29
+ #
30
+ # ActiveStorage::Blob.service = ActiveStorage::Service.configure(
31
+ # :Disk,
32
+ # root: Pathname("/foo/bar/storage")
33
+ # )
34
+ class ActiveStorage::Service
35
+ class ActiveStorage::IntegrityError < StandardError; end
36
+
37
+ extend ActiveSupport::Autoload
38
+ autoload :Configurator
39
+
40
+ class_attribute :logger
41
+
42
+ class << self
43
+ # Configure an Active Storage service by name from a set of configurations,
44
+ # typically loaded from a YAML file. The Active Storage engine uses this
45
+ # to set the global Active Storage service when the app boots.
46
+ def configure(service_name, configurations)
47
+ Configurator.build(service_name, configurations)
48
+ end
49
+
50
+ # Override in subclasses that stitch together multiple services and hence
51
+ # need to build additional services using the configurator.
52
+ #
53
+ # Passes the configurator and all of the service's config as keyword args.
54
+ #
55
+ # See MirrorService for an example.
56
+ def build(configurator:, service: nil, **service_config) #:nodoc:
57
+ new(**service_config)
58
+ end
59
+ end
60
+
61
+ # Upload the `io` to the `key` specified. If a `checksum` is provided, the service will
62
+ # ensure a match when the upload has completed or raise an `ActiveStorage::IntegrityError`.
63
+ def upload(key, io, checksum: nil)
64
+ raise NotImplementedError
65
+ end
66
+
67
+ # Return the content of the file at the `key`.
68
+ def download(key)
69
+ raise NotImplementedError
70
+ end
71
+
72
+ # Delete the file at the `key`.
73
+ def delete(key)
74
+ raise NotImplementedError
75
+ end
76
+
77
+ # Return true if a file exists at the `key`.
78
+ def exist?(key)
79
+ raise NotImplementedError
80
+ end
81
+
82
+ # Returns a signed, temporary URL for the file at the `key`. The URL will be valid for the amount
83
+ # of seconds specified in `expires_in`. You most also provide the `disposition` (`:inline` or `:attachment`),
84
+ # `filename`, and `content_type` that you wish the file to be served with on request.
85
+ def url(key, expires_in:, disposition:, filename:, content_type:)
86
+ raise NotImplementedError
87
+ end
88
+
89
+ # Returns a signed, temporary URL that a direct upload file can be PUT to on the `key`.
90
+ # The URL will be valid for the amount of seconds specified in `expires_in`.
91
+ # You most also provide the `content_type`, `content_length`, and `checksum` of the file
92
+ # that will be uploaded. All these attributes will be validated by the service upon upload.
93
+ def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:)
94
+ raise NotImplementedError
95
+ end
96
+
97
+ # Returns a Hash of headers for `url_for_direct_upload` requests.
98
+ def headers_for_direct_upload(key, filename:, content_type:, content_length:, checksum:)
99
+ {}
100
+ end
101
+
102
+ private
103
+ def instrument(operation, key, payload = {}, &block)
104
+ ActiveSupport::Notifications.instrument(
105
+ "service_#{operation}.active_storage",
106
+ payload.merge(key: key, service: service_name), &block)
107
+ end
108
+
109
+ def service_name
110
+ # ActiveStorage::Service::DiskService => Disk
111
+ self.class.name.split("::").third.gsub!('Service', '')
112
+ end
113
+ end