activestorage_legacy 0.1

Sign up to get free protection for your applications and to get access to all the features.
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