activestorage 0.1 → 5.2.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of activestorage might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.md +5 -0
- data/README.md +94 -25
- data/app/assets/javascripts/activestorage.js +1 -0
- data/app/controllers/active_storage/blobs_controller.rb +16 -0
- data/app/controllers/active_storage/direct_uploads_controller.rb +23 -0
- data/app/controllers/active_storage/disk_controller.rb +51 -0
- data/app/controllers/active_storage/previews_controller.rb +12 -0
- data/app/controllers/active_storage/variants_controller.rb +16 -0
- data/app/javascript/activestorage/blob_record.js +54 -0
- data/app/javascript/activestorage/blob_upload.js +35 -0
- data/app/javascript/activestorage/direct_upload.js +42 -0
- data/app/javascript/activestorage/direct_upload_controller.js +67 -0
- data/app/javascript/activestorage/direct_uploads_controller.js +50 -0
- data/app/javascript/activestorage/file_checksum.js +53 -0
- data/app/javascript/activestorage/helpers.js +42 -0
- data/app/javascript/activestorage/index.js +11 -0
- data/app/javascript/activestorage/ujs.js +75 -0
- data/app/jobs/active_storage/analyze_job.rb +8 -0
- data/app/jobs/active_storage/base_job.rb +5 -0
- data/app/jobs/active_storage/purge_job.rb +11 -0
- data/app/models/active_storage/attachment.rb +35 -0
- data/app/models/active_storage/blob.rb +313 -0
- data/app/models/active_storage/filename.rb +73 -0
- data/app/models/active_storage/filename/parameters.rb +36 -0
- data/app/models/active_storage/preview.rb +90 -0
- data/app/models/active_storage/variant.rb +86 -0
- data/app/models/active_storage/variation.rb +67 -0
- data/config/routes.rb +43 -0
- data/lib/active_storage.rb +37 -2
- data/lib/active_storage/analyzer.rb +33 -0
- data/lib/active_storage/analyzer/image_analyzer.rb +36 -0
- data/lib/active_storage/analyzer/null_analyzer.rb +13 -0
- data/lib/active_storage/analyzer/video_analyzer.rb +79 -0
- data/lib/active_storage/attached.rb +28 -22
- data/lib/active_storage/attached/macros.rb +89 -16
- data/lib/active_storage/attached/many.rb +53 -21
- data/lib/active_storage/attached/one.rb +74 -20
- data/lib/active_storage/downloading.rb +26 -0
- data/lib/active_storage/engine.rb +72 -0
- data/lib/active_storage/gem_version.rb +17 -0
- data/lib/active_storage/log_subscriber.rb +52 -0
- data/lib/active_storage/previewer.rb +58 -0
- data/lib/active_storage/previewer/pdf_previewer.rb +17 -0
- data/lib/active_storage/previewer/video_previewer.rb +23 -0
- data/lib/active_storage/service.rb +112 -24
- data/lib/active_storage/service/azure_storage_service.rb +124 -0
- data/lib/active_storage/service/configurator.rb +32 -0
- data/lib/active_storage/service/disk_service.rb +103 -44
- data/lib/active_storage/service/gcs_service.rb +87 -29
- data/lib/active_storage/service/mirror_service.rb +38 -22
- data/lib/active_storage/service/s3_service.rb +83 -38
- data/lib/active_storage/version.rb +10 -0
- data/lib/tasks/activestorage.rake +4 -15
- metadata +64 -108
- data/.gitignore +0 -1
- data/Gemfile +0 -11
- data/Gemfile.lock +0 -235
- data/Rakefile +0 -11
- data/activestorage.gemspec +0 -21
- data/lib/active_storage/attachment.rb +0 -30
- data/lib/active_storage/blob.rb +0 -80
- data/lib/active_storage/disk_controller.rb +0 -28
- data/lib/active_storage/download.rb +0 -90
- data/lib/active_storage/filename.rb +0 -31
- data/lib/active_storage/migration.rb +0 -28
- data/lib/active_storage/purge_job.rb +0 -10
- data/lib/active_storage/railtie.rb +0 -56
- data/lib/active_storage/storage_services.yml +0 -27
- data/lib/active_storage/verified_key_with_expiration.rb +0 -24
- data/test/attachments_test.rb +0 -95
- data/test/blob_test.rb +0 -28
- data/test/database/create_users_migration.rb +0 -7
- data/test/database/setup.rb +0 -6
- data/test/disk_controller_test.rb +0 -34
- data/test/filename_test.rb +0 -36
- data/test/service/.gitignore +0 -1
- data/test/service/configurations-example.yml +0 -11
- data/test/service/disk_service_test.rb +0 -8
- data/test/service/gcs_service_test.rb +0 -20
- data/test/service/mirror_service_test.rb +0 -50
- data/test/service/s3_service_test.rb +0 -11
- data/test/service/shared_service_tests.rb +0 -68
- data/test/test_helper.rb +0 -28
- data/test/verified_key_with_expiration_test.rb +0 -19
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveStorage
|
4
|
+
module Downloading
|
5
|
+
private
|
6
|
+
# Opens a new tempfile in #tempdir and copies blob data into it. Yields the tempfile.
|
7
|
+
def download_blob_to_tempfile # :doc:
|
8
|
+
Tempfile.open("ActiveStorage", tempdir) do |file|
|
9
|
+
download_blob_to file
|
10
|
+
yield file
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# Efficiently downloads blob data into the given file.
|
15
|
+
def download_blob_to(file) # :doc:
|
16
|
+
file.binmode
|
17
|
+
blob.download { |chunk| file.write(chunk) }
|
18
|
+
file.rewind
|
19
|
+
end
|
20
|
+
|
21
|
+
# Returns the directory in which tempfiles should be opened. Defaults to +Dir.tmpdir+.
|
22
|
+
def tempdir # :doc:
|
23
|
+
Dir.tmpdir
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rails"
|
4
|
+
require "active_storage"
|
5
|
+
|
6
|
+
require "active_storage/previewer/pdf_previewer"
|
7
|
+
require "active_storage/previewer/video_previewer"
|
8
|
+
|
9
|
+
require "active_storage/analyzer/image_analyzer"
|
10
|
+
require "active_storage/analyzer/video_analyzer"
|
11
|
+
|
12
|
+
module ActiveStorage
|
13
|
+
class Engine < Rails::Engine # :nodoc:
|
14
|
+
isolate_namespace ActiveStorage
|
15
|
+
|
16
|
+
config.active_storage = ActiveSupport::OrderedOptions.new
|
17
|
+
config.active_storage.previewers = [ ActiveStorage::Previewer::PDFPreviewer, ActiveStorage::Previewer::VideoPreviewer ]
|
18
|
+
config.active_storage.analyzers = [ ActiveStorage::Analyzer::ImageAnalyzer, ActiveStorage::Analyzer::VideoAnalyzer ]
|
19
|
+
|
20
|
+
config.eager_load_namespaces << ActiveStorage
|
21
|
+
|
22
|
+
initializer "active_storage.configs" do
|
23
|
+
config.after_initialize do |app|
|
24
|
+
ActiveStorage.logger = app.config.active_storage.logger || Rails.logger
|
25
|
+
ActiveStorage.queue = app.config.active_storage.queue
|
26
|
+
ActiveStorage.previewers = app.config.active_storage.previewers || []
|
27
|
+
ActiveStorage.analyzers = app.config.active_storage.analyzers || []
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
initializer "active_storage.attached" do
|
32
|
+
require "active_storage/attached"
|
33
|
+
|
34
|
+
ActiveSupport.on_load(:active_record) do
|
35
|
+
extend ActiveStorage::Attached::Macros
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
initializer "active_storage.verifier" do
|
40
|
+
config.after_initialize do |app|
|
41
|
+
ActiveStorage.verifier = app.message_verifier("ActiveStorage")
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
initializer "active_storage.services" do
|
46
|
+
config.to_prepare do
|
47
|
+
if config_choice = Rails.configuration.active_storage.service
|
48
|
+
configs = Rails.configuration.active_storage.service_configurations ||= begin
|
49
|
+
config_file = Pathname.new(Rails.root.join("config/storage.yml"))
|
50
|
+
raise("Couldn't find Active Storage configuration in #{config_file}") unless config_file.exist?
|
51
|
+
|
52
|
+
require "yaml"
|
53
|
+
require "erb"
|
54
|
+
|
55
|
+
YAML.load(ERB.new(config_file.read).result) || {}
|
56
|
+
rescue Psych::SyntaxError => e
|
57
|
+
raise "YAML syntax error occurred while parsing #{config_file}. " \
|
58
|
+
"Please note that YAML must be consistently indented using spaces. Tabs are not allowed. " \
|
59
|
+
"Error: #{e.message}"
|
60
|
+
end
|
61
|
+
|
62
|
+
ActiveStorage::Blob.service =
|
63
|
+
begin
|
64
|
+
ActiveStorage::Service.configure config_choice, configs
|
65
|
+
rescue => e
|
66
|
+
raise e, "Cannot load `Rails.config.active_storage.service`:\n#{e.message}", e.backtrace
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveStorage
|
4
|
+
# Returns the version of the currently loaded Active Storage as a <tt>Gem::Version</tt>.
|
5
|
+
def self.gem_version
|
6
|
+
Gem::Version.new VERSION::STRING
|
7
|
+
end
|
8
|
+
|
9
|
+
module VERSION
|
10
|
+
MAJOR = 5
|
11
|
+
MINOR = 2
|
12
|
+
TINY = 0
|
13
|
+
PRE = "beta1"
|
14
|
+
|
15
|
+
STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/log_subscriber"
|
4
|
+
|
5
|
+
module ActiveStorage
|
6
|
+
class LogSubscriber < ActiveSupport::LogSubscriber
|
7
|
+
def service_upload(event)
|
8
|
+
message = "Uploaded file to key: #{key_in(event)}"
|
9
|
+
message += " (checksum: #{event.payload[:checksum]})" if event.payload[:checksum]
|
10
|
+
info event, color(message, GREEN)
|
11
|
+
end
|
12
|
+
|
13
|
+
def service_download(event)
|
14
|
+
info event, color("Downloaded file from key: #{key_in(event)}", BLUE)
|
15
|
+
end
|
16
|
+
|
17
|
+
def service_delete(event)
|
18
|
+
info event, color("Deleted file from key: #{key_in(event)}", RED)
|
19
|
+
end
|
20
|
+
|
21
|
+
def service_exist(event)
|
22
|
+
debug event, color("Checked if file exists at key: #{key_in(event)} (#{event.payload[:exist] ? "yes" : "no"})", BLUE)
|
23
|
+
end
|
24
|
+
|
25
|
+
def service_url(event)
|
26
|
+
debug event, color("Generated URL for file at key: #{key_in(event)} (#{event.payload[:url]})", BLUE)
|
27
|
+
end
|
28
|
+
|
29
|
+
def logger
|
30
|
+
ActiveStorage.logger
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
def info(event, colored_message)
|
35
|
+
super log_prefix_for_service(event) + colored_message
|
36
|
+
end
|
37
|
+
|
38
|
+
def debug(event, colored_message)
|
39
|
+
super log_prefix_for_service(event) + colored_message
|
40
|
+
end
|
41
|
+
|
42
|
+
def log_prefix_for_service(event)
|
43
|
+
color " #{event.payload[:service]} Storage (#{event.duration.round(1)}ms) ", CYAN
|
44
|
+
end
|
45
|
+
|
46
|
+
def key_in(event)
|
47
|
+
event.payload[:key]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
ActiveStorage::LogSubscriber.attach_to :active_storage
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_storage/downloading"
|
4
|
+
|
5
|
+
module ActiveStorage
|
6
|
+
# This is an abstract base class for previewers, which generate images from blobs. See
|
7
|
+
# ActiveStorage::Previewer::PDFPreviewer and ActiveStorage::Previewer::VideoPreviewer for examples of
|
8
|
+
# concrete subclasses.
|
9
|
+
class Previewer
|
10
|
+
include Downloading
|
11
|
+
|
12
|
+
attr_reader :blob
|
13
|
+
|
14
|
+
# Implement this method in a concrete subclass. Have it return true when given a blob from which
|
15
|
+
# the previewer can generate an image.
|
16
|
+
def self.accept?(blob)
|
17
|
+
false
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize(blob)
|
21
|
+
@blob = blob
|
22
|
+
end
|
23
|
+
|
24
|
+
# Override this method in a concrete subclass. Have it yield an attachable preview image (i.e.
|
25
|
+
# anything accepted by ActiveStorage::Attached::One#attach).
|
26
|
+
def preview
|
27
|
+
raise NotImplementedError
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
# Executes a system command, capturing its binary output in a tempfile. Yields the tempfile.
|
32
|
+
#
|
33
|
+
# Use this method to shell out to a system library (e.g. mupdf or ffmpeg) for preview image
|
34
|
+
# generation. The resulting tempfile can be used as the +:io+ value in an attachable Hash:
|
35
|
+
#
|
36
|
+
# def preview
|
37
|
+
# download_blob_to_tempfile do |input|
|
38
|
+
# draw "my-drawing-command", input.path, "--format", "png", "-" do |output|
|
39
|
+
# yield io: output, filename: "#{blob.filename.base}.png", content_type: "image/png"
|
40
|
+
# end
|
41
|
+
# end
|
42
|
+
# end
|
43
|
+
#
|
44
|
+
# The output tempfile is opened in the directory returned by ActiveStorage::Downloading#tempdir.
|
45
|
+
def draw(*argv) # :doc:
|
46
|
+
Tempfile.open("ActiveStorage", tempdir) do |file|
|
47
|
+
capture(*argv, to: file)
|
48
|
+
yield file
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def capture(*argv, to:)
|
53
|
+
to.binmode
|
54
|
+
IO.popen(argv) { |out| IO.copy_stream(out, to) }
|
55
|
+
to.rewind
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveStorage
|
4
|
+
class Previewer::PDFPreviewer < Previewer
|
5
|
+
def self.accept?(blob)
|
6
|
+
blob.content_type == "application/pdf"
|
7
|
+
end
|
8
|
+
|
9
|
+
def preview
|
10
|
+
download_blob_to_tempfile do |input|
|
11
|
+
draw "mutool", "draw", "-F", "png", "-o", "-", input.path, "1" do |output|
|
12
|
+
yield io: output, filename: "#{blob.filename.base}.png", content_type: "image/png"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveStorage
|
4
|
+
class Previewer::VideoPreviewer < Previewer
|
5
|
+
def self.accept?(blob)
|
6
|
+
blob.video?
|
7
|
+
end
|
8
|
+
|
9
|
+
def preview
|
10
|
+
download_blob_to_tempfile do |input|
|
11
|
+
draw_relevant_frame_from input do |output|
|
12
|
+
yield io: output, filename: "#{blob.filename.base}.png", content_type: "image/png"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
def draw_relevant_frame_from(file, &block)
|
19
|
+
draw "ffmpeg", "-i", file.path, "-y", "-vcodec", "png",
|
20
|
+
"-vf", "thumbnail", "-vframes", "1", "-f", "image2", "-", &block
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -1,34 +1,122 @@
|
|
1
|
-
#
|
2
|
-
class ActiveStorage::Service
|
3
|
-
class ActiveStorage::IntegrityError < StandardError; end
|
1
|
+
# frozen_string_literal: true
|
4
2
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
3
|
+
require "active_storage/log_subscriber"
|
4
|
+
|
5
|
+
module ActiveStorage
|
6
|
+
class IntegrityError < StandardError; end
|
7
|
+
|
8
|
+
# Abstract class serving as an interface for concrete services.
|
9
|
+
#
|
10
|
+
# The available services are:
|
11
|
+
#
|
12
|
+
# * +Disk+, to manage attachments saved directly on the hard drive.
|
13
|
+
# * +GCS+, to manage attachments through Google Cloud Storage.
|
14
|
+
# * +S3+, to manage attachments through Amazon S3.
|
15
|
+
# * +AzureStorage+, to manage attachments through Microsoft Azure Storage.
|
16
|
+
# * +Mirror+, to be able to use several services to manage attachments.
|
17
|
+
#
|
18
|
+
# Inside a Rails application, you can set-up your services through the
|
19
|
+
# generated <tt>config/storage.yml</tt> file and reference one
|
20
|
+
# of the aforementioned constant under the +service+ key. For example:
|
21
|
+
#
|
22
|
+
# local:
|
23
|
+
# service: Disk
|
24
|
+
# root: <%= Rails.root.join("storage") %>
|
25
|
+
#
|
26
|
+
# You can checkout the service's constructor to know which keys are required.
|
27
|
+
#
|
28
|
+
# Then, in your application's configuration, you can specify the service to
|
29
|
+
# use like this:
|
30
|
+
#
|
31
|
+
# config.active_storage.service = :local
|
32
|
+
#
|
33
|
+
# If you are using Active Storage outside of a Ruby on Rails application, you
|
34
|
+
# can configure the service to use like this:
|
35
|
+
#
|
36
|
+
# ActiveStorage::Blob.service = ActiveStorage::Service.configure(
|
37
|
+
# :Disk,
|
38
|
+
# root: Pathname("/foo/bar/storage")
|
39
|
+
# )
|
40
|
+
class Service
|
41
|
+
extend ActiveSupport::Autoload
|
42
|
+
autoload :Configurator
|
43
|
+
|
44
|
+
class_attribute :url_expires_in, default: 5.minutes
|
45
|
+
|
46
|
+
class << self
|
47
|
+
# Configure an Active Storage service by name from a set of configurations,
|
48
|
+
# typically loaded from a YAML file. The Active Storage engine uses this
|
49
|
+
# to set the global Active Storage service when the app boots.
|
50
|
+
def configure(service_name, configurations)
|
51
|
+
Configurator.build(service_name, configurations)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Override in subclasses that stitch together multiple services and hence
|
55
|
+
# need to build additional services using the configurator.
|
56
|
+
#
|
57
|
+
# Passes the configurator and all of the service's config as keyword args.
|
58
|
+
#
|
59
|
+
# See MirrorService for an example.
|
60
|
+
def build(configurator:, service: nil, **service_config) #:nodoc:
|
61
|
+
new(**service_config)
|
62
|
+
end
|
11
63
|
end
|
12
|
-
end
|
13
64
|
|
65
|
+
# Upload the +io+ to the +key+ specified. If a +checksum+ is provided, the service will
|
66
|
+
# ensure a match when the upload has completed or raise an ActiveStorage::IntegrityError.
|
67
|
+
def upload(key, io, checksum: nil)
|
68
|
+
raise NotImplementedError
|
69
|
+
end
|
14
70
|
|
15
|
-
|
16
|
-
|
17
|
-
|
71
|
+
# Return the content of the file at the +key+.
|
72
|
+
def download(key)
|
73
|
+
raise NotImplementedError
|
74
|
+
end
|
18
75
|
|
19
|
-
|
20
|
-
|
21
|
-
|
76
|
+
# Delete the file at the +key+.
|
77
|
+
def delete(key)
|
78
|
+
raise NotImplementedError
|
79
|
+
end
|
22
80
|
|
23
|
-
|
24
|
-
|
25
|
-
|
81
|
+
# Return +true+ if a file exists at the +key+.
|
82
|
+
def exist?(key)
|
83
|
+
raise NotImplementedError
|
84
|
+
end
|
26
85
|
|
27
|
-
|
28
|
-
|
29
|
-
|
86
|
+
# Returns a signed, temporary URL for the file at the +key+. The URL will be valid for the amount
|
87
|
+
# of seconds specified in +expires_in+. You most also provide the +disposition+ (+:inline+ or +:attachment+),
|
88
|
+
# +filename+, and +content_type+ that you wish the file to be served with on request.
|
89
|
+
def url(key, expires_in:, disposition:, filename:, content_type:)
|
90
|
+
raise NotImplementedError
|
91
|
+
end
|
92
|
+
|
93
|
+
# Returns a signed, temporary URL that a direct upload file can be PUT to on the +key+.
|
94
|
+
# The URL will be valid for the amount of seconds specified in +expires_in+.
|
95
|
+
# You most also provide the +content_type+, +content_length+, and +checksum+ of the file
|
96
|
+
# that will be uploaded. All these attributes will be validated by the service upon upload.
|
97
|
+
def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:)
|
98
|
+
raise NotImplementedError
|
99
|
+
end
|
100
|
+
|
101
|
+
# Returns a Hash of headers for +url_for_direct_upload+ requests.
|
102
|
+
def headers_for_direct_upload(key, filename:, content_type:, content_length:, checksum:)
|
103
|
+
{}
|
104
|
+
end
|
105
|
+
|
106
|
+
private
|
107
|
+
def instrument(operation, key, payload = {}, &block)
|
108
|
+
ActiveSupport::Notifications.instrument(
|
109
|
+
"service_#{operation}.active_storage",
|
110
|
+
payload.merge(key: key, service: service_name), &block)
|
111
|
+
end
|
112
|
+
|
113
|
+
def service_name
|
114
|
+
# ActiveStorage::Service::DiskService => Disk
|
115
|
+
self.class.name.split("::").third.remove("Service")
|
116
|
+
end
|
30
117
|
|
31
|
-
|
32
|
-
|
118
|
+
def content_disposition_with(type: "inline", filename:)
|
119
|
+
(type.to_s.presence_in(%w( attachment inline )) || "inline") + "; #{filename.parameters}"
|
120
|
+
end
|
33
121
|
end
|
34
122
|
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/core_ext/numeric/bytes"
|
4
|
+
require "azure/storage"
|
5
|
+
require "azure/storage/core/auth/shared_access_signature"
|
6
|
+
|
7
|
+
module ActiveStorage
|
8
|
+
# Wraps the Microsoft Azure Storage Blob Service as an Active Storage service.
|
9
|
+
# See ActiveStorage::Service for the generic API documentation that applies to all services.
|
10
|
+
class Service::AzureStorageService < Service
|
11
|
+
attr_reader :client, :path, :blobs, :container, :signer
|
12
|
+
|
13
|
+
def initialize(path:, storage_account_name:, storage_access_key:, container:)
|
14
|
+
@client = Azure::Storage::Client.create(storage_account_name: storage_account_name, storage_access_key: storage_access_key)
|
15
|
+
@signer = Azure::Storage::Core::Auth::SharedAccessSignature.new(storage_account_name, storage_access_key)
|
16
|
+
@blobs = client.blob_client
|
17
|
+
@container = container
|
18
|
+
@path = path
|
19
|
+
end
|
20
|
+
|
21
|
+
def upload(key, io, checksum: nil)
|
22
|
+
instrument :upload, key, checksum: checksum do
|
23
|
+
begin
|
24
|
+
blobs.create_block_blob(container, key, io, content_md5: checksum)
|
25
|
+
rescue Azure::Core::Http::HTTPError
|
26
|
+
raise ActiveStorage::IntegrityError
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def download(key, &block)
|
32
|
+
if block_given?
|
33
|
+
instrument :streaming_download, key do
|
34
|
+
stream(key, &block)
|
35
|
+
end
|
36
|
+
else
|
37
|
+
instrument :download, key do
|
38
|
+
_, io = blobs.get_blob(container, key)
|
39
|
+
io.force_encoding(Encoding::BINARY)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def delete(key)
|
45
|
+
instrument :delete, key do
|
46
|
+
begin
|
47
|
+
blobs.delete_blob(container, key)
|
48
|
+
rescue Azure::Core::Http::HTTPError
|
49
|
+
false
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def exist?(key)
|
55
|
+
instrument :exist, key do |payload|
|
56
|
+
answer = blob_for(key).present?
|
57
|
+
payload[:exist] = answer
|
58
|
+
answer
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def url(key, expires_in:, filename:, disposition:, content_type:)
|
63
|
+
instrument :url, key do |payload|
|
64
|
+
base_url = url_for(key)
|
65
|
+
generated_url = signer.signed_uri(
|
66
|
+
URI(base_url), false,
|
67
|
+
permissions: "r",
|
68
|
+
expiry: format_expiry(expires_in),
|
69
|
+
content_disposition: content_disposition_with(type: disposition, filename: filename),
|
70
|
+
content_type: content_type
|
71
|
+
).to_s
|
72
|
+
|
73
|
+
payload[:url] = generated_url
|
74
|
+
|
75
|
+
generated_url
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:)
|
80
|
+
instrument :url, key do |payload|
|
81
|
+
base_url = url_for(key)
|
82
|
+
generated_url = signer.signed_uri(URI(base_url), false, permissions: "rw",
|
83
|
+
expiry: format_expiry(expires_in)).to_s
|
84
|
+
|
85
|
+
payload[:url] = generated_url
|
86
|
+
|
87
|
+
generated_url
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def headers_for_direct_upload(key, content_type:, checksum:, **)
|
92
|
+
{ "Content-Type" => content_type, "Content-MD5" => checksum, "x-ms-blob-type" => "BlockBlob" }
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
def url_for(key)
|
97
|
+
"#{path}/#{container}/#{key}"
|
98
|
+
end
|
99
|
+
|
100
|
+
def blob_for(key)
|
101
|
+
blobs.get_blob_properties(container, key)
|
102
|
+
rescue Azure::Core::Http::HTTPError
|
103
|
+
false
|
104
|
+
end
|
105
|
+
|
106
|
+
def format_expiry(expires_in)
|
107
|
+
expires_in ? Time.now.utc.advance(seconds: expires_in).iso8601 : nil
|
108
|
+
end
|
109
|
+
|
110
|
+
# Reads the object for the given key in chunks, yielding each to the block.
|
111
|
+
def stream(key)
|
112
|
+
blob = blob_for(key)
|
113
|
+
|
114
|
+
chunk_size = 5.megabytes
|
115
|
+
offset = 0
|
116
|
+
|
117
|
+
while offset < blob.properties[:content_length]
|
118
|
+
_, chunk = blobs.get_blob(container, key, start_range: offset, end_range: offset + chunk_size - 1)
|
119
|
+
yield chunk.force_encoding(Encoding::BINARY)
|
120
|
+
offset += chunk_size
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|