jd-paperclip-azure 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.autotest +25 -0
- data/.github/workflows/tests.yml +22 -0
- data/.gitignore +58 -0
- data/.ruby-version +1 -0
- data/Gemfile +3 -0
- data/History.txt +6 -0
- data/Manifest.txt +9 -0
- data/README.md +138 -0
- data/Rakefile +29 -0
- data/lib/paperclip/azure.rb +5 -0
- data/lib/paperclip/storage/azure/environment.rb +19 -0
- data/lib/paperclip/storage/azure.rb +271 -0
- data/lib/paperclip-azure.rb +1 -0
- data/paperclip-azure.gemspec +33 -0
- data/spec/database.yml +3 -0
- data/spec/paperclip/storage/azure/environment_spec.rb +26 -0
- data/spec/paperclip/storage/azure_spec.rb +426 -0
- data/spec/spec_helper.rb +40 -0
- data/spec/support/fake_model.rb +25 -0
- data/spec/support/fake_rails.rb +12 -0
- data/spec/support/fixtures/12k.png +0 -0
- data/spec/support/fixtures/50x50.png +0 -0
- data/spec/support/fixtures/5k.png +0 -0
- data/spec/support/fixtures/animated +0 -0
- data/spec/support/fixtures/animated.gif +0 -0
- data/spec/support/fixtures/animated.unknown +0 -0
- data/spec/support/fixtures/azure.yml +8 -0
- data/spec/support/fixtures/bad.png +1 -0
- data/spec/support/fixtures/empty.html +1 -0
- data/spec/support/fixtures/empty.xlsx +0 -0
- data/spec/support/fixtures/fog.yml +8 -0
- data/spec/support/fixtures/rotated.jpg +0 -0
- data/spec/support/fixtures/spaced file.jpg +0 -0
- data/spec/support/fixtures/spaced file.png +0 -0
- data/spec/support/fixtures/text.txt +1 -0
- data/spec/support/fixtures/twopage.pdf +0 -0
- data/spec/support/fixtures/uppercase.PNG +0 -0
- data/spec/support/mock_attachment.rb +24 -0
- data/spec/support/mock_interpolator.rb +24 -0
- data/spec/support/mock_url_generator_builder.rb +27 -0
- data/spec/support/model_reconstruction.rb +68 -0
- data/spec/support/test_data.rb +13 -0
- data/spec/support/version_helper.rb +9 -0
- metadata +256 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: bfc4bcb85a94ff22f13b24dcfc41e6de1b6db1a99507944da8f8482b98ffcfef
|
4
|
+
data.tar.gz: 9f3d9ffcf35af0ab3690933d1e2fe500c4008070e2673e8d55a46de21cd9a348
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 9358e35a59dd40553ee0f19949a8af4b3e7125f9791f8e049924af7cd183add4bdc60c9791d74eba9b7c3ef5a20f3537aa6a43811348b50d5c5f9a0744f2bd45
|
7
|
+
data.tar.gz: a7f025569b498c62dc86fa337270f0853214e71658753f2198261c24f0ebfefbb277b502bb9a169ade65e1978909d36946c9e60181ee7750dbe26f5fed674887
|
data/.autotest
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
|
3
|
+
require "autotest/restart"
|
4
|
+
|
5
|
+
# Autotest.add_hook :initialize do |at|
|
6
|
+
# at.testlib = "minitest/unit"
|
7
|
+
#
|
8
|
+
# at.extra_files << "../some/external/dependency.rb"
|
9
|
+
#
|
10
|
+
# at.libs << ":../some/external"
|
11
|
+
#
|
12
|
+
# at.add_exception "vendor"
|
13
|
+
#
|
14
|
+
# at.add_mapping(/dependency.rb/) do |f, _|
|
15
|
+
# at.files_matching(/test_.*rb$/)
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# %w(TestA TestB).each do |klass|
|
19
|
+
# at.extra_class_map[klass] = "test/test_misc.rb"
|
20
|
+
# end
|
21
|
+
# end
|
22
|
+
|
23
|
+
# Autotest.add_hook :run_command do |at|
|
24
|
+
# system "rake build"
|
25
|
+
# end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
name: rspec test suite
|
2
|
+
on:
|
3
|
+
push:
|
4
|
+
branches:
|
5
|
+
- master
|
6
|
+
pull_request:
|
7
|
+
branches:
|
8
|
+
- master
|
9
|
+
jobs:
|
10
|
+
test:
|
11
|
+
name: "💎 rspec"
|
12
|
+
runs-on: ubuntu-latest
|
13
|
+
steps:
|
14
|
+
- uses: actions/checkout@v3
|
15
|
+
- name: Set up Ruby
|
16
|
+
uses: ruby/setup-ruby@v1
|
17
|
+
with:
|
18
|
+
ruby-version: '3.2.1'
|
19
|
+
- name: Install dependencies
|
20
|
+
run: bundle install
|
21
|
+
- name: Run tests
|
22
|
+
run: bundle exec rspec
|
data/.gitignore
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
|
2
|
+
# Created by https://www.gitignore.io/api/ruby
|
3
|
+
|
4
|
+
### Ruby ###
|
5
|
+
*.gem
|
6
|
+
*.rbc
|
7
|
+
/.config
|
8
|
+
/coverage/
|
9
|
+
/Gemfile.lock
|
10
|
+
/InstalledFiles
|
11
|
+
/pkg/
|
12
|
+
/spec/debug.log
|
13
|
+
/spec/reports/
|
14
|
+
/spec/examples.txt
|
15
|
+
/test/tmp/
|
16
|
+
/test/version_tmp/
|
17
|
+
/tmp/
|
18
|
+
|
19
|
+
# Used by dotenv library to load environment variables.
|
20
|
+
# .env
|
21
|
+
|
22
|
+
## Specific to RubyMotion:
|
23
|
+
.dat*
|
24
|
+
.repl_history
|
25
|
+
build/
|
26
|
+
*.bridgesupport
|
27
|
+
build-iPhoneOS/
|
28
|
+
build-iPhoneSimulator/
|
29
|
+
|
30
|
+
## Specific to RubyMotion (use of CocoaPods):
|
31
|
+
#
|
32
|
+
# We recommend against adding the Pods directory to your .gitignore. However
|
33
|
+
# you should judge for yourself, the pros and cons are mentioned at:
|
34
|
+
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
|
35
|
+
#
|
36
|
+
# vendor/Pods/
|
37
|
+
|
38
|
+
## Documentation cache and generated files:
|
39
|
+
/.yardoc/
|
40
|
+
/_yardoc/
|
41
|
+
/doc/
|
42
|
+
/rdoc/
|
43
|
+
|
44
|
+
## Environment normalization:
|
45
|
+
/.bundle/
|
46
|
+
/vendor/bundle
|
47
|
+
/lib/bundler/man/
|
48
|
+
|
49
|
+
# for a library or gem, you might want to ignore these files since the code is
|
50
|
+
# intended to run in multiple environments; otherwise, check them in:
|
51
|
+
# Gemfile.lock
|
52
|
+
# .ruby-version
|
53
|
+
# .ruby-gemset
|
54
|
+
|
55
|
+
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
|
56
|
+
.rvmrc
|
57
|
+
|
58
|
+
# End of https://www.gitignore.io/api/ruby
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
3.2.1
|
data/Gemfile
ADDED
data/History.txt
ADDED
data/Manifest.txt
ADDED
data/README.md
ADDED
@@ -0,0 +1,138 @@
|
|
1
|
+
= paperclip-azure
|
2
|
+
|
3
|
+
home :: https://github.com/mistydemeo/paperclip-azure
|
4
|
+
code :: https://github.com/mistydemeo/paperclip-azure
|
5
|
+
bugs :: https://github.com/mistydemeo/paperclip-azure/issues
|
6
|
+
|
7
|
+
== DESCRIPTION:
|
8
|
+
|
9
|
+
Paperclip-Azure is a [Paperclip](https://github.com/thoughtbot/paperclip) storage driver for storing files in a Microsoft Azure Blob. This is a friendly fork of the [original gem](https://github.com/supportify/paperclip-azure); it incorporates bugfixes that haven't yet been incldued in point releases of the upstream gem and some new features.
|
10
|
+
|
11
|
+
== FEATURES/PROBLEMS:
|
12
|
+
|
13
|
+
* FIX (list of features or problems)
|
14
|
+
|
15
|
+
== SYNOPSIS:
|
16
|
+
|
17
|
+
The Azure storage engine has been developed to work as similarly to S3 storage configuration as is possible. This gem can be configured in a Paperclip initializer or environment file as follows:
|
18
|
+
|
19
|
+
Paperclip::Attachment.default_options[:storage] = :azure
|
20
|
+
Paperclip::Attachment.default_options[:azure_credentials] = {
|
21
|
+
storage_account_name: ENV['AZURE_STORAGE_ACCOUNT'],
|
22
|
+
storage_access_key: ENV['AZURE_STORAGE_ACCESS_KEY'],
|
23
|
+
container: ENV['AZURE_CONTAINER_NAME']
|
24
|
+
}
|
25
|
+
|
26
|
+
Or, at the level of the model such as in the following example:
|
27
|
+
|
28
|
+
has_attached_file :download,
|
29
|
+
storage: :azure,
|
30
|
+
azure_credentials: {
|
31
|
+
storage_account_name: ENV['AZURE_STORAGE_ACCOUNT'],
|
32
|
+
storage_access_key: ENV['AZURE_STORAGE_ACCESS_KEY'],
|
33
|
+
container: ENV['AZURE_CONTAINER_NAME']
|
34
|
+
}
|
35
|
+
|
36
|
+
Additionally, you can also supply credentials using a path or a File that contains the +storage_access_key+ and +storage_account_name+ that Azure gives you. You can 'environment-space' this just like you do to your `database.yml` file, so different environments can use different accounts:
|
37
|
+
|
38
|
+
development:
|
39
|
+
storage_account_name: foo
|
40
|
+
storage_access_key: 123...
|
41
|
+
test:
|
42
|
+
storage_account_name: foo
|
43
|
+
storage_access_key: abc...
|
44
|
+
production:
|
45
|
+
storage_account_name: foo
|
46
|
+
storage_access_key: 456...
|
47
|
+
|
48
|
+
This is not required, however, and the file may simply look like this:
|
49
|
+
|
50
|
+
storage_account_name: foo
|
51
|
+
storage_access_key: 456...
|
52
|
+
|
53
|
+
In which case, those access keys will be used in all environments. You can also put your container name in this file, instead of adding it to the code directly. This is useful when you want the same account but a different container for development versus production.
|
54
|
+
|
55
|
+
=== Private Blob Access
|
56
|
+
|
57
|
+
In the even that are using a Blob that has been configured for Private access, you will need to use the Shared Access Signature functionality of Azure. This functionality has been baked in to the `Attachment#expiring_url` method. Simply specify a time and a style and you will get a proper URL as follows:
|
58
|
+
|
59
|
+
object.attachment.expiring_url(30.minutes.since, :thumb)
|
60
|
+
|
61
|
+
For more information about Azure Shared Access Signatures, please refer to [here](http://azure.microsoft.com/en-us/documentation/articles/storage-dotnet-shared-access-signature-part-1/).
|
62
|
+
|
63
|
+
=== Azure Environments
|
64
|
+
|
65
|
+
Microsoft offers specialized Azure implementations for special circumstances should the need arise. As of the most recent update of this gem, the AzureChinaCloud, AzureUSGovernment, and AzureGermanCloud environments all offer specific storage URL's that differ from those of the standard AzureCloud. These regions can be specified via the `:region` key of the `:azure_credentials` dictionary by using the symbols `:cn`, `:usgovt`, and `:de` respectively. When working with one of these environments, simply update your credentials to include the region as follows:
|
66
|
+
|
67
|
+
Paperclip::Attachment.default_options[:azure_credentials] = {
|
68
|
+
storage_account_name: ENV['AZURE_STORAGE_ACCOUNT'],
|
69
|
+
storage_access_key: ENV['AZURE_STORAGE_ACCESS_KEY'],
|
70
|
+
container: ENV['AZURE_CONTAINER_NAME'],
|
71
|
+
region: :de
|
72
|
+
}
|
73
|
+
|
74
|
+
Or, in the instance where the credentials are specified at the model level:
|
75
|
+
|
76
|
+
has_attached_file :download,
|
77
|
+
storage: :azure,
|
78
|
+
azure_credentials: {
|
79
|
+
storage_account_name: ENV['AZURE_STORAGE_ACCOUNT'],
|
80
|
+
storage_access_key: ENV['AZURE_STORAGE_ACCESS_KEY'],
|
81
|
+
container: ENV['AZURE_CONTAINER_NAME'],
|
82
|
+
region: :cn
|
83
|
+
}
|
84
|
+
|
85
|
+
=== Managed identities (Entra ID)
|
86
|
+
|
87
|
+
To authenticate through managed identities instead of using a shared access key, omit passing the access key and pass the principal id of the identity instead. For example:
|
88
|
+
|
89
|
+
Paperclip::Attachment.default_options[:azure_credentials] = {
|
90
|
+
storage_account_name: ENV['AZURE_STORAGE_ACCOUNT'],
|
91
|
+
pincipal_id: ENV['AZURE_STORAGE_PRINCIPAL_ID],
|
92
|
+
container: ENV['AZURE_CONTAINER_NAME'],
|
93
|
+
region: :de
|
94
|
+
}
|
95
|
+
|
96
|
+
|
97
|
+
== REQUIREMENTS:
|
98
|
+
|
99
|
+
* An Azure storage account.
|
100
|
+
|
101
|
+
== INSTALL:
|
102
|
+
|
103
|
+
Add this line to your application's Gemfile after the Paperclip gem:
|
104
|
+
|
105
|
+
gem 'md-paperclip-azure', '~> 2.0'
|
106
|
+
|
107
|
+
And then execute:
|
108
|
+
|
109
|
+
$ bundle install
|
110
|
+
|
111
|
+
== DEVELOPERS:
|
112
|
+
|
113
|
+
* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
|
114
|
+
* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it.
|
115
|
+
* Fork the project.
|
116
|
+
* After checking out the source, run:
|
117
|
+
|
118
|
+
$ rake newb
|
119
|
+
|
120
|
+
This task will install any missing dependencies, run the tests/specs, and generate the RDoc.
|
121
|
+
* Start a feature/bugfix branch.
|
122
|
+
* Commit and push until you are happy with your contribution.
|
123
|
+
* Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
|
124
|
+
* Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
|
125
|
+
* Submit a pull request for the finished product's integration.
|
126
|
+
|
127
|
+
== LICENSE:
|
128
|
+
|
129
|
+
(The MIT License)
|
130
|
+
|
131
|
+
Copyright (c) 2017 Supportify, Inc.
|
132
|
+
Copyright (c) 2023 Misty De Méo
|
133
|
+
|
134
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
135
|
+
|
136
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
137
|
+
|
138
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
|
3
|
+
require "rubygems"
|
4
|
+
require "hoe"
|
5
|
+
|
6
|
+
Hoe.plugin :bundler
|
7
|
+
Hoe.plugin :debug
|
8
|
+
Hoe.plugin :git
|
9
|
+
Hoe.plugin :gemspec
|
10
|
+
Hoe.plugin :rubygems
|
11
|
+
|
12
|
+
Hoe.spec "md-paperclip-azure" do
|
13
|
+
developer("Misty De Méo", "mistydemeo@gmail.com")
|
14
|
+
license "MIT" # this should match the license in the README
|
15
|
+
|
16
|
+
extra_deps << ['azure-storage-blob', '~> 2.0.1']
|
17
|
+
extra_deps << ['hashie', '~> 5.0']
|
18
|
+
extra_deps << ['addressable', '~> 2.5']
|
19
|
+
|
20
|
+
extra_dev_deps << ['kt-paperclip', '~> 7.1']
|
21
|
+
extra_dev_deps << ['sqlite3', '~> 1.3.8']
|
22
|
+
extra_dev_deps << ['rspec', '~> 3.0']
|
23
|
+
extra_dev_deps << ['activerecord', '~> 6.1']
|
24
|
+
extra_dev_deps << ['activerecord-import', '~> 1.4']
|
25
|
+
extra_dev_deps << ['activemodel', '~> 6.1']
|
26
|
+
extra_dev_deps << ['activesupport', '~> 6.1']
|
27
|
+
end
|
28
|
+
|
29
|
+
# vim: syntax=ruby
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Paperclip
|
2
|
+
module Storage
|
3
|
+
module Azure
|
4
|
+
class Environment
|
5
|
+
|
6
|
+
ENVIRONMENT_SUFFIX = {
|
7
|
+
global: 'core.windows.net',
|
8
|
+
cn: 'core.chinacloudapi.cn',
|
9
|
+
de: "core.cloudapi.de",
|
10
|
+
usgovt: 'core.usgovcloudapi.net'
|
11
|
+
}
|
12
|
+
|
13
|
+
def self.url_for(account_name, region = nil)
|
14
|
+
"#{account_name}.blob.#{ENVIRONMENT_SUFFIX[region || :global]}"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,271 @@
|
|
1
|
+
require "azure_blob"
|
2
|
+
|
3
|
+
module Paperclip
|
4
|
+
module Storage
|
5
|
+
# Azure's container file hosting service is a scalable, easy place to store files for
|
6
|
+
# distribution. You can find out more about it at http://azure.microsoft.com/en-us/services/storage/
|
7
|
+
#
|
8
|
+
# To use Paperclip with Azure, include the +azure-storage-blob+ gem in your Gemfile:
|
9
|
+
# gem 'azure-storage-blob'
|
10
|
+
# There are a few Azure-specific options for has_attached_file:
|
11
|
+
# * +azure_credentials+: Takes a path, a File, a Hash or a Proc. The path (or File) must point
|
12
|
+
# to a YAML file containing the +storage_access_key+ and +storage_account_name+ that azure
|
13
|
+
# gives you. You can 'environment-space' this just like you do to your
|
14
|
+
# database.yml file, so different environments can use different accounts:
|
15
|
+
# development:
|
16
|
+
# storage_account_name: foo
|
17
|
+
# storage_access_key: 123...
|
18
|
+
# test:
|
19
|
+
# storage_account_name: foo
|
20
|
+
# storage_access_key: abc...
|
21
|
+
# production:
|
22
|
+
# storage_account_name: foo
|
23
|
+
# storage_access_key: 456...
|
24
|
+
# This is not required, however, and the file may simply look like this:
|
25
|
+
# storage_account_name: foo
|
26
|
+
# storage_access_key: 456...
|
27
|
+
# In which case, those access keys will be used in all environments. You can also
|
28
|
+
# put your container name in this file, instead of adding it to the code directly.
|
29
|
+
# This is useful when you want the same account but a different container for
|
30
|
+
# development versus production.
|
31
|
+
# When using a Proc it provides a single parameter which is the attachment itself. A
|
32
|
+
# method #instance is available on the attachment which will take you back to your
|
33
|
+
# code. eg.
|
34
|
+
# class User
|
35
|
+
# has_attached_file :download,
|
36
|
+
# :storage => :azure,
|
37
|
+
# :azure_credentials => Proc.new{|a| a.instance.azure_credentials }
|
38
|
+
#
|
39
|
+
# def azure_credentials
|
40
|
+
# { :container => "xxx", :storage_account_name => "xxx", :storage_access_key => "xxx" }
|
41
|
+
# end
|
42
|
+
# end
|
43
|
+
#
|
44
|
+
# * +container+: This is the name of the Azure container that will store your files. Remember
|
45
|
+
# that the container must be unique across the storage account. If the container does not exist
|
46
|
+
# Paperclip will attempt to create it. The container name will not be interpolated.
|
47
|
+
# You can define the container as a Proc if you want to determine it's name at runtime.
|
48
|
+
# Paperclip will call that Proc with attachment as the only argument.
|
49
|
+
# * +path+: This is the key under the container in which the file will be stored. The
|
50
|
+
# URL will be constructed from the container and the path. This is what you will want
|
51
|
+
# to interpolate. Keys should be unique, like filenames, and despite the fact that
|
52
|
+
# Azure (strictly speaking) does not support directories, you can still use a / to
|
53
|
+
# separate parts of your file name.
|
54
|
+
# * +region+: Depending on the region, different base urls are used. Supported values :global, :de
|
55
|
+
|
56
|
+
module Azure
|
57
|
+
def self.extended base
|
58
|
+
base.instance_eval do
|
59
|
+
@azure_options = @options[:azure_options] || {}
|
60
|
+
|
61
|
+
unless @options[:url].to_s.match(/\A:azure.*url\z/) || @options[:url] == ":asset_host".freeze
|
62
|
+
@options[:path] = path_option.gsub(/:url/, @options[:url]).sub(/\A:rails_root\/public\/system/, "".freeze)
|
63
|
+
@options[:url] = ":azure_path_url".freeze
|
64
|
+
end
|
65
|
+
@options[:url] = @options[:url].inspect if @options[:url].is_a?(Symbol)
|
66
|
+
|
67
|
+
@http_proxy = @options[:http_proxy] || nil
|
68
|
+
end
|
69
|
+
|
70
|
+
unless Paperclip::Interpolations.respond_to? :azure_alias_url
|
71
|
+
Paperclip.interpolates(:azure_alias_url) do |attachment, style|
|
72
|
+
protocol = attachment.azure_protocol(style, true)
|
73
|
+
host = attachment.azure_host_alias
|
74
|
+
path = attachment.path(style).
|
75
|
+
split("/")[attachment.azure_prefixes_in_alias..-1].
|
76
|
+
join("/").
|
77
|
+
sub(%r{\A/}, "")
|
78
|
+
"#{protocol}//#{host}/#{path}"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
Paperclip.interpolates(:azure_path_url) do |attachment, style|
|
82
|
+
attachment.azure_uri(style)
|
83
|
+
end unless Paperclip::Interpolations.respond_to? :azure_path_url
|
84
|
+
Paperclip.interpolates(:asset_host) do |attachment, style|
|
85
|
+
"#{attachment.path(style).sub(%r{\A/}, "".freeze)}"
|
86
|
+
end unless Paperclip::Interpolations.respond_to? :asset_host
|
87
|
+
end
|
88
|
+
|
89
|
+
def expiring_url(time = 3600, style_name = default_style)
|
90
|
+
if path(style_name)
|
91
|
+
obj_path = path(style_name).gsub(%r{\A/}, '')
|
92
|
+
expiry = Time.now.utc.advance(seconds: time).iso8601
|
93
|
+
azure_interface.signed_uri(obj_path, permissions: 'r', expiry:).to_s
|
94
|
+
else
|
95
|
+
url(style_name)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def azure_host_alias
|
100
|
+
@azure_host_alias = @options[:azure_host_alias]
|
101
|
+
@azure_host_alias = @azure_host_alias.call(self) if @azure_host_alias.respond_to?(:call)
|
102
|
+
@azure_host_alias
|
103
|
+
end
|
104
|
+
|
105
|
+
def azure_prefixes_in_alias
|
106
|
+
@azure_prefixes_in_alias ||= @options[:azure_prefixes_in_alias].to_i
|
107
|
+
end
|
108
|
+
|
109
|
+
def azure_protocol(style = default_style, with_colon = false)
|
110
|
+
protocol = @azure_options[:protocol]
|
111
|
+
protocol = protocol.call(style, self) if protocol.respond_to?(:call)
|
112
|
+
|
113
|
+
if with_colon && !protocol.empty?
|
114
|
+
"#{protocol}:"
|
115
|
+
else
|
116
|
+
protocol.to_s
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def auto_connect_duration
|
121
|
+
@auto_connect_duration ||= @options[:auto_connect_duration] || azure_credentials[:auto_connect_duration] || 10
|
122
|
+
@auto_connect_duration
|
123
|
+
end
|
124
|
+
|
125
|
+
def azure_credentials
|
126
|
+
@azure_credentials ||= parse_credentials(@options[:azure_credentials])
|
127
|
+
end
|
128
|
+
|
129
|
+
def azure_account_name
|
130
|
+
account_name = @options[:azure_storage_account_name] || azure_credentials[:storage_account_name]
|
131
|
+
account_name = account_name.call(self) if account_name.is_a?(Proc)
|
132
|
+
|
133
|
+
account_name
|
134
|
+
end
|
135
|
+
|
136
|
+
def container_name
|
137
|
+
@container ||= @options[:container] || azure_credentials[:container]
|
138
|
+
@container = @container.call(self) if @container.respond_to?(:call)
|
139
|
+
@container or raise ArgumentError, "missing required :container option"
|
140
|
+
end
|
141
|
+
|
142
|
+
def azure_interface
|
143
|
+
@azure_interface ||= begin
|
144
|
+
config = {}
|
145
|
+
|
146
|
+
[:storage_account_name, :storage_access_key, :container, :principal_id, :use_managed_identities].each do |opt|
|
147
|
+
config[opt] = azure_credentials[opt] if azure_credentials[opt]
|
148
|
+
end
|
149
|
+
|
150
|
+
obtain_azure_instance_for(config.merge(@azure_options))
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def obtain_azure_instance_for(options)
|
155
|
+
AzureBlob::Client.new(
|
156
|
+
account_name: options[:storage_account_name],
|
157
|
+
access_key: options[:storage_access_key],
|
158
|
+
principal_id: options[:principal_id],
|
159
|
+
use_managed_identities: options[:use_managed_identities],
|
160
|
+
container: container_name,
|
161
|
+
cloud_regions: azure_credentials[:region],
|
162
|
+
)
|
163
|
+
end
|
164
|
+
|
165
|
+
def azure_uri(style_name = default_style)
|
166
|
+
azure_interface.generate_uri("#{container_name}/#{path(style_name).gsub(%r{\A/}, '')}")
|
167
|
+
end
|
168
|
+
|
169
|
+
def azure_container
|
170
|
+
@azure_container ||= azure_interface.get_container_properties
|
171
|
+
end
|
172
|
+
|
173
|
+
def azure_object(style_name = default_style)
|
174
|
+
azure_interface.get_blob_properties path(style_name).sub(%r{\A/},'')
|
175
|
+
end
|
176
|
+
|
177
|
+
def parse_credentials(creds)
|
178
|
+
creds = creds.respond_to?('call') ? creds.call(self) : creds
|
179
|
+
creds = find_credentials(creds).stringify_keys
|
180
|
+
env = Object.const_defined?(:Rails) ? Rails.env : nil
|
181
|
+
(creds[env] || creds).symbolize_keys
|
182
|
+
end
|
183
|
+
|
184
|
+
def exists?(style = default_style)
|
185
|
+
if original_filename
|
186
|
+
azure_object(style).present?
|
187
|
+
else
|
188
|
+
false
|
189
|
+
end
|
190
|
+
rescue AzureBlob::Http::FileNotFoundError
|
191
|
+
false
|
192
|
+
end
|
193
|
+
|
194
|
+
def create_container
|
195
|
+
azure_interface.create_container container_name
|
196
|
+
end
|
197
|
+
|
198
|
+
def flush_writes #:nodoc:
|
199
|
+
@queued_for_write.each do |style, file|
|
200
|
+
begin
|
201
|
+
log("saving #{path(style)}")
|
202
|
+
|
203
|
+
write_options = {
|
204
|
+
content_type: file.content_type,
|
205
|
+
}
|
206
|
+
|
207
|
+
if azure_container
|
208
|
+
save_blob path(style).sub(%r{\A/},''), file, write_options
|
209
|
+
end
|
210
|
+
rescue AzureBlob::Http::FileNotFoundError
|
211
|
+
create_container
|
212
|
+
retry
|
213
|
+
ensure
|
214
|
+
file.rewind
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
after_flush_writes # allows attachment to clean up temp files
|
219
|
+
|
220
|
+
@queued_for_write = {}
|
221
|
+
end
|
222
|
+
|
223
|
+
def save_blob(storage_path, file, write_options)
|
224
|
+
write_options = write_options.merge(block_size: 4.megabytes) if file.size >= 64.megabytes
|
225
|
+
azure_interface.create_block_blob(storage_path, file, write_options)
|
226
|
+
end
|
227
|
+
|
228
|
+
def flush_deletes #:nodoc:
|
229
|
+
@queued_for_delete.each do |path|
|
230
|
+
begin
|
231
|
+
log("deleting #{path}")
|
232
|
+
|
233
|
+
azure_interface.delete_blob path
|
234
|
+
rescue AzureBlob::Http::FileNotFoundError
|
235
|
+
end
|
236
|
+
end
|
237
|
+
@queued_for_delete = []
|
238
|
+
end
|
239
|
+
|
240
|
+
def copy_to_local_file(style, local_dest_path)
|
241
|
+
log("copying #{path(style)} to local file #{local_dest_path}")
|
242
|
+
|
243
|
+
content = azure_interface.get_blob(path(style).sub(%r{\A/},''))
|
244
|
+
|
245
|
+
::File.open(local_dest_path, 'wb') do |local_file|
|
246
|
+
local_file.write(content)
|
247
|
+
end
|
248
|
+
rescue AzureBlob::Http::FileNotFoundError
|
249
|
+
warn("Cannot copy #{path(style)} to local file #{local_dest_path}")
|
250
|
+
false
|
251
|
+
end
|
252
|
+
|
253
|
+
private
|
254
|
+
|
255
|
+
def find_credentials creds
|
256
|
+
case creds
|
257
|
+
when File
|
258
|
+
YAML::load(ERB.new(File.read(creds.path)).result)
|
259
|
+
when String, Pathname
|
260
|
+
YAML::load(ERB.new(File.read(creds)).result)
|
261
|
+
when Hash
|
262
|
+
creds
|
263
|
+
when NilClass
|
264
|
+
{}
|
265
|
+
else
|
266
|
+
raise ArgumentError, "Credentials given are not a path, file, proc, or hash."
|
267
|
+
end
|
268
|
+
end
|
269
|
+
end
|
270
|
+
end
|
271
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'paperclip', 'storage', 'azure')
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'paperclip/azure.rb'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "jd-paperclip-azure"
|
8
|
+
gem.version = Paperclip::Azure::VERSION
|
9
|
+
gem.authors = [ "Joé Dupuis" ]
|
10
|
+
gem.email = [ "joe@dupuis.io" ]
|
11
|
+
|
12
|
+
gem.summary = %q{Paperclip-Azure is a Paperclip storage driver for storing files in a Microsoft Azure Blob}
|
13
|
+
gem.homepage = "https://github.com/joedupuis/paperclip-azure"
|
14
|
+
|
15
|
+
gem.files = `git ls-files`.split($/)
|
16
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
17
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
18
|
+
gem.require_paths = ["lib"]
|
19
|
+
gem.license = "MIT"
|
20
|
+
|
21
|
+
|
22
|
+
gem.add_dependency "azure-blob", "~> 0.5.2"
|
23
|
+
gem.add_dependency "hashie", "~> 5.0"
|
24
|
+
gem.add_dependency "addressable", "~> 2.5"
|
25
|
+
|
26
|
+
gem.add_development_dependency "kt-paperclip", "~> 7.1"
|
27
|
+
gem.add_development_dependency "sqlite3", "~> 1.3"
|
28
|
+
gem.add_development_dependency "rspec", "~> 3.0"
|
29
|
+
gem.add_development_dependency "activerecord", "~> 6.1"
|
30
|
+
gem.add_development_dependency "activerecord-import", "~> 1.4"
|
31
|
+
gem.add_development_dependency "activemodel", "~> 6.1"
|
32
|
+
gem.add_development_dependency "activesupport", "~> 6.1"
|
33
|
+
end
|
data/spec/database.yml
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'paperclip/storage/azure/environment'
|
3
|
+
|
4
|
+
describe 'Paperclip::Storage::Azure::Environment' do
|
5
|
+
subject { Paperclip::Storage::Azure::Environment }
|
6
|
+
|
7
|
+
describe '#url_for' do
|
8
|
+
let(:account_name) { 'foo' }
|
9
|
+
|
10
|
+
describe 'when the region is not supplied' do
|
11
|
+
it { expect(subject.url_for(account_name)).to eq("#{account_name}.blob.core.windows.net")}
|
12
|
+
end
|
13
|
+
|
14
|
+
describe 'when the region is China' do
|
15
|
+
it { expect(subject.url_for(account_name, :cn)).to eq("#{account_name}.blob.core.chinacloudapi.cn")}
|
16
|
+
end
|
17
|
+
|
18
|
+
describe 'when the region is Germany' do
|
19
|
+
it { expect(subject.url_for(account_name, :de)).to eq("#{account_name}.blob.core.cloudapi.de")}
|
20
|
+
end
|
21
|
+
|
22
|
+
describe 'when the region is the US Govt' do
|
23
|
+
it { expect(subject.url_for(account_name, :usgovt)).to eq("#{account_name}.blob.core.usgovcloudapi.net")}
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|