├── .dockerignore
├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── Dockerfile
├── Gemfile
├── LICENSE.txt
├── README.md
├── Rakefile
├── SELF_CONTAINED_EXAMPLE.md
├── bin
└── publish
├── demo
├── .gitignore
├── Gemfile
├── README.md
├── Rakefile
├── app.rb
├── assets
│ ├── css
│ │ └── app.css
│ └── js
│ │ └── app.js
├── config.ru
├── config
│ ├── credentials.rb
│ ├── sequel.rb
│ └── shrine.rb
├── db
│ └── migrations
│ │ ├── 001_create_albums.rb
│ │ └── 002_create_photos.rb
├── jobs
│ └── attachment
│ │ ├── destroy_job.rb
│ │ └── promote_job.rb
├── lib
│ └── generate_thumbnail.rb
├── models
│ ├── album.rb
│ └── photo.rb
├── routes
│ ├── albums.rb
│ ├── base.rb
│ └── direct_upload.rb
├── test
│ ├── acceptance_test.rb
│ ├── fixtures
│ │ └── image.jpg
│ └── test_helper.rb
├── uploaders
│ └── image_uploader.rb
└── views
│ ├── albums
│ ├── _form.erb
│ ├── _photo.erb
│ ├── index.erb
│ ├── new.erb
│ └── show.erb
│ └── layout.erb
├── doc
├── advantages.md
├── attacher.md
├── carrierwave.md
├── changing_derivatives.md
├── changing_location.md
├── changing_storage.md
├── creating_persistence_plugins.md
├── creating_plugins.md
├── creating_storages.md
├── design.md
├── direct_s3.md
├── external
│ ├── articles.md
│ ├── extensions.md
│ └── misc.md
├── getting_started.md
├── metadata.md
├── multiple_files.md
├── paperclip.md
├── plugins
│ ├── activerecord.md
│ ├── add_metadata.md
│ ├── atomic_helpers.md
│ ├── backgrounding.md
│ ├── cached_attachment_data.md
│ ├── column.md
│ ├── data_uri.md
│ ├── default_storage.md
│ ├── default_url.md
│ ├── delete_raw.md
│ ├── derivation_endpoint.md
│ ├── derivatives.md
│ ├── determine_mime_type.md
│ ├── download_endpoint.md
│ ├── dynamic_storage.md
│ ├── entity.md
│ ├── form_assign.md
│ ├── included.md
│ ├── infer_extension.md
│ ├── instrumentation.md
│ ├── keep_files.md
│ ├── metadata_attributes.md
│ ├── mirroring.md
│ ├── model.md
│ ├── module_include.md
│ ├── multi_cache.md
│ ├── persistence.md
│ ├── presign_endpoint.md
│ ├── pretty_location.md
│ ├── processing.md
│ ├── rack_file.md
│ ├── rack_response.md
│ ├── recache.md
│ ├── refresh_metadata.md
│ ├── remote_url.md
│ ├── remove_attachment.md
│ ├── remove_invalid.md
│ ├── restore_cached_data.md
│ ├── sequel.md
│ ├── signature.md
│ ├── store_dimensions.md
│ ├── tempfile.md
│ ├── type_predicates.md
│ ├── upload_endpoint.md
│ ├── upload_options.md
│ ├── url_options.md
│ ├── validation.md
│ ├── validation_helpers.md
│ └── versions.md
├── processing.md
├── refile.md
├── release_notes
│ ├── 1.0.0.md
│ ├── 1.1.0.md
│ ├── 1.2.0.md
│ ├── 1.3.0.md
│ ├── 1.4.0.md
│ ├── 1.4.1.md
│ ├── 1.4.2.md
│ ├── 2.0.0.md
│ ├── 2.0.1.md
│ ├── 2.1.0.md
│ ├── 2.1.1.md
│ ├── 2.10.0.md
│ ├── 2.10.1.md
│ ├── 2.11.0.md
│ ├── 2.12.0.md
│ ├── 2.13.0.md
│ ├── 2.14.0.md
│ ├── 2.15.0.md
│ ├── 2.16.0.md
│ ├── 2.17.0.md
│ ├── 2.18.0.md
│ ├── 2.19.0.md
│ ├── 2.2.0.md
│ ├── 2.3.0.md
│ ├── 2.3.1.md
│ ├── 2.4.0.md
│ ├── 2.4.1.md
│ ├── 2.5.0.md
│ ├── 2.6.0.md
│ ├── 2.6.1.md
│ ├── 2.7.0.md
│ ├── 2.8.0.md
│ ├── 2.9.0.md
│ ├── 3.0.0.md
│ ├── 3.0.1.md
│ ├── 3.1.0.md
│ ├── 3.2.0.md
│ ├── 3.2.1.md
│ ├── 3.2.2.md
│ ├── 3.3.0.md
│ ├── 3.4.0.md
│ ├── 3.5.0.md
│ └── 3.6.0.md
├── retrieving_uploads.md
├── securing_uploads.md
├── storage
│ ├── file_system.md
│ ├── memory.md
│ └── s3.md
├── testing.md
├── upgrading_to_3.md
└── validation.md
├── lib
├── shrine.rb
└── shrine
│ ├── attacher.rb
│ ├── attachment.rb
│ ├── plugins.rb
│ ├── plugins
│ ├── _persistence.rb
│ ├── _urlsafe_serialization.rb
│ ├── activerecord.rb
│ ├── add_metadata.rb
│ ├── atomic_helpers.rb
│ ├── backgrounding.rb
│ ├── cached_attachment_data.rb
│ ├── column.rb
│ ├── data_uri.rb
│ ├── default_storage.rb
│ ├── default_url.rb
│ ├── default_url_options.rb
│ ├── delete_raw.rb
│ ├── derivation_endpoint.rb
│ ├── derivatives.rb
│ ├── determine_mime_type.rb
│ ├── download_endpoint.rb
│ ├── dynamic_storage.rb
│ ├── entity.rb
│ ├── form_assign.rb
│ ├── included.rb
│ ├── infer_extension.rb
│ ├── instrumentation.rb
│ ├── keep_files.rb
│ ├── metadata_attributes.rb
│ ├── mirroring.rb
│ ├── model.rb
│ ├── module_include.rb
│ ├── multi_cache.rb
│ ├── presign_endpoint.rb
│ ├── pretty_location.rb
│ ├── processing.rb
│ ├── rack_file.rb
│ ├── rack_response.rb
│ ├── recache.rb
│ ├── refresh_metadata.rb
│ ├── remote_url.rb
│ ├── remove_attachment.rb
│ ├── remove_invalid.rb
│ ├── restore_cached_data.rb
│ ├── sequel.rb
│ ├── signature.rb
│ ├── store_dimensions.rb
│ ├── tempfile.rb
│ ├── type_predicates.rb
│ ├── upload_endpoint.rb
│ ├── upload_options.rb
│ ├── url_options.rb
│ ├── validation.rb
│ ├── validation_helpers.rb
│ └── versions.rb
│ ├── storage
│ ├── file_system.rb
│ ├── linter.rb
│ ├── memory.rb
│ └── s3.rb
│ ├── uploaded_file.rb
│ └── version.rb
├── shrine.gemspec
├── test
├── attacher_test.rb
├── attachment_test.rb
├── fixtures
│ └── image.jpg
├── integration
│ ├── activerecord_backgrounding_test.rb
│ └── sequel_backgrounding_test.rb
├── plugin
│ ├── activerecord_test.rb
│ ├── add_metadata_test.rb
│ ├── atomic_helpers_test.rb
│ ├── backgrounding_test.rb
│ ├── cached_attachment_data_test.rb
│ ├── column_test.rb
│ ├── data_uri_test.rb
│ ├── default_storage_test.rb
│ ├── default_url_test.rb
│ ├── delete_raw_test.rb
│ ├── derivation_endpoint_test.rb
│ ├── derivatives_test.rb
│ ├── determine_mime_type_test.rb
│ ├── download_endpoint_test.rb
│ ├── dynamic_storage_test.rb
│ ├── entity_test.rb
│ ├── form_assign_test.rb
│ ├── included_test.rb
│ ├── infer_extension_test.rb
│ ├── instrumentation_test.rb
│ ├── keep_files_test.rb
│ ├── metadata_attributes_test.rb
│ ├── mirroring_test.rb
│ ├── model_test.rb
│ ├── module_include_test.rb
│ ├── multi_cache_test.rb
│ ├── presign_endpoint_test.rb
│ ├── pretty_location_test.rb
│ ├── processing_test.rb
│ ├── rack_file_test.rb
│ ├── rack_response_test.rb
│ ├── recache_test.rb
│ ├── refresh_metadata_test.rb
│ ├── remote_url_test.rb
│ ├── remove_attachment_test.rb
│ ├── remove_invalid_test.rb
│ ├── restore_cached_data_test.rb
│ ├── sequel_test.rb
│ ├── signature_test.rb
│ ├── store_dimensions_test.rb
│ ├── tempfile_test.rb
│ ├── type_predicates_test.rb
│ ├── upload_endpoint_test.rb
│ ├── upload_options_test.rb
│ ├── url_options_test.rb
│ ├── urlsafe_serialization_test.rb
│ ├── validation_helpers_test.rb
│ ├── validation_test.rb
│ └── versions_test.rb
├── plugin_test.rb
├── shrine_test.rb
├── storage
│ ├── file_system_test.rb
│ ├── linter_test.rb
│ ├── memory_test.rb
│ └── s3_test.rb
├── support
│ ├── activerecord.rb
│ ├── fakeio.rb
│ ├── file_helper.rb
│ ├── logging_helper.rb
│ ├── sequel.rb
│ └── shrine_helper.rb
├── test_helper.rb
└── uploaded_file_test.rb
└── website
├── docusaurus.config.js
├── package-lock.json
├── package.json
├── sidebars.json
├── src
├── css
│ └── customTheme.css
└── pages
│ ├── _demo.md
│ ├── _sponsors.js
│ └── index.js
└── static
└── img
├── favicon.ico
└── logo.png
/.dockerignore:
--------------------------------------------------------------------------------
1 | */node_modules
2 | *.log
3 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches: [master]
6 | pull_request:
7 | branches: ["**"]
8 |
9 | env:
10 | RACK_ENV: development
11 | S3_REAL: true
12 | S3_REGION: us-east-1
13 | S3_BUCKET: minio-bucket
14 | S3_ACCESS_KEY_ID: weak_key
15 | S3_SECRET_ACCESS_KEY: weak_key
16 | S3_ENDPOINT: http://localhost:9000
17 | MINIO_ACCESS_KEY: weak_key
18 | MINIO_SECRET_KEY: weak_key
19 |
20 | jobs:
21 | tests:
22 | runs-on: ubuntu-latest
23 | strategy:
24 | fail-fast: false
25 | matrix:
26 | ruby:
27 | - "2.3"
28 | - "2.4"
29 | - "2.5"
30 | - "2.6"
31 | - "2.7"
32 | - "3.0"
33 | - "3.1"
34 | - "3.2"
35 | - "3.3"
36 | - "jruby-9.4"
37 |
38 | steps:
39 | - uses: actions/checkout@v3
40 |
41 | - name: Set up Ruby
42 | uses: ruby/setup-ruby@v1
43 | with:
44 | ruby-version: ${{ matrix.ruby }}
45 | bundler-cache: true
46 | rubygems: ${{ matrix.ruby <= '2.5' && '3.3.26' || 'latest' }}
47 |
48 | - name: Set up Minio
49 | run: |
50 | mkdir -p "${GITHUB_WORKSPACE}"/minio/data/minio-bucket
51 | wget -nc -O "${GITHUB_WORKSPACE}"/minio/minio https://dl.min.io/server/minio/release/linux-amd64/minio
52 | chmod +x "${GITHUB_WORKSPACE}"/minio/minio
53 | ${GITHUB_WORKSPACE}/minio/minio server ${GITHUB_WORKSPACE}/minio/data --address localhost:9000 &>${GITHUB_WORKSPACE}/minio/data/server.log &
54 |
55 | - name: Run tests
56 | run: bundle exec rake test
57 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | Gemfile.lock
2 | pkg/
3 | .env
4 | website/build
5 | coverage/
6 | node_modules
7 | .docusaurus
8 | gemfiles/*.lock
9 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | Issue Guidelines
2 | ================
3 |
4 | 1. Issues should only be created for things that are definitely bugs. If you
5 | are not sure that the behavior is a bug, ask about it on [Github Discussions] or [Discourse]. Otherwise Github gets overwhelmed with issues and it is very difficult for the maintainers to manage.
6 |
7 | 2. If you are sure it is a bug, then post a complete description of the issue,
8 | the simplest possible [self-contained example] showing the problem (please do review the link), and the full backtrace of any exception.
9 |
10 | Pull Request Guidelines
11 | =======================
12 |
13 | 1. Try to include tests for all new features and substantial bug
14 | fixes.
15 |
16 | 2. Try to include documentation for all new features. In most cases
17 | this should include RDoc method documentation, but updates to the
18 | README is also appropriate in some cases.
19 |
20 | 3. Follow the style conventions of the surrounding code. In most
21 | cases, this is standard ruby style.
22 |
23 | Understanding the codebase
24 | ==========================
25 |
26 | * The [Design of Shrine] guide gives a general overview of Shrine's core
27 | classes.
28 |
29 | * The [Creating a New Plugin] guide and the [Plugin system of Sequel and Roda]
30 | article explain how Shrine's plugin system works.
31 |
32 | * The [Notes on study of shrine implementation] article gives an in-depth
33 | walkthrough through the Shrine codebase.
34 |
35 | Running tests
36 | =============
37 |
38 | The test suite requires that you have the following installed:
39 |
40 | * [libmagic]
41 | * [SQLite]
42 | * [libvips] - please download the appropriate package suiting your operating system.
43 |
44 | With Hombrew this would be:
45 |
46 | ```sh
47 | $ brew install libmagic sqlite libvips
48 | ```
49 |
50 | The test suite is best run using Rake:
51 |
52 | ```
53 | $ rake test
54 | ```
55 |
56 | Code of Conduct
57 | ===============
58 |
59 | Everyone interacting in the Shrine project’s codebases, issue trackers, chat
60 | rooms, and mailing lists is expected to follow the [Shrine code of conduct].
61 |
62 | [Github Discussions]: https://github.com/shrinerb/shrine/discussions
63 | [Discourse]: https://discourse.shrinerb.com
64 | [Shrine code of conduct]: https://github.com/shrinerb/shrine/blob/master/CODE_OF_CONDUCT.md
65 | [libmagic]: https://github.com/threatstack/libmagic
66 | [libvips]: https://github.com/libvips/libvips/wiki
67 | [SQLite]: https://www.sqlite.org
68 | [Design of Shrine]: /doc/design.md#readme
69 | [Creating a New Plugin]: /doc/creating_plugins.md#readme
70 | [Plugin system of Sequel and Roda]: https://twin.github.io/the-plugin-system-of-sequel-and-roda/
71 | [Notes on study of shrine implementation]: https://bibwild.wordpress.com/2018/09/12/notes-on-study-of-shrine-implementation/
72 | [self-contained example]: https://github.com/shrinerb/shrine/blob/master/SELF_CONTAINED_EXAMPLE.md
73 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:8.11.4
2 |
3 | WORKDIR /app/website
4 |
5 | EXPOSE 3000 35729
6 | COPY ./docs /app/docs
7 | COPY ./website /app/website
8 | RUN yarn install
9 |
10 | CMD ["yarn", "start"]
11 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | gemspec
4 |
5 | gem "pry"
6 | gem "simplecov"
7 |
8 | gem "hanna", require: false
9 |
10 | gem "activerecord-jdbcsqlite3-adapter", "~> 70.0", platform: :jruby if RUBY_ENGINE == "jruby"
11 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015-2019 Janko Marohnić
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require "bundler/gem_tasks"
2 | require "rake/testtask"
3 | require "rdoc/task"
4 |
5 | Rake::TestTask.new do |t|
6 | t.libs << "test"
7 | t.test_files = FileList["test/**/*_test.rb"]
8 | t.warning = false
9 | end
10 |
11 | task default: :test
12 |
13 | RDoc::Task.new do |t|
14 | t.rdoc_dir = "website/build/rdoc"
15 | t.options += [
16 | "--line-numbers",
17 | "--title", "Shrine: Toolkit for file uploads",
18 | "--markup", "markdown",
19 | "--format", "hanna",
20 | "--main", "README.md",
21 | "--visibility", "public",
22 | ]
23 | t.rdoc_files.add Dir[
24 | "README.md",
25 | "CHANGELOG.md",
26 | "lib/**/*.rb",
27 | "doc/*.md",
28 | "doc/release_notes/*.md",
29 | ]
30 | end
31 |
32 | namespace :website do
33 | task :publish => :build do
34 | sh "git switch gh-pages"
35 | sh "cp -R website/build/* ."
36 | sh "git add --all"
37 | sh "git commit -m 'Update website'"
38 | sh "git push origin gh-pages"
39 | sh "git switch master"
40 | end
41 |
42 | task :build do
43 | sh "yarn build", chdir: "website"
44 | Rake::Task["rdoc"].invoke
45 | end
46 | end
47 |
--------------------------------------------------------------------------------
/bin/publish:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | cd website
4 | npm run build
5 | cd ..
6 | git switch gh-pages
7 | cp -R website/build/* .
8 | git add --all
9 | git commit -m "Update website"
10 | git push origin gh-pages
11 | git switch master
12 |
--------------------------------------------------------------------------------
/demo/.gitignore:
--------------------------------------------------------------------------------
1 | Gemfile.lock
2 | database.sqlite3
3 | public/uploads
4 |
--------------------------------------------------------------------------------
/demo/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | # Web framework
4 | gem "puma"
5 | gem "roda", "~> 3.36"
6 |
7 | # Rendering
8 | gem "tilt", ">= 2.0.8"
9 | gem "erubi"
10 | gem "forme", "~> 1.8"
11 |
12 | # Database
13 | gem "sequel"
14 | gem "sqlite3"
15 |
16 | # Attachments
17 | gem "shrine", path: ".."
18 | gem "aws-sdk-s3", "~> 1.14"
19 | gem "dotenv"
20 | gem "image_processing", "~> 1.8"
21 | gem "marcel"
22 | gem "fastimage"
23 | gem "dry-monitor"
24 |
25 | # Background jobs
26 | gem "sucker_punch", "~> 2.0"
27 |
28 | # Debugging
29 | gem "pry-byebug"
30 |
31 | # Testing
32 | gem "minitest"
33 | gem "capybara", "~> 3.33"
34 | gem "cuprite"
35 | gem "rake"
36 |
--------------------------------------------------------------------------------
/demo/Rakefile:
--------------------------------------------------------------------------------
1 | require "sequel"
2 | require "rake/testtask"
3 |
4 | Rake::TestTask.new do |t|
5 | t.libs << "test"
6 | t.test_files = FileList["test/**/*_test.rb"]
7 | t.warning = false
8 | end
9 |
10 | task default: :test
11 |
12 | namespace :db do
13 | task :migrate, [:version] do |task, args|
14 | db = Sequel.sqlite("database.sqlite3")
15 | Sequel.extension :migration
16 | version = Integer(args[:version]) if args[:version]
17 | Sequel::Migrator.apply(db, "db/migrations", version)
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/demo/app.rb:
--------------------------------------------------------------------------------
1 | require "roda"
2 |
3 | require "./routes/albums"
4 | require "./routes/direct_upload"
5 | require "./uploaders/image_uploader"
6 |
7 | class ShrineDemo < Roda
8 | plugin :public
9 | plugin :assets, css: "app.css", js: "app.js"
10 | plugin :run_handler
11 |
12 | use Rack::Session::Cookie, secret: "secret"
13 | plugin :route_csrf, check_header: true
14 |
15 | route do |r|
16 | r.public # serve static assets
17 | r.assets # serve dynamic assets
18 |
19 | check_csrf!
20 |
21 | # redirect '/' to '/albums'
22 | r.root do
23 | r.redirect "/albums", 301
24 | end
25 |
26 | # all '/albums'
27 | r.on "albums" do
28 | r.run Routes::Albums
29 | end
30 |
31 | r.on "derivations/image" do
32 | r.run ImageUploader.derivation_endpoint
33 | end
34 |
35 | # all other routes
36 | r.run Routes::DirectUpload, not_found: :pass
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/demo/assets/css/app.css:
--------------------------------------------------------------------------------
1 | .container {
2 | width: 800px;
3 | }
4 |
5 | .file-upload-preview[src=""] {
6 | visibility: hidden;
7 | }
8 |
--------------------------------------------------------------------------------
/demo/assets/js/app.js:
--------------------------------------------------------------------------------
1 | const singleFileUpload = (fileInput) => {
2 | const imagePreview = document.getElementById(fileInput.dataset.previewElement)
3 | const formGroup = fileInput.parentNode
4 |
5 | formGroup.removeChild(fileInput)
6 |
7 | const uppy = fileUpload(fileInput)
8 |
9 | uppy
10 | .use(Uppy.FileInput, {
11 | target: formGroup,
12 | locale: { strings: { chooseFiles: 'Choose file' } },
13 | })
14 | .use(Uppy.Informer, {
15 | target: formGroup,
16 | })
17 | .use(Uppy.ProgressBar, {
18 | target: imagePreview.parentNode,
19 | })
20 | .use(Uppy.ThumbnailGenerator, {
21 | thumbnailWidth: 600,
22 | })
23 |
24 | uppy.on('upload-success', (file, response) => {
25 | // set hidden field value to the uploaded file data so that it's submitted with the form as the attachment
26 | const hiddenInput = document.getElementById(fileInput.dataset.uploadResultElement)
27 | hiddenInput.value = uploadedFileData(file, response, fileInput)
28 | })
29 |
30 | uppy.on('thumbnail:generated', (file, preview) => {
31 | imagePreview.src = preview
32 | })
33 | }
34 |
35 | const multipleFileUpload = (fileInput) => {
36 | var formGroup = fileInput.parentNode
37 |
38 | var uppy = fileUpload(fileInput)
39 |
40 | uppy
41 | .use(Uppy.Dashboard, {
42 | target: formGroup,
43 | inline: true,
44 | height: 300,
45 | replaceTargetContent: true,
46 | })
47 |
48 | uppy.on('upload-success', (file, response) => {
49 | const hiddenField = document.createElement('input')
50 |
51 | hiddenField.type = 'hidden'
52 | hiddenField.name = 'album[photos_attributes]['+ Math.random().toString(36).substr(2, 9) + '][image]'
53 | hiddenField.value = uploadedFileData(file, response, fileInput)
54 |
55 | document.querySelector('form').appendChild(hiddenField)
56 | })
57 | }
58 |
59 | const fileUpload = (fileInput) => {
60 | const uppy = new Uppy.Core({
61 | id: fileInput.id,
62 | autoProceed: true,
63 | restrictions: {
64 | allowedFileTypes: fileInput.accept.split(','),
65 | },
66 | })
67 |
68 | if (fileInput.dataset.uploadServer == 's3') {
69 | uppy.use(Uppy.AwsS3, {
70 | companionUrl: '/', // will call Shrine's presign endpoint mounted on `/s3/params`
71 | })
72 | } else {
73 | uppy.use(Uppy.XHRUpload, {
74 | endpoint: '/upload', // Shrine's upload endpoint
75 | headers: { 'X-CSRF-Token': fileInput.dataset.uploadCsrfToken }
76 | })
77 | }
78 |
79 | return uppy
80 | }
81 |
82 | const uploadedFileData = (file, response, fileInput) => {
83 | if (fileInput.dataset.uploadServer == 's3') {
84 | // construct uploaded file data in the format that Shrine expects
85 | return JSON.stringify({
86 | id: file.meta['key'].match(/^cache\/(.+)/)[1], // object key without prefix
87 | storage: 'cache',
88 | metadata: {
89 | size: file.size,
90 | filename: file.name,
91 | mime_type: file.type,
92 | }
93 | })
94 | } else {
95 | return JSON.stringify(response.body)
96 | }
97 | }
98 |
99 | document.querySelectorAll('input[type=file]').forEach((fileInput) => {
100 | if (fileInput.multiple) {
101 | multipleFileUpload(fileInput)
102 | } else {
103 | singleFileUpload(fileInput)
104 | }
105 | })
106 |
--------------------------------------------------------------------------------
/demo/config.ru:
--------------------------------------------------------------------------------
1 | require "./app"
2 |
3 | run ShrineDemo
4 |
--------------------------------------------------------------------------------
/demo/config/credentials.rb:
--------------------------------------------------------------------------------
1 | require "dotenv"
2 |
3 | Dotenv.load!
4 |
--------------------------------------------------------------------------------
/demo/config/sequel.rb:
--------------------------------------------------------------------------------
1 | require "sequel"
2 |
3 | DB = Sequel.sqlite("database.sqlite3")
4 |
5 | Sequel::Model.plugin :nested_attributes
6 | Sequel::Model.plugin :association_dependencies
7 | Sequel::Model.plugin :validation_helpers
8 |
9 | Sequel::Model.plugin :forme
10 |
--------------------------------------------------------------------------------
/demo/config/shrine.rb:
--------------------------------------------------------------------------------
1 | # This is a general base configuration for Shrine in the app.
2 | # It's typically placed in a `config` and/or `initializers` folder.
3 |
4 | require "./config/credentials"
5 | require "shrine"
6 | require "dry-monitor"
7 |
8 | # needed by `backgrounding` plugin
9 | require "./jobs/attachment/promote_job"
10 | require "./jobs/attachment/destroy_job"
11 |
12 | # use S3 for production and local file for other environments
13 | if ENV["RACK_ENV"] == "production"
14 | require "shrine/storage/s3"
15 |
16 | s3_options = {
17 | bucket: ENV.fetch("S3_BUCKET"),
18 | region: ENV.fetch("S3_REGION"),
19 | access_key_id: ENV.fetch("S3_ACCESS_KEY_ID"),
20 | secret_access_key: ENV.fetch("S3_SECRET_ACCESS_KEY"),
21 | }
22 |
23 | # both `cache` and `store` storages are needed
24 | Shrine.storages = {
25 | cache: Shrine::Storage::S3.new(prefix: "cache", **s3_options),
26 | store: Shrine::Storage::S3.new(**s3_options),
27 | }
28 | else
29 | require "shrine/storage/file_system"
30 |
31 | # both `cache` and `store` storages are needed
32 | Shrine.storages = {
33 | cache: Shrine::Storage::FileSystem.new("public", prefix: "uploads/cache"),
34 | store: Shrine::Storage::FileSystem.new("public", prefix: "uploads"),
35 | }
36 | end
37 |
38 | Shrine.plugin :sequel
39 | Shrine.plugin :instrumentation, notifications: Dry::Monitor::Notifications.new(:test)
40 | Shrine.plugin :determine_mime_type, analyzer: :marcel, log_subscriber: nil
41 | Shrine.plugin :cached_attachment_data
42 | Shrine.plugin :restore_cached_data
43 | Shrine.plugin :derivatives # eager processing
44 | Shrine.plugin :derivation_endpoint, # on-the-fly processing
45 | secret_key: "secret"
46 |
47 | if ENV["RACK_ENV"] == "production"
48 | Shrine.plugin :presign_endpoint, presign_options: -> (request) {
49 | # Uppy will send the "filename" and "type" query parameters
50 | filename = request.params["filename"]
51 | type = request.params["type"]
52 |
53 | {
54 | content_disposition: ContentDisposition.inline(filename), # set download filename
55 | content_type: type, # set content type
56 | content_length_range: 0..(10*1024*1024), # limit upload size to 10 MB
57 | }
58 | }
59 | else
60 | Shrine.plugin :upload_endpoint
61 | end
62 |
63 | # delay promoting and deleting files to a background job (`backgrounding` plugin)
64 | Shrine.plugin :backgrounding
65 |
66 | Shrine::Attacher.promote_block do
67 | Attachment::PromoteJob.perform_async(
68 | self.class.name,
69 | record.class.name,
70 | record.id,
71 | name,
72 | file_data,
73 | )
74 | end
75 |
76 | Shrine::Attacher.destroy_block do
77 | Attachment::DestroyJob.perform_async(
78 | self.class.name,
79 | data,
80 | )
81 | end
82 |
--------------------------------------------------------------------------------
/demo/db/migrations/001_create_albums.rb:
--------------------------------------------------------------------------------
1 | Sequel.migration do
2 | change do
3 | create_table(:albums) do
4 | primary_key :id
5 |
6 | String :name
7 | String :cover_photo_data # Shrine will store the file info here for the album's cover_photo
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/demo/db/migrations/002_create_photos.rb:
--------------------------------------------------------------------------------
1 | Sequel.migration do
2 | change do
3 | create_table(:photos) do
4 | primary_key :id
5 | foreign_key :album_id, :albums
6 |
7 | String :title
8 | String :image_data # Shrine will store the file info here for the photo's image
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/demo/jobs/attachment/destroy_job.rb:
--------------------------------------------------------------------------------
1 | require "sucker_punch"
2 |
3 | module Attachment
4 | class DestroyJob
5 | include SuckerPunch::Job
6 |
7 | def perform(attacher_class, data)
8 | attacher_class = Object.const_get(attacher_class)
9 |
10 | attacher = attacher_class.from_data(data)
11 | attacher.destroy
12 | end
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/demo/jobs/attachment/promote_job.rb:
--------------------------------------------------------------------------------
1 | require "sucker_punch"
2 |
3 | module Attachment
4 | class PromoteJob
5 | include SuckerPunch::Job
6 |
7 | def perform(attacher_class, record_class, record_id, name, file_data)
8 | attacher_class = Object.const_get(attacher_class)
9 | record = Object.const_get(record_class).with_pk!(record_id)
10 |
11 | attacher = attacher_class.retrieve(model: record, name: name, file: file_data)
12 | attacher.create_derivatives if record.is_a?(Album)
13 | attacher.atomic_promote
14 | rescue Shrine::AttachmentChanged
15 | # attachment has changed, so nothing to do
16 | rescue Sequel::NoMatchingRow, Sequel::NoExistingObject
17 | # record has been deleted, so nothing to do
18 | end
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/demo/lib/generate_thumbnail.rb:
--------------------------------------------------------------------------------
1 | require "image_processing/mini_magick"
2 |
3 | class GenerateThumbnail
4 | def self.call(file, width, height)
5 | magick = ImageProcessing::MiniMagick.source(file)
6 | magick.resize_to_limit!(width, height)
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/demo/models/album.rb:
--------------------------------------------------------------------------------
1 | require "./config/sequel"
2 | require "./uploaders/image_uploader"
3 |
4 | class Album < Sequel::Model
5 | one_to_many :photos
6 | nested_attributes :photos, destroy: true
7 | add_association_dependencies photos: :destroy
8 |
9 | include ImageUploader::Attachment(:cover_photo) # ImageUploader will attach and manage `cover_photo`
10 |
11 | def validate
12 | super
13 | validates_presence [:name, :cover_photo] # Normal model validations - optional
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/demo/models/photo.rb:
--------------------------------------------------------------------------------
1 | require "./config/sequel"
2 | require "./uploaders/image_uploader"
3 |
4 | class Photo < Sequel::Model
5 | include ImageUploader::Attachment(:image) # ImageUploader will attach and manage `image`
6 | end
7 |
--------------------------------------------------------------------------------
/demo/routes/albums.rb:
--------------------------------------------------------------------------------
1 | require "./routes/base"
2 | require "./models/album"
3 | require "./models/photo"
4 |
5 | module Routes
6 | class Albums < Base
7 | route do |r|
8 | # '/albums'
9 | r.is do
10 | # GET '/albums'
11 | r.get do
12 | albums = Album.all
13 | view("albums/index", locals: { albums: albums })
14 | end
15 |
16 | # POST '/albums'
17 | r.post do
18 | album = Album.new(params[:album])
19 |
20 | if album.valid?
21 | album.save
22 | r.redirect album_path(album)
23 | else
24 | view("albums/new", locals: { album: album })
25 | end
26 | end
27 | end
28 |
29 | # GET '/albums/new'
30 | r.get "new" do
31 | album = Album.new
32 | view("albums/new", locals: { album: album })
33 | end
34 |
35 | # '/albums/:id'
36 | r.is Integer do |album_id|
37 | album = Album[album_id] or not_found!
38 |
39 | # GET '/albums/:id'
40 | r.get do
41 | view("albums/show", locals: { album: album })
42 | end
43 |
44 | # PUT '/albums/:id'
45 | r.put do
46 | album.set(params[:album])
47 |
48 | if album.valid?
49 | album.save
50 | r.redirect album_path(album)
51 | else
52 | view("albums/show", locals: { album: album })
53 | end
54 | end
55 |
56 | # DELETE '/albums/:id'
57 | r.delete do
58 | album.destroy
59 | r.redirect albums_path
60 | end
61 | end
62 | end
63 | end
64 | end
65 |
--------------------------------------------------------------------------------
/demo/routes/base.rb:
--------------------------------------------------------------------------------
1 | require "roda"
2 |
3 | # Base Roda route configuration
4 | module Routes
5 | class Base < Roda
6 | plugin :environments
7 |
8 | plugin :render
9 | plugin :forme_route_csrf
10 | plugin :partials
11 | plugin :assets, css: "app.css", js: "app.js"
12 |
13 | use Rack::MethodOverride
14 | plugin :all_verbs
15 |
16 | plugin :indifferent_params
17 | plugin :path
18 |
19 | path(:albums, "/albums")
20 | path(:new_album, "/albums/new")
21 | path(:album) { |album| "/albums/#{album.id}" }
22 |
23 | def not_found!
24 | response.status = 404
25 | request.halt
26 | end
27 |
28 | def upload_server
29 | if self.class.production?
30 | :s3
31 | else
32 | :app
33 | end
34 | end
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/demo/routes/direct_upload.rb:
--------------------------------------------------------------------------------
1 | require "./routes/base"
2 | require "./config/shrine"
3 |
4 | module Routes
5 | class DirectUpload < Base
6 | if production?
7 | route do |r|
8 | # Only '/s3/params'
9 | r.is "s3/params" do
10 | # GET /s3/params
11 | r.run Shrine.presign_endpoint(:cache)
12 | end
13 | end
14 | else
15 | # In development and test environment we're using filesystem storage
16 | # for speed, so on the client side we'll upload files to our app.
17 | route do |r|
18 | # Only '/upload'
19 | r.is "upload" do
20 | # POST /upload
21 | r.run Shrine.upload_endpoint(:cache)
22 | end
23 | end
24 | end
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/demo/test/acceptance_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | describe "Album form" do
4 | it "handles album cover photo upload" do
5 | visit "/"
6 | click_on "New Album"
7 | fill_in "Name", with: "My Album"
8 | find(".uppy-FileInput-input").set(fixture("image.jpg"))
9 | assert_no_selector "#album-cover-photo-upload-result[value=\"\"]"
10 | uploaded_file_data = find("#album-cover-photo-upload-result").value
11 | assert_equal %w[id storage metadata], JSON.parse(uploaded_file_data).keys
12 | assert_no_selector "#preview-cover-photo[value=\"\"]"
13 | preview_url = find("#preview-cover-photo")[:src]
14 | refute_empty preview_url
15 |
16 | click_on "Save"
17 | assert_no_selector ".validation-errors"
18 | preview_url = find("#preview-cover-photo")[:src]
19 | refute_empty preview_url
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/demo/test/fixtures/image.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shrinerb/shrine/1ce6da4be0ca62ecd5776f107fcd0d657b773f28/demo/test/fixtures/image.jpg
--------------------------------------------------------------------------------
/demo/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | require "bundler/setup"
2 |
3 | ENV["MT_NO_EXPECTATIONS"] = "1" # disable Minitest's expectations monkey-patches
4 |
5 | require "minitest/autorun"
6 | require "minitest/pride"
7 |
8 | require "capybara"
9 | require "capybara/dsl"
10 | require "capybara/minitest"
11 | require "capybara/cuprite"
12 |
13 | require "./app"
14 | require "sucker_punch/testing/inline"
15 |
16 | Capybara.register_driver :cuprite do |app|
17 | Capybara::Cuprite::Driver.new(app, window_size: [1200, 800])
18 | end
19 |
20 | Capybara.default_driver = :cuprite
21 | Capybara.app = ShrineDemo
22 | Capybara.ignore_hidden_elements = false
23 |
24 | class Minitest::Test
25 | include Capybara::DSL
26 | include Capybara::Minitest::Assertions
27 |
28 | def teardown
29 | Capybara.reset_sessions!
30 | DB[:photos].truncate
31 | DB[:albums].truncate
32 | end
33 |
34 | def fixture(filename)
35 | File.expand_path("test/fixtures/#{filename}")
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/demo/uploaders/image_uploader.rb:
--------------------------------------------------------------------------------
1 | # This is a subclass of Shrine base that will be further configured for it's requirements.
2 | # This will be included in the model to manage the file.
3 |
4 | require "./config/shrine"
5 | require "./lib/generate_thumbnail"
6 |
7 | class ImageUploader < Shrine
8 | ALLOWED_TYPES = %w[image/jpeg image/png image/webp]
9 | MAX_SIZE = 10*1024*1024 # 10 MB
10 | MAX_DIMENSIONS = [5000, 5000] # 5000x5000
11 |
12 | THUMBNAILS = {
13 | small: [300, 300],
14 | medium: [600, 600],
15 | large: [800, 800],
16 | }
17 |
18 | plugin :remove_attachment
19 | plugin :pretty_location
20 | plugin :validation_helpers
21 | plugin :store_dimensions, log_subscriber: nil
22 | plugin :derivation_endpoint, prefix: "derivations/image"
23 |
24 | # File validations (requires `validation_helpers` plugin)
25 | Attacher.validate do
26 | validate_size 0..MAX_SIZE
27 |
28 | if validate_mime_type ALLOWED_TYPES
29 | validate_max_dimensions MAX_DIMENSIONS
30 | end
31 | end
32 |
33 | # Thumbnails processor (requires `derivatives` plugin)
34 | Attacher.derivatives do |original|
35 | THUMBNAILS.transform_values do |(width, height)|
36 | GenerateThumbnail.call(original, width, height)
37 | end
38 | end
39 |
40 | # Default to dynamic thumbnail URL (requires `default_url` plugin)
41 | Attacher.default_url do |derivative: nil, **|
42 | file&.derivation_url(:thumbnail, *THUMBNAILS.fetch(derivative)) if derivative
43 | end
44 |
45 | # Dynamic thumbnail definition (requires `derivation_endpoint` plugin)
46 | derivation :thumbnail do |file, width, height|
47 | GenerateThumbnail.call(file, width.to_i, height.to_i)
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/demo/views/albums/_form.erb:
--------------------------------------------------------------------------------
1 | <% form album, { action: form_action, enctype: "multipart/form-data" }, labeler: :explicit do |f| %>
2 | <%= f.input :hidden, name: "_method", value: form_method, obj: nil %> <%# # used in tandem with Rack::MethodOverride to enable PUT requests %>
3 |
4 |
5 | <%= f.input :name, class: "form-control", label_attr: { class: "form-label" } %>
6 |
7 |
8 |
9 |
10 | <%= f.input :cover_photo,
11 | type: :file,
12 | attr: {
13 | accept: ImageUploader::ALLOWED_TYPES.join(",")
14 | },
15 | label_attr: {
16 | class: "form-label"
17 | },
18 | data: {
19 | upload_server: upload_server,
20 | upload_csrf_token: csrf_token("/upload"),
21 | preview_element: "preview-cover-photo",
22 | upload_result_element: "album-cover-photo-upload-result"
23 | },
24 | dasherize_data: true %>
25 |
26 |
27 | <%= f.input :cover_photo,
28 | type: :hidden,
29 | error_handler: false,
30 | value: album.cached_cover_photo_data,
31 | id: "album-cover-photo-upload-result" %>
32 |
33 |
34 |
35 |
36 |
 %>)
40 |
41 |
42 | <% unless album.new? %>
43 |
44 |
45 | <%= f.input :photos,
46 | type: :file,
47 | label: "Select photos",
48 | attr: {
49 | multiple: true,
50 | accept: ImageUploader::ALLOWED_TYPES.join(",")
51 | },
52 | label_attr: {
53 | class: "form-label"
54 | },
55 | data: {
56 | upload_server: upload_server,
57 | upload_csrf_token: csrf_token("/upload")
58 | },
59 | dasherize_data: true %>
60 |
61 | <% end %>
62 |
63 |
64 | <% f.subform :photos, legend: nil do %>
65 | <%= partial("albums/photo", locals: { photo: f.obj, f: f }) %>
66 | <% end %>
67 |
68 |
69 |
70 | ← Back to albums
71 | <% end %>
72 |
--------------------------------------------------------------------------------
/demo/views/albums/_photo.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
 %>)
6 |
7 |
8 |
9 | <%= f.input :title, class: "form-control", label_attr: { class: "form-label" } %>
10 |
11 |
12 | <%= f.input :_delete, type: :checkbox, class: "form-check-input", label: "Remove photo", label_attr: { class: "form-check-label" } %>
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/demo/views/albums/index.erb:
--------------------------------------------------------------------------------
1 | Albums
2 |
3 |
4 |
5 |
6 |
7 |
8 | Cover |
9 | Name |
10 | Actions |
11 |
12 |
13 |
14 | <% albums.each do |album| %>
15 |
16 |  %>) |
17 | <%= album.name %> |
18 | Edit |
19 |
20 | <% end %>
21 |
22 |
23 |
--------------------------------------------------------------------------------
/demo/views/albums/new.erb:
--------------------------------------------------------------------------------
1 | New album
2 |
3 | <%= partial("albums/form", locals: { album: album, form_action: albums_path, form_method: "post" }) %>
4 |
--------------------------------------------------------------------------------
/demo/views/albums/show.erb:
--------------------------------------------------------------------------------
1 | <%= album.name %>
2 |
3 | <%= partial("albums/form", locals: { album: album, form_action: album_path(album), form_method: "put" }) %>
4 |
--------------------------------------------------------------------------------
/demo/views/layout.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Shrine Demo
5 |
6 |
7 |
8 |
9 |
10 | <%= assets(:css) %>
11 |
12 |
13 |
14 |
15 | <%= yield %>
16 |
17 |
18 |
19 | <%= assets(:js) %>
20 |
21 |
22 |
--------------------------------------------------------------------------------
/doc/changing_storage.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: changing-storage
3 | title: Migrating File Storage
4 | ---
5 |
6 | This guides shows how to move file attachments to a different storage in
7 | production, with zero downtime.
8 |
9 | Let's assume we have a `Photo` model with an `image` file attachment stored
10 | in AWS S3 storage:
11 |
12 | ```rb
13 | Shrine.storages = {
14 | cache: Shrine::Storage::S3.new(...),
15 | store: Shrine::Storage::S3.new(...),
16 | }
17 |
18 | Shrine.plugin :activerecord
19 | ```
20 | ```rb
21 | class ImageUploader < Shrine
22 | # ...
23 | end
24 | ```
25 | ```rb
26 | class Photo < ActiveRecord::Base
27 | include ImageUploader::Attachment(:image)
28 | end
29 | ```
30 |
31 | Let's also assume that we're migrating from AWS S3 to Google Cloud Storage, and
32 | we've added the new storage to `Shrine.storages`:
33 |
34 | ```rb
35 | Shrine.storages = {
36 | ...
37 | store: Shrine::Storage::S3.new(...),
38 | gcs: Shrine::Storage::GoogleCloudStorage.new(...),
39 | }
40 | ```
41 |
42 | ## 1. Mirror upload and delete operations
43 |
44 | The first step is to start mirroring uploads and deletes made on your current
45 | storage to the new storage. We can do this by loading the `mirroring` plugin:
46 |
47 | ```rb
48 | Shrine.plugin :mirroring, mirror: { store: :gcs }
49 | ```
50 |
51 | Put the above code in an initializer and deploy it.
52 |
53 | You can additionally delay the mirroring into a [background job][mirroring
54 | backgrounding] for better performance.
55 |
56 | ## 2. Copy the files
57 |
58 | Next step is to copy all remaining files from current storage into the new
59 | storage using the following script. It fetches the photos in batches, downloads
60 | the image, and re-uploads it to the new storage.
61 |
62 | ```rb
63 | Photo.find_each do |photo|
64 | attacher = photo.image_attacher
65 |
66 | next unless attacher.stored?
67 |
68 | attacher.file.trigger_mirror_upload
69 |
70 | # if using derivatives
71 | attacher.map_derivative(attacher.derivatives) do |_, derivative|
72 | derivative.trigger_mirror_upload
73 | end
74 | end
75 | ```
76 |
77 | Now the new storage should have all files the current storage has, and new
78 | uploads will continue being mirrored to the new storage.
79 |
80 | ## 3. Update storage
81 |
82 | Once all the files are copied over to the new storage, everything should be
83 | ready for us to update the storage in the Shrine configuration. We can keep
84 | mirroring, in case the change would need to reverted.
85 |
86 | ```rb
87 | Shrine.storages = {
88 | ...
89 | store: Shrine::Storage::GoogleCloudStorage.new(...),
90 | s3: Shrine::Storage::S3.new(...),
91 | }
92 |
93 | Shrine.plugin :mirroring, mirror: { store: :s3 } # mirror to :s3 storage
94 | ```
95 |
96 | ## 4. Remove mirroring
97 |
98 | Once everything is looking good, we can remove the mirroring:
99 |
100 | ```diff
101 | Shrine.storages = {
102 | ...
103 | store: Shrine::Storage::GoogleCloudStorage.new(...),
104 | - s3: Shrine::Storage::S3.new(...),
105 | }
106 |
107 | - Shrine.plugin :mirroring, mirror: { store: :s3 } # mirror to :s3 storage
108 | ```
109 |
110 | [mirroring backgrounding]: https://shrinerb.com/docs/plugins/mirroring#backgrounding
111 |
--------------------------------------------------------------------------------
/doc/creating_plugins.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: creating-plugins
3 | title: Writing a Plugin
4 | ---
5 |
6 | Shrine has a lot of plugins built-in, but you can use Shrine's plugin system to
7 | create your own.
8 |
9 | ## Definition
10 |
11 | Simply put, a plugin is a module:
12 |
13 | ```rb
14 | module MyPlugin
15 | # ...
16 | end
17 |
18 | Shrine.plugin MyPlugin
19 | ```
20 |
21 | If you would like to load plugins with a symbol (like you already do with
22 | plugins that ship with Shrine), you need to put the plugin in
23 | `shrine/plugins/my_plugin.rb` in your load path and register it:
24 |
25 | ```rb
26 | # shrine/plugins/my_plugin.rb
27 | class Shrine
28 | module Plugins
29 | module MyPlugin
30 | # ...
31 | end
32 |
33 | register_plugin(:my_plugin, MyPlugin)
34 | end
35 | end
36 | ```
37 | ```rb
38 | Shrine.plugin :my_plugin
39 | ```
40 |
41 | ## Methods
42 |
43 | The way to make plugins actually extend Shrine's core classes is by defining
44 | special modules inside the plugin. Here's a list of all "special" modules:
45 |
46 | ```rb
47 | InstanceMethods # gets included into `Shrine`
48 | ClassMethods # gets extended into `Shrine`
49 | AttachmentMethods # gets included into `Shrine::Attachment`
50 | AttachmentClassMethods # gets extended into `Shrine::Attachment`
51 | AttacherMethods # gets included into `Shrine::Attacher`
52 | AttacherClassMethods # gets extended into `Shrine::Attacher`
53 | FileMethods # gets included into `Shrine::UploadedFile`
54 | FileClassMethods # gets extended into `Shrine::UploadedFile`
55 | ```
56 |
57 | For example, this is how you would make your plugin add some logging to
58 | uploading:
59 |
60 | ```rb
61 | module MyPlugin
62 | module InstanceMethods
63 | def upload(io, **options)
64 | time = Time.now
65 | result = super
66 | duration = Time.now - time
67 | puts "Upload duration: #{duration}s"
68 | end
69 | end
70 | end
71 | ```
72 |
73 | Notice that we can call `super` to get the original behaviour.
74 |
75 | ## Configuration
76 |
77 | You'll likely want to make your plugin configurable. You can do that by
78 | overriding the `.configure` class method and storing received options into
79 | `Shrine.opts`:
80 |
81 | ```rb
82 | module MyPlugin
83 | def self.configure(uploader, **opts)
84 | uploader.opts[:my_plugin] ||= {}
85 | uploader.opts[:my_plugin].merge!(opts)
86 | end
87 |
88 | module InstanceMethods
89 | def upload(io, **options)
90 | opts[:my_plugin] #=> { ... }
91 | # ...
92 | end
93 | end
94 | end
95 | ```
96 |
97 | Users can now pass these configuration options when loading your plugin:
98 |
99 | ```rb
100 | Shrine.plugin :my_plugin, foo: "bar"
101 | ```
102 |
103 | ## Dependencies
104 |
105 | If your plugin depends on other plugins, you can load them inside of
106 | `.load_dependencies`:
107 |
108 | ```rb
109 | module MyPlugin
110 | def self.load_dependencies(uploader, **opts)
111 | uploader.plugin :derivatives # depends on the derivatives plugin
112 | end
113 | end
114 | ```
115 |
116 | The dependencies will get loaded before your plugin, allowing you to override
117 | methods of your dependencies in your method modules.
118 |
119 | The same configuration options passed to `.configure` are passed to
120 | `.load_dependencies` as well.
121 |
--------------------------------------------------------------------------------
/doc/external/misc.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Miscellaneous
3 | ---
4 |
5 | ## Demos
6 |
7 | | Demo | Description |
8 | | :--- | :---------- |
9 | | [Dropzone demo](https://github.com/codyeatworld/example-shrine-dropzone) | Shows direct upload using [Dropzone.js] |
10 | | [Hanami demo](https://github.com/katafrakt/hanami-shrine-example) | Shows file attachment in [Hanami] |
11 | | [Crop demo](https://github.com/shrinerb/shrine-crop-example) | Shows image cropping using [Cropper.js] |
12 | | [Rails demo](https://github.com/erikdahlstrand/shrine-rails-example) | Shows direct upload in [Rails] |
13 | | [Resumable uploads demo](https://github.com/shrinerb/shrine-tus-demo) | Shows resumable direct upload on [tus] |
14 | | [Roda demo (official)](https://github.com/shrinerb/shrine/tree/master/demo) | Shows direct upload in [Roda] |
15 | | [rom-rb & dry-rb demo](https://github.com/shrinerb/shrine-rom/tree/master/demo) | Shows file attachment with [rom-rb] and [dry-rb] |
16 | | [Transloadit demo](https://github.com/shrinerb/shrine-transloadit/tree/master/demo) | Shows file processing using [Transloadit] |
17 |
18 | ## Projects
19 |
20 | | Project | Description |
21 | | :------ | :---------- |
22 | | [CortexCMS](https://docs.cortexcms.org) | An open source, enterprise content management and distribution platform |
23 |
24 | [Dropzone.js]: https://www.dropzonejs.com/
25 | [Hanami]: https://hanamirb.org/
26 | [Cropper.js]: https://github.com/fengyuanchen/cropperjs
27 | [Rails]: https://rubyonrails.org/
28 | [tus]: https://tus.io/
29 | [Roda]: https://roda.jeremyevans.net/
30 | [rom-rb]: https://rom-rb.org/
31 | [dry-rb]: https://dry-rb.org/
32 | [Transloadit]: https://transloadit.com/
33 |
--------------------------------------------------------------------------------
/doc/plugins/cached_attachment_data.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Cached Attachment Data
3 | ---
4 |
5 | The [`cached_attachment_data`][cached_attachment_data] plugin adds the ability
6 | to retain the cached file across form redisplays, which means the file doesn't
7 | have to be reuploaded in case of validation errors.
8 |
9 | ```rb
10 | plugin :cached_attachment_data
11 | ```
12 |
13 | The plugin adds `#cached__data` to the model, which returns the
14 | cached file as JSON, and should be used to set the value of the hidden form
15 | field.
16 |
17 | ```rb
18 | photo.cached_image_data #=> '{"id":"38k25.jpg","storage":"cache","metadata":{...}}'
19 | ```
20 |
21 | This method delegates to `Attacher#cached_data`:
22 |
23 | ```rb
24 | attacher.cached_data #=> '{"id":"38k25.jpg","storage":"cache","metadata":{...}}'
25 | ```
26 |
27 | [cached_attachment_data]: https://github.com/shrinerb/shrine/blob/master/lib/shrine/plugins/cached_attachment_data.rb
28 |
--------------------------------------------------------------------------------
/doc/plugins/default_storage.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Default Storage
3 | ---
4 |
5 | The [`default_storage`][default_storage] plugin allows you to change the
6 | default temporary and permanent storage a `Shrine::Attacher` object will use
7 | (the default is `:cache` and `:store`).
8 |
9 | ```rb
10 | plugin :default_storage, cache: :other_cache, store: :other_store
11 | ```
12 |
13 | If you want the storage to be dynamic based on `Attacher` data, you can use a
14 | block, and it will be evaluated in context of the `Attacher` instance:
15 |
16 | ```rb
17 | plugin :default_storage, store: -> {
18 | if record.is_a?(Photo)
19 | :photo_store
20 | else
21 | :store
22 | end
23 | }
24 | ```
25 |
26 | You can also set default storage with `Attacher#default_cache` and
27 | `Attacher#default_store`:
28 |
29 | ```rb
30 | # default temporary storage
31 | Attacher.default_cache :other_cache
32 | # or
33 | Attacher.default_cache { :other_cache }
34 |
35 | # default permanent storage
36 | Attacher.default_store :other_store
37 | # or
38 | Attacher.default_store { :other_store }
39 | ```
40 |
41 | The dynamic block is useful in combination with the
42 | [`dynamic_storage`][dynamic_storage] plugin.
43 |
44 | [default_storage]: https://github.com/shrinerb/shrine/blob/master/lib/shrine/plugins/default_storage.rb
45 | [dynamic_storage]: https://shrinerb.com/docs/plugins/dynamic_storage
46 |
--------------------------------------------------------------------------------
/doc/plugins/default_url.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Default URL
3 | ---
4 |
5 | The [`default_url`][default_url] plugin allows setting the URL which will be
6 | returned when there is no attached file.
7 |
8 | ```rb
9 | plugin :default_url
10 |
11 | Attacher.default_url do |**options|
12 | "/#{name}/missing.jpg"
13 | end
14 | ```
15 |
16 | The `Attacher#url` method will return the default URL when attachment is
17 | missing:
18 |
19 | ```rb
20 | user.avatar_url #=> "/avatar/missing.jpg"
21 | # or
22 | attacher.url #=> "/avatar/missing.jpg"
23 | ```
24 |
25 | Any URL options passed will be available in the default URL block:
26 |
27 | ```rb
28 | attacher.url(foo: "bar")
29 | ```
30 | ```rb
31 | Attacher.default_url do |**options|
32 | options #=> { foo: "bar" }
33 | end
34 | ```
35 |
36 | The default URL block is evaluated in the context of an instance of
37 | `Shrine::Attacher`.
38 |
39 | ```rb
40 | Attacher.default_url do |**options|
41 | self #=> #
42 |
43 | file #=> #
44 | name #=> :avatar
45 | record #=> #
46 | context #=> { ... }
47 |
48 | # ...
49 | end
50 | ```
51 |
52 | ## Host
53 |
54 | If the default URL is relative, the URL host can be specified via the `:host`
55 | option:
56 |
57 | ```rb
58 | plugin :default_url, host: "https://example.com"
59 | ```
60 | ```rb
61 | attacher.url #=> "https://example.com/avatar/missing.jpg"
62 | ```
63 |
64 | [default_url]: https://github.com/shrinerb/shrine/blob/master/lib/shrine/plugins/default_url.rb
65 |
--------------------------------------------------------------------------------
/doc/plugins/delete_raw.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Delete Raw
3 | ---
4 |
5 | The [`delete_raw`][delete_raw] plugin will automatically delete raw files that
6 | have been uploaded. This is especially useful when doing processing, to ensure
7 | that temporary files have been deleted after upload.
8 |
9 | ```rb
10 | plugin :delete_raw
11 | ```
12 |
13 | By default any raw file that was uploaded will be deleted, but you can limit
14 | this only to files uploaded to certain storages:
15 |
16 | ```rb
17 | plugin :delete_raw, storages: [:store]
18 | ```
19 |
20 | If you want to skip deletion for a certain upload, you can pass `delete: false`
21 | to the uploader:
22 |
23 | ```rb
24 | uploader.upload(file, delete: false)
25 | ```
26 |
27 | [delete_raw]: https://github.com/shrinerb/shrine/blob/master/lib/shrine/plugins/delete_raw.rb
28 |
--------------------------------------------------------------------------------
/doc/plugins/dynamic_storage.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Dynamic Storage
3 | ---
4 |
5 | The [`dynamic_storage`][dynamic_storage] plugin allows you to register a
6 | storage using a regex, and evaluate the storage class dynamically depending on
7 | the regex.
8 |
9 | Example:
10 |
11 | ```rb
12 | plugin :dynamic_storage
13 |
14 | storage /store_(\w+)/ do |match|
15 | Shrine::Storage::S3.new(bucket: match[1])
16 | end
17 | ```
18 |
19 | The above example uses S3 storage where the bucket name depends on the storage
20 | name suffix. For example, `:store_foo` will use S3 storage which saves files to
21 | the bucket "foo". The block is yielded an instance of `MatchData`.
22 |
23 | This can be useful in combination with the `default_storage` plugin.
24 |
25 | [dynamic_storage]: https://github.com/shrinerb/shrine/blob/master/lib/shrine/plugins/dynamic_storage.rb
26 |
--------------------------------------------------------------------------------
/doc/plugins/form_assign.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Form Assign
3 | ---
4 |
5 | The [`form_assign`][form_assign] plugin allows attaching file from form params
6 | without a form object.
7 |
8 | ```rb
9 | plugin :form_assign
10 | ```
11 |
12 | The `Attacher#form_assign` method will detect the file param and assign it to
13 | the attacher:
14 |
15 | ```rb
16 | attacher = photo.image_attacher
17 | attacher.form_assign({ "image" => file, "title" => "...", "description" => "..." })
18 | attacher.file #=> #
19 | ```
20 |
21 | It works with `remote_url`, `data_uri`, and `remove_attachment` plugins:
22 |
23 | ```rb
24 | # remote_url plugin
25 | attacher.form_assign({ "image_remote_url" => "https://example.com/..." })
26 | attacher.file #=> #
27 | ```
28 | ```rb
29 | # data_uri plugin
30 | attacher.form_assign({ "image_data_uri" => "data:image/jpeg;base64,..." })
31 | attacher.file #=> #
32 | ```
33 | ```rb
34 | # remove_attachment plugin
35 | attacher.form_assign({ "remove_image" => "1" })
36 | attacher.file #=> nil
37 | ```
38 |
39 | The return value is a hash with form params, with file param replaced with
40 | cached file data, which can later be assigned again to the record.
41 |
42 | ```rb
43 | attacher.form_assign({ "image" => file, "title" => "...", "description" => "..." })
44 | #=> { :image => '{"id":"...","storage":"...","metadata":"..."}', "title" => "...", "description" => "..." }
45 | ```
46 |
47 | You can also have attached file data returned as the `_data` attribute,
48 | suitable for persisting.
49 |
50 | ```rb
51 | attacher.form_assign({ "image" => image, ... }, result: :attributes)
52 | #=> { :image_data => '{"id":"...","storage":"...","metadata":"..."}', "title" => "...", "description" => "..." }
53 | ```
54 |
55 | [form_assign]: https://github.com/shrinerb/shrine/blob/master/lib/shrine/plugins/form_assign.rb
56 |
--------------------------------------------------------------------------------
/doc/plugins/included.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Included
3 | ---
4 |
5 | The [`included`][included] plugin allows you to hook up to the `.included` hook
6 | of the attachment module, and call additional methods on the model that
7 | includes it.
8 |
9 | ```rb
10 | class ImageUploader < Shrine
11 | plugin :included do |name|
12 | # called when attachment module is included into a model
13 |
14 | self #=> Photo (the model class)
15 | name #=> :image
16 | end
17 | end
18 | ```
19 | ```rb
20 | class Photo
21 | include ImageUploader::Attachment(:image) # triggers the included block
22 | end
23 | ```
24 |
25 | For example, you can use it to define additional methods on the model:
26 |
27 | ```rb
28 | class ImageUploader < Shrine
29 | plugin :included do |name|
30 | define_method(:"#{name}_width") { send(name)&.width }
31 | define_method(:"#{name}_height") { send(name)&.height }
32 | end
33 | end
34 | ```
35 | ```rb
36 | photo = Photo.new(image: file)
37 | photo.image_width #=> 1200
38 | photo.image_height #=> 800
39 | ```
40 |
41 | [included]: https://github.com/shrinerb/shrine/blob/master/lib/shrine/plugins/included.rb
42 |
--------------------------------------------------------------------------------
/doc/plugins/keep_files.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Keep Files
3 | ---
4 |
5 | The [`keep_files`][keep_files] plugin prevents the attached file (and any of
6 | its [derivatives]) from being deleted when the attachment would normally be
7 | destroyed, which happens when the attachment is removed/replaced, or when the
8 | record is deleted. This functionality is useful when implementing soft deletes,
9 | versioning, or in general any scenario where you need to keep history.
10 |
11 | ```rb
12 | plugin :keep_files
13 | ```
14 | ```rb
15 | photo.image #=> #
16 | photo.destroy
17 | photo.image.exists? #=> true
18 | ```
19 |
20 | [keep_files]: https://github.com/shrinerb/shrine/blob/master/lib/shrine/plugins/keep_files.rb
21 | [derivatives]: https://shrinerb.com/docs/plugins/derivatives
22 |
--------------------------------------------------------------------------------
/doc/plugins/metadata_attributes.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Metadata Attributes
3 | ---
4 |
5 | The [`metadata_attributes`][metadata_attributes] plugin allows you to write
6 | attachment metadata to additional record attributes. You can configure the
7 | plugin with a hash of mappings:
8 |
9 | ```rb
10 | plugin :metadata_attributes, :size => :size, :mime_type => :type
11 | # or
12 | plugin :metadata_attributes
13 | Attacher.metadata_attributes :size => :size, :mime_type => :type
14 | ```
15 |
16 | The above configuration will write `size` metadata field to `_size`
17 | record attribute, and `mime_type` metadata field to `_type` record
18 | attribute.
19 |
20 | ```rb
21 | user.avatar = image
22 | user.avatar.metadata["size"] #=> 95724
23 | user.avatar_size #=> 95724
24 | user.avatar.metadata["mime_type"] #=> "image/jpeg"
25 | user.avatar_type #=> "image/jpeg"
26 |
27 | user.avatar = nil
28 | user.avatar_size #=> nil
29 | user.avatar_type #=> nil
30 | ```
31 |
32 | ## Model and Entity
33 |
34 | With the [`model`][model] plugin, any method that internally calls
35 | `Attacher#write` will trigger metadata attributes writing (`Attacher#assign`,
36 | `Attacher#attach`, `Attacher#change`, `Attacher#set`).
37 |
38 | ```rb
39 | attacher.file.metadata["mime_type"] = "other/type"
40 | attacher.write
41 | attacher.record.avatar_type #=> "other/type"
42 | ```
43 |
44 | If you're using the [`entity`][entity] plugin, metadata attributes will be
45 | added to `Attacher#column_values`:
46 |
47 | ```rb
48 | attacher.assign(io)
49 | attacher.column_values #=>
50 | # {
51 | # :image_data => '{ ... }',
52 | # :image_size => 95724,
53 | # :image_type => "image/jpeg",
54 | # }
55 | ```
56 |
57 | Any metadata attributes that were declared but are missing on the record will
58 | be skipped.
59 |
60 | ## Full attribute name
61 |
62 | If you want to specify the full record attribute name, pass the record
63 | attribute name as a string instead of a symbol.
64 |
65 | ```rb
66 | Attacher.metadata_attributes :filename => "original_filename"
67 | ```
68 | ```rb
69 | photo.image = image
70 | photo.original_filename #=> "nature.jpg"
71 | ```
72 |
73 | [metadata_attributes]: https://github.com/shrinerb/shrine/blob/master/lib/shrine/plugins/metadata_attributes.rb
74 | [entity]: https://shrinerb.com/docs/plugins/entity
75 | [model]: https://shrinerb.com/docs/plugins/model
76 |
--------------------------------------------------------------------------------
/doc/plugins/module_include.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Module Include
3 | ---
4 |
5 | The [`module_include`][module_include] plugin allows you to extend Shrine's
6 | core classes for the given uploader with modules/methods.
7 |
8 | ```rb
9 | plugin :module_include
10 | ```
11 |
12 | To add a module to a core class, call the appropriate method:
13 |
14 | ```rb
15 | attachment_module CustomAttachmentMethods
16 | attacher_module CustomAttacherMethods
17 | file_module CustomFileMethods
18 | ```
19 |
20 | Alternatively you can pass in a block (which internally creates a module):
21 |
22 | ```rb
23 | attachment_module do
24 | def included(model)
25 | super
26 |
27 | name = attachment_name
28 |
29 | define_method :"#{name}_size" do |version|
30 | attachment = send(name)
31 | if attachment.is_a?(Hash)
32 | attachment[version].size
33 | elsif attachment
34 | attachment.size
35 | end
36 | end
37 | end
38 | end
39 | ```
40 |
41 | The above defines an additional `#_size` method on the attachment
42 | module, which is what is included in your model.
43 |
44 | [module_include]: https://github.com/shrinerb/shrine/blob/master/lib/shrine/plugins/module_include.rb
45 |
--------------------------------------------------------------------------------
/doc/plugins/multi_cache.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Multi Cache
3 | ---
4 |
5 | The [`multi_cache`][multi_cache] plugin allows an attacher to accept files from
6 | additional temporary storages.
7 |
8 | ```rb
9 | Shrine.storages = { cache: ..., cache_one: ..., cache_two: ..., store: ... }
10 |
11 | Shrine.plugin :multi_cache, additional_cache: [:cache_one, :cache_two]
12 | ```
13 | ```rb
14 | photo.image = { "id" => "...", "storage" => "cache", "metadata" => { ... } }
15 | photo.image.storage_key #=> :cache
16 | # or
17 | photo.image = { "id" => "...", "storage" => "cache_one", "metadata" => { ... } }
18 | photo.image.storage_key #=> :cache_one
19 | # or
20 | photo.image = { "id" => "...", "storage" => "cache_two", "metadata" => { ... } }
21 | photo.image.storage_key #=> :cache_two
22 | ```
23 |
24 | [multi_cache]: https://github.com/shrinerb/shrine/blob/master/lib/shrine/plugins/multi_cache.rb
25 |
--------------------------------------------------------------------------------
/doc/plugins/pretty_location.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Pretty Location
3 | ---
4 |
5 | The [`pretty_location`][pretty_location] plugin attempts to generate a nicer
6 | folder structure for uploaded files.
7 |
8 | ```rb
9 | plugin :pretty_location
10 | ```
11 |
12 | This plugin uses the context information from the Attacher to try to generate a
13 | nested folder structure which separates files for each record. The newly
14 | generated locations will typically look like this:
15 |
16 | ```rb
17 | "user/564/avatar/thumb-493g82jf23.jpg"
18 | # :model/:id/:attachment/:derivative-:uid.:extension
19 | ```
20 |
21 | By default if a record class is inside a namespace, only the "inner" class name
22 | is used in the location. If you want to include the namespace, you can pass in
23 | the `:namespace` option with the desired separator as the value:
24 |
25 | ```rb
26 | plugin :pretty_location, namespace: "_"
27 | # "blog_user/.../493g82jf23.jpg"
28 |
29 | plugin :pretty_location, namespace: "/"
30 | # "blog/user/.../493g82jf23.jpg"
31 | ```
32 |
33 | By default, if there is a record present, the record `id` will is used in the location.
34 | If you want to use a different identifier for the record, you can pass in
35 | the `:identifier` option with the desired method/attribute name as the value:
36 |
37 | ```rb
38 | plugin :pretty_location, identifier: "uuid"
39 | # "user/aa357797-5845-451b-8662-08eecdc9f762/profile_picture/493g82jf23.jpg"
40 |
41 | plugin :pretty_location, identifier: :email
42 | # "user/foo@bar.com/profile_picture/493g82jf23.jpg"
43 | ```
44 |
45 | By default, the class name will be only downcased. We can also have the class
46 | name underscored with the `:class_underscore` option:
47 |
48 | ```ruby
49 | plugin :pretty_location
50 | # "blogpost/aa357797-5845-451b-8662-08eecdc9f762/image/493g82jf23.jpg"
51 |
52 | plugin :pretty_location, class_underscore: :true
53 | # "blog_post/aa357797-5845-451b-8662-08eecdc9f762/image/493g82jf23.jpg"
54 | ```
55 |
56 | For a more custom identifier logic, you can overwrite the method
57 | `#generate_location` and call `#pretty_location` with the identifier you have
58 | calculated.
59 |
60 | ```rb
61 | def generate_location(io, record: nil, **context)
62 | identifier = record.email if record.is_a?(User)
63 | pretty_location(io, record: record, identifier: identifier, **context)
64 | end
65 | ```
66 |
67 | [pretty_location]: https://github.com/shrinerb/shrine/blob/master/lib/shrine/plugins/pretty_location.rb
68 |
--------------------------------------------------------------------------------
/doc/plugins/processing.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Processing
3 | ---
4 |
5 | Shrine uploaders can define the `#process` method, which will get called
6 | whenever a file is uploaded. It is given the original file, and is expected to
7 | return the processed files.
8 |
9 | ```rb
10 | def process(io, context)
11 | # you can process the original file `io` and return processed file(s)
12 | end
13 | ```
14 |
15 | However, when handling files as attachments, the same file is uploaded to
16 | temporary and permanent storage. Since we only want to apply the same
17 | processing once, we need to branch based on the context.
18 |
19 | ```rb
20 | def process(io, context)
21 | if context[:action] == :store # promote phase
22 | # ...
23 | end
24 | end
25 | ```
26 |
27 | The [`processing`][processing] plugin simplifies this by allowing us to
28 | declaratively define file processing for specified actions.
29 |
30 | ```rb
31 | plugin :processing
32 |
33 | process(:store) do |io, context|
34 | # ...
35 | end
36 | ```
37 |
38 | An example of resizing an image using the [image_processing] library:
39 |
40 | ```rb
41 | require "image_processing/mini_magick"
42 |
43 | process(:store) do |io, context|
44 | io.download do |original|
45 | ImageProcessing::MiniMagick
46 | .source(original)
47 | .resize_to_limit!(800, 800)
48 | end
49 | end
50 | ```
51 |
52 | The declarations are additive and inheritable, so for the same action you can
53 | declare multiple blocks, and they will be performed in the same order, with
54 | output from previous block being the input to next.
55 |
56 | ## Manually Run Processing
57 |
58 | You can manually trigger the defined processing via the uploader by calling
59 | `#upload` or `#process` and setting `:action` to the name of your processing
60 | block:
61 |
62 | ```rb
63 | uploader.upload(file, action: :store) # process and upload
64 | uploader.process(file, action: :store) # only process
65 | ```
66 |
67 | If you want the result of processing to be multiple files, use the `versions`
68 | plugin.
69 |
70 | [processing]: https://github.com/shrinerb/shrine/blob/master/lib/shrine/plugins/processing.rb
71 | [image_processing]: https://github.com/janko/image_processing
72 |
--------------------------------------------------------------------------------
/doc/plugins/rack_file.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Rack File
3 | ---
4 |
5 | The [`rack_file`][rack_file] plugin enables uploaders to accept Rack uploaded
6 | file hashes for uploading.
7 |
8 | ```rb
9 | plugin :rack_file
10 | ```
11 |
12 | ## Usage
13 |
14 | When a file is uploaded to your Rack application using the
15 | `multipart/form-data` parameter encoding, Rack converts the uploaded file to a
16 | hash.
17 |
18 | ```rb
19 | file_hash #=>
20 | # {
21 | # :name => "file",
22 | # :filename => "cats.png",
23 | # :type => "image/png",
24 | # :tempfile => #,
25 | # :head => "Content-Disposition: form-data; ...",
26 | # }
27 | ```
28 |
29 | Since Shrine only accepts IO objects, you would normally need to fetch the
30 | `:tempfile` object and pass it directly. This plugin enables the attacher to
31 | accept the Rack uploaded file hash directly, which is convenient when doing
32 | mass attribute assignment.
33 |
34 | ```rb
35 | user.avatar = file_hash
36 | # or
37 | attacher.assign(file_hash)
38 | ```
39 |
40 | ## API
41 |
42 | Internally the Rack uploaded file hash will be converted into an IO object
43 | using `Shrine.rack_file`, which you can also use directly:
44 |
45 | ```rb
46 | # or YourUploader.rack_file(file_hash)
47 | io = Shrine.rack_file(file_hash)
48 | io.original_filename #=> "cats.png"
49 | io.content_type #=> "image/png"
50 | io.size #=> 58342
51 | ```
52 |
53 | Note that this plugin is not needed in Rails applications, as Rails already
54 | wraps the Rack uploaded file hash into an `ActionDispatch::Http::UploadedFile`
55 | object.
56 |
57 | [rack_file]: https://github.com/shrinerb/shrine/blob/master/lib/shrine/plugins/rack_file.rb
58 |
--------------------------------------------------------------------------------
/doc/plugins/recache.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Re-cache
3 | ---
4 |
5 | The [`recache`][recache] plugin allows you to process your attachment after
6 | validations succeed, but before the attachment is promoted. This is useful for
7 | example when you want to generate some versions upfront (so the user
8 | immediately sees them) and other versions you want to generate in the promotion
9 | phase in a background job.
10 |
11 | ```rb
12 | plugin :recache
13 | plugin :processing
14 |
15 | process(:recache) do |io, context|
16 | # perform cheap processing
17 | end
18 |
19 | process(:store) do |io, context|
20 | # perform more expensive processing
21 | end
22 | ```
23 |
24 | Recaching will be automatically triggered in a "before save" callback, but if
25 | you're using the attacher directly, you can call it manually:
26 |
27 | ```rb
28 | attacher.recache if attacher.changed?
29 | ```
30 |
31 | [recache]: https://github.com/shrinerb/shrine/blob/master/lib/shrine/plugins/recache.rb
32 |
--------------------------------------------------------------------------------
/doc/plugins/refresh_metadata.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Refresh Metadata
3 | ---
4 |
5 | The [`refresh_metadata`][refresh_metadata] plugin allows you to re-extract
6 | metadata from an uploaded file.
7 |
8 | ```rb
9 | plugin :refresh_metadata
10 | ```
11 |
12 | It provides `#refresh_metadata!` method, which triggers metadata extraction
13 | (calls `Shrine#extract_metadata`) with the uploaded file opened for reading,
14 | and updates the existing metadata hash with the results. This can be done
15 | on the `Shrine::Attacher` or the `Shrine::UploadedFile` level.
16 |
17 | ## Attacher
18 |
19 | Calling `#refresh_metadata!` on a `Shrine::Attacher` object will re-extract
20 | metadata of the attached file, and when used with a [model], it will write new
21 | file data back into the attachment attribute.
22 |
23 | ```rb
24 | attacher.refresh_metadata!
25 | attacher.file.metadata # re-extracted metadata
26 | attacher.record.file_data #=> '{ ... data with updated metadata ... }'
27 | ```
28 |
29 | The `Attacher#context` hash will be forwarded to metadata extraction, as well
30 | as any options that you pass in.
31 |
32 | ```rb
33 | # via context
34 | attacher.context[:foo] = "bar"
35 | attacher.refresh_metadata! # passes `{ foo: "bar" }` options to metadata extraction
36 |
37 | # via arguments
38 | attacher.refresh_metadata!(foo: "bar") # passes `{ foo: "bar" }` options to metadata extraction
39 | ```
40 |
41 | ## Uploaded File
42 |
43 | The `#refresh_metadata!` method can be called on a `Shrine::UploadedFile` object
44 | as well.
45 |
46 | ```rb
47 | uploaded_file.refresh_metadata!
48 | uploaded_file.metadata # re-extracted metadata
49 | ```
50 |
51 | If the uploaded file is not open, it is opened before and closed after metadata
52 | extraction. For remote storage services this will make an HTTP request.
53 | However, only the portion of the file needed for extracting metadata will be
54 | downloaded.
55 |
56 | If the uploaded file is already open, it is passed to metadata extraction as
57 | is.
58 |
59 | ```rb
60 | uploaded_file.open do
61 | uploaded_file.refresh_metadata! # uses the already opened file
62 | # ...
63 | end
64 | ```
65 |
66 | Any options passed in will be forwarded to metadata extraction:
67 |
68 | ```rb
69 | uploaded_file.refresh_metadata!(foo: "bar") # passes `{ foo: "bar" }` options to metadata extraction
70 | ```
71 |
72 | [refresh_metadata]: https://github.com/shrinerb/shrine/blob/master/lib/shrine/plugins/refresh_metadata.rb
73 | [model]: https://shrinerb.com/docs/plugins/model
74 |
--------------------------------------------------------------------------------
/doc/plugins/remove_attachment.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Remove Attachment
3 | ---
4 |
5 | The [`remove_attachment`][remove_attachment] plugin allows you to delete
6 | attachments through checkboxes on the web form.
7 |
8 | ```rb
9 | plugin :remove_attachment
10 | ```
11 |
12 | The plugin adds the `#remove_` accessor to your model, which removes the
13 | attached file if it receives a truthy value:
14 |
15 | ```rb
16 | photo.image #=> #
17 | photo.remove_image = 'true'
18 | photo.image #=> nil
19 | ```
20 |
21 | This allows you to add a checkbox form field for removing attachments:
22 |
23 | ```rb
24 | form_for photo do |f|
25 | # ...
26 | f.check_box :remove_image
27 | end
28 | ```
29 |
30 | If you're using the `Shrine::Attacher` directly, you can use the
31 | `Attacher#remove` accessor:
32 |
33 | ```rb
34 | attacher.file #=> #
35 | attacher.remove = '1'
36 | attacher.file #=> nil
37 | ```
38 |
39 | [remove_attachment]: https://github.com/shrinerb/shrine/blob/master/lib/shrine/plugins/remove_attachment.rb
40 |
--------------------------------------------------------------------------------
/doc/plugins/remove_invalid.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Remove Invalid
3 | ---
4 |
5 | The [`remove_invalid`][remove_invalid] plugin automatically deletes and
6 | deassigns a new assigned file if it was invalid. If there was a previous file
7 | attached, it will be assigned back.
8 |
9 | ```rb
10 | plugin :remove_invalid
11 | ```
12 |
13 | ```rb
14 | # without previous file
15 | photo.image #=> nil
16 | photo.image = file # validation fails, assignment is reverted
17 | photo.valid? #=> false
18 | photo.image #=> nil
19 |
20 | # with previous file
21 | photo.image #=> #
22 | photo.image = file # validation fails, assignment is reverted
23 | photo.valid? #=> false
24 | photo.image #=> #
25 | ```
26 |
27 | [remove_invalid]: https://github.com/shrinerb/shrine/blob/master/lib/shrine/plugins/remove_invalid.rb
28 |
--------------------------------------------------------------------------------
/doc/plugins/restore_cached_data.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Restore Cached Data
3 | ---
4 |
5 | The [`restore_cached_data`][restore_cached_data] plugin re-extracts metadata
6 | when assigning already cached files, i.e. when the attachment has been retained
7 | on validation errors or assigned from a direct upload. In both cases you may
8 | want to re-extract metadata on the server side, mainly to prevent tempering,
9 | but also in case of direct uploads to obtain metadata that couldn't be
10 | extracted on the client side.
11 |
12 | ```rb
13 | plugin :restore_cached_data
14 | ```
15 | ```rb
16 | photo.image = { "id" => "path/to/image.jpg", "storage" => "cache", "metadata" => {} }
17 | photo.image.metadata #=> { "size" => 4823763, "mime_type" => "image/jpeg", ... }
18 | ```
19 |
20 | It uses the [`refresh_metadata`][refresh_metadata] plugin to re-extract
21 | metadata.
22 |
23 | [restore_cached_data]: https://github.com/shrinerb/shrine/blob/master/lib/shrine/plugins/restore_cached_data.rb
24 | [refresh_metadata]: https://shrinerb.com/docs/plugins/refresh_metadata
25 |
--------------------------------------------------------------------------------
/doc/plugins/signature.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Signature
3 | ---
4 |
5 | The [`signature`][signature] plugin provides the ability to calculate a hash
6 | from file content. This hash can be used as a checksum or just as a unique
7 | signature for the uploaded file.
8 |
9 | ```rb
10 | Shrine.plugin :signature
11 | ```
12 |
13 | ## API
14 |
15 | The plugin adds a `#calculate_signature` instance and class method to the
16 | uploader. The method accepts an IO object and a hashing algorithm, and returns
17 | the calculated hash.
18 |
19 | ```rb
20 | Shrine.calculate_signature(io, :md5) #=> "9a0364b9e99bb480dd25e1f0284c8555"
21 | # or just
22 | Shrine.signature(io, :md5) #=> "9a0364b9e99bb480dd25e1f0284c8555"
23 | ```
24 |
25 | The following hashing algorithms are supported: SHA1, SHA256, SHA384, SHA512,
26 | MD5, and CRC32.
27 |
28 | You can also choose which format will the calculated hash be encoded in:
29 |
30 | ```rb
31 | Shrine.calculate_signature(io, :sha256, format: :base64)
32 | ```
33 |
34 | The supported encoding formats are `hex` (default), `base64`, and `none`.
35 |
36 | ## Adding metadata
37 |
38 | You can then use the `add_metadata` plugin to add a new metadata field with the
39 | calculated hash.
40 |
41 | ```rb
42 | plugin :add_metadata
43 |
44 | add_metadata :md5 do |io|
45 | calculate_signature(io, :md5)
46 | end
47 | ```
48 |
49 | This will generate a hash for each uploaded file, but if you want to generate
50 | one only for the original file, you can add a conditional:
51 |
52 | ```rb
53 | add_metadata :md5 do |io, action: nil, **|
54 | calculate_signature(io, :md5) if action == :cache
55 | end
56 | ```
57 |
58 | ## Rewinding
59 |
60 | If you want to calculate signature from a non-rewindable IO object, you can
61 | tell Shrine to skip rewinding:
62 |
63 | ```rb
64 | Shrine.calculate_signature(io, :md5, rewind: false)
65 | ```
66 |
67 | ## Instrumentation
68 |
69 | If the `instrumentation` plugin has been loaded, the `signature` plugin adds
70 | instrumentation around signature calculation.
71 |
72 | ```rb
73 | # instrumentation plugin needs to be loaded *before* signature
74 | plugin :instrumentation
75 | plugin :signature
76 | ```
77 |
78 | Calculating signature will trigger a `signature.shrine` event with the
79 | following payload:
80 |
81 | | Key | Description |
82 | | :-- | :---- |
83 | | `:io` | The IO object |
84 | | `:uploader` | The uploader class that sent the event |
85 |
86 | A default log subscriber is added as well which logs these events:
87 |
88 | ```
89 | MIME Type (33ms) – {:io=>StringIO, :uploader=>Shrine}
90 | ```
91 |
92 | You can also use your own log subscriber:
93 |
94 | ```rb
95 | plugin :signature, log_subscriber: -> (event) {
96 | Shrine.logger.info JSON.generate(name: event.name, duration: event.duration, **event.payload)
97 | }
98 | ```
99 | ```
100 | {"name":"signature","duration":24,"io":"#","uploader":"Shrine"}
101 | ```
102 |
103 | Or disable logging altogether:
104 |
105 | ```rb
106 | plugin :signature, log_subscriber: nil
107 | ```
108 |
109 | [signature]: https://github.com/shrinerb/shrine/blob/master/lib/shrine/plugins/signature.rb
110 |
--------------------------------------------------------------------------------
/doc/plugins/tempfile.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Tempfile
3 | ---
4 |
5 | The [`tempfile`][tempfile] plugin makes it easier to reuse a single copy of an
6 | uploaded file on disk.
7 |
8 | ```rb
9 | Shrine.plugin :tempfile
10 | ```
11 |
12 | The plugin provides the `UploadedFile#tempfile` method, which when called on an
13 | open uploaded file will return a copy of its content on disk. The first time
14 | the method is called the file content will cached into a temporary file and
15 | returned. On any subsequent method calls the cached temporary file will be
16 | returned directly. The temporary file is deleted when the uploaded file is
17 | closed.
18 |
19 | ```rb
20 | uploaded_file.open do
21 | # ...
22 | uploaded_file.tempfile #=> # (file is cached)
23 | # ...
24 | uploaded_file.tempfile #=> # (cache is returned)
25 | # ...
26 | end # tempfile is deleted
27 |
28 | # OR
29 |
30 | uploaded_file.open
31 | # ...
32 | uploaded_file.tempfile #=> # (file is cached)
33 | # ...
34 | uploaded_file.tempfile #=> # (cache is returned)
35 | # ...
36 | uploaded_file.close # tempfile is deleted
37 | ```
38 |
39 | This plugin also modifies `Shrine.with_file` to call `UploadedFile#tempfile`
40 | when the given IO object is an open `UploadedFile`. Since `Shrine.with_file` is
41 | typically called on the `Shrine` class directly, it's recommended to load this
42 | plugin globally.
43 |
44 | [tempfile]: https://github.com/shrinerb/shrine/blob/master/lib/shrine/plugins/tempfile.rb
45 |
--------------------------------------------------------------------------------
/doc/plugins/upload_options.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Upload Options
3 | ---
4 |
5 | The [`upload_options`][upload_options] plugin allows you to automatically pass
6 | additional upload options to storage on every upload:
7 |
8 | ```rb
9 | plugin :upload_options, cache: { acl: "private" }
10 | ```
11 |
12 | Keys are names of the registered storages, and values are either hashes or
13 | blocks.
14 |
15 | ```rb
16 | plugin :upload_options, store: -> (io, options) do
17 | if options[:derivative]
18 | { acl: "public-read" }
19 | else
20 | { acl: "private" }
21 | end
22 | end
23 | ```
24 |
25 | If you're uploading the file directly, you can also pass `:upload_options` to
26 | the uploader.
27 |
28 | ```rb
29 | uploader.upload(file, upload_options: { acl: "public-read" })
30 | ```
31 |
32 | [upload_options]: https://github.com/shrinerb/shrine/blob/master/lib/shrine/plugins/upload_options.rb
33 |
--------------------------------------------------------------------------------
/doc/plugins/url_options.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: URL Options
3 | ---
4 |
5 | The [`url_options`][url_options] plugin allows you to specify
6 | URL options that will be applied by default for uploaded files of specified
7 | storages. `url_options` are parameters specific to the storage service.
8 |
9 | ```rb
10 | plugin :url_options, store: { expires_in: 24*60*60 } # `expires_in` is a URL option for AWS S3
11 | ```
12 |
13 | You can also generate the default URL options dynamically by using a block,
14 | which will receive the UploadedFile object along with any options that were
15 | passed to `UploadedFile#url`.
16 |
17 | ```rb
18 | plugin :url_options, store: -> (file, options) do
19 | { response_content_disposition: ContentDisposition.attachment(file.original_filename) }
20 | end
21 | ```
22 |
23 | In both cases the default options are merged with options passed to
24 | `UploadedFile#url`, and the latter will always have precedence over default
25 | options.
26 |
27 | [url_options]: https://github.com/shrinerb/shrine/blob/master/lib/shrine/plugins/url_options.rb
28 |
--------------------------------------------------------------------------------
/doc/plugins/validation.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Validation
3 | ---
4 |
5 | The [`validation`][validation] plugin provides a framework for validating
6 | attached files. For some useful validators, see the
7 | [`validation_helpers`][validation_helpers] plugin.
8 |
9 | ```rb
10 | plugin :validation
11 | ```
12 |
13 | ## Usage
14 |
15 | The `Attacher.validate` method is used to register a validation block, which
16 | is called on attachment:
17 |
18 | ```rb
19 | class VideoUploader < Shrine
20 | Attacher.validate do
21 | if file.duration > 5*60*60
22 | errors << "duration must not be longer than 5 hours"
23 | end
24 | end
25 | end
26 | ```
27 | ```rb
28 | attacher.assign(file)
29 | attacher.errors #=> ["duration must not be longer than 5 hours"]
30 | ```
31 |
32 | The validation block is executed in context of a `Shrine::Attacher` instance:
33 |
34 | ```rb
35 | class VideoUploader < Shrine
36 | Attacher.validate do
37 | self #=> #
38 |
39 | file #=> #
40 | record #=> #
41 | name #=> :video
42 | context #=> { ... }
43 | end
44 | end
45 | ```
46 |
47 | ## Inheritance
48 |
49 | If you're subclassing an uploader that has validations defined, you can call
50 | those validations via `super()`:
51 |
52 | ```rb
53 | class ApplicationUploader < Shrine
54 | Attacher.validate { validate_max_size 5.megabytes }
55 | end
56 | ```
57 | ```rb
58 | class ImageUploader < ApplicationUploader
59 | Attacher.validate do
60 | super() # empty parentheses are required
61 | validate_mime_type %w[image/jpeg image/png image/webp]
62 | end
63 | end
64 | ```
65 |
66 | ## Validation options
67 |
68 | You can pass options to the validator via the `:validate` option:
69 |
70 | ```rb
71 | attacher.assign(file, validate: { foo: "bar" })
72 | ```
73 | ```rb
74 | class MyUploader < Shrine
75 | Attacher.validate do |**options|
76 | options #=> { foo: "bar" }
77 | end
78 | end
79 | ```
80 |
81 | You can also skip validation by passing `validate: false`:
82 |
83 | ```rb
84 | attacher.assign(file, validate: false) # skips validation
85 | ```
86 |
87 | ## Manual validation
88 |
89 | You can also run validation manually via `Attacher#validate`:
90 |
91 | ```rb
92 | attacher.set(uploaded_file) # doesn't trigger validation
93 | attacher.validate # runs validation
94 | ```
95 |
96 | [validation]: https://github.com/shrinerb/shrine/blob/master/lib/shrine/plugins/validation.rb
97 | [validation_helpers]: https://shrinerb.com/docs/plugins/validation_helpers
98 |
--------------------------------------------------------------------------------
/doc/release_notes/1.2.0.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Shrine 1.2.0
3 | ---
4 |
5 | New features
6 | ============
7 |
8 | * `Shrine::Attacher.promote` and `Shrine::Attacher.delete` from the
9 | backgrounding plugin now return the record (unless the record wasn't found or
10 | the action aborted).
11 |
12 | ```rb
13 | class PromoteJob
14 | include Sidekiq::Worker
15 |
16 | def perform(data)
17 | record = Shrine::Attacher.promote(data)
18 | record.update(published: true) if record.is_a?(Post)
19 | end
20 | end
21 | ```
22 |
23 | Other improvements
24 | ==================
25 |
26 | * Custom metadata (which can be set by overriding `Shrine#extract_metadata`) is
27 | now properly inherited from cached files, and `Shrine#extact_metadata` is now
28 | only called once.
29 |
30 | * If the record was deleted before the background job for promoting started,
31 | instead of erroring, the job now ignores that error and immediately finishes.
32 |
33 | * Fixed a very unlikely scenario where a background job could replaced an
34 | updated attachment on subsequent attachment updates, by wrapping the check if
35 | the attachment changed and updating the attachment in a database transaction.
36 |
37 | * Prevented starting the background job if the attachment has already changed,
38 | which fixes a relatively harmless attack where the end user could create a
39 | lot of retrying jobs by updating the attachment with a corrupted file.
40 |
41 | * All IOs are now closed even if their upload errors.
42 |
--------------------------------------------------------------------------------
/doc/release_notes/1.3.0.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Shrine 1.3.0
3 | ---
4 |
5 | New features
6 | ============
7 |
8 | * Added `:namespace` option to pretty_location plugin for including the model
9 | namespace into the location.
10 |
11 | * Addded `:include_error` option to remote_url plugin which gives you access to
12 | the download error instance when generating the error message.
13 |
14 | * Added `_cached?` and `_stored?` to migration_helpers
15 | for easier handling of uploading logic in ORM hooks:
16 |
17 | ```rb
18 | class Photo < Sequel::Model
19 | def before_save
20 | if column_changed?(:image) && image_stored? # promote
21 | # ...
22 | end
23 | super
24 | end
25 | end
26 | ```
27 |
28 | Other improvements
29 | ==================
30 |
31 | * Fixed uploading an S3 file making an additional unneeded GET request in
32 | addition to the S3 COPY request.
33 |
34 | * When the model with an attachment is namespaced, the namespace isn't included
35 | in the location anymore when using the pretty_location plugin.
36 |
37 | * Dumped Down version to 2.0.0 to fix downloading files from URLs which have
38 | "[]" characters in them.
39 |
40 | * The IO is now rewinded even if FastImage fails to extract dimensions from the
41 | IO
42 |
43 | * The IO is now rewinded when using MimeMagic to extract the MIME type.
44 |
45 | * When uploading a `Shrine::UploadedFile`, the default location generator will
46 | first look for the extension in `#id` before looking at `#original_filename`.
47 |
48 | * The remote_url plugin now returns separate error messsages when file wasn't
49 | found and when the file was too large.
50 |
51 | * Fixed promoting raising an error if the record got deleted before
52 | promoting finished
53 |
54 | * `UploadedFile#original_filename`, `#size` and `#content_type` now return `nil`
55 | when corresponding metadata keys are missing instead of raising a `KeyError`.
56 |
57 | * Fixed errors being able to accumulate on the record object when errors occur
58 | in data_uri and remote_url plugins.
59 |
60 | * The "metadata" key isn't required anymore when instantiating a
61 | `Shrine::UploadedFile`.
62 |
63 | * The top-level key-values aren't cached anymore when instantiating a
64 | `Shrine::UploadedFile`, which means that `#id`, `#storage_key` and
65 | `#metadata` will now pick up any mutations on the data hash.
66 |
67 | * Removed a deprecation warning in the S3 storage which occurs when AWS
68 | credentials are inferred implicitly.
69 |
70 | * The extracted metadata is now accessible inside `Shrine#generate_location`
71 | via the `:metadata` key in the `context` hash.
72 |
73 | * `Shrine#upload` doesn't mutate the given context hash anymore.
74 |
75 | * `Shrine::Attacher#backup_file` from the backup plugin doesn't modify the
76 | input `UploadedFile` anymore.
77 |
78 | Deprecations
79 | ============
80 |
81 | * The keep_location plugin is deprecated, as the same behaviour can easily be
82 | achieved by overriding `Shrine#generate_location`:
83 |
84 | ```rb
85 | class ImageUploader < Shrine
86 | def generate_location(io, context)
87 | if io.is_a?(UploadedFile) && context[:phase] == :store
88 | io.id
89 | else
90 | super
91 | end
92 | end
93 | end
94 | ```
95 |
--------------------------------------------------------------------------------
/doc/release_notes/1.4.1.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Shrine 1.4.1
3 | ---
4 |
5 | Regressions
6 | ===========
7 |
8 | * Shrine 1.4.0 introduced a regression where it didn't trigger callbacks for
9 | promoting files anymore. This was done in order to eliminate a tiny chance
10 | for a race condition when same attachment is updated subsequently with
11 | backgrounding. We bring back triggering callbacks on promote. For Sequel we
12 | eliminate the chance of a race condition, while ActiveRecord users can now
13 | add optimistic locking to prevent this.
14 |
--------------------------------------------------------------------------------
/doc/release_notes/1.4.2.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Shrine 1.4.2
3 | ---
4 |
5 | Regressions
6 | ===========
7 |
8 | * Version 1.4.1 introduced handling optimistic locking in ActiveRecord plugin,
9 | however the implementation wasn't stable and wouldn't work correctly in
10 | some cases, so we revert the behaviour as it was in version 1.4.0. Developers
11 | can still handle optimistic locking themselves inside background jobs.
12 |
13 | * Version 1.4.1 introduced an instance filter of cached attachment in Sequel
14 | plugin, which makes the same record instance unupdateable after promoting,
15 | even when not using backgrounding, so we revert the behaviour as it was in
16 | version 1.4.0. This is only a problem if you're doing something with the
17 | record instance after updating attachment.
18 |
19 | Improvements
20 | ============
21 |
22 | * The Sequel plugin now doesn't rescue every `Sequel::Error` on promoting, it
23 | just rescues "Record not found" ones (in Sequel 4.28+ this is a
24 | `Sequel::NoExistingObject`, but prior to that was a generic `Sequel::Error`).
25 |
--------------------------------------------------------------------------------
/doc/release_notes/2.0.1.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Shrine 2.0.1
3 | ---
4 |
5 | Regressions
6 | ===========
7 |
8 | * Fixed versions plugin removing what was set by default_url plugin, if
9 | default_url plugin was loaded before versions
10 |
11 | ```rb
12 | class MyUploader < Shrine
13 | plugin(:default_url) { |context| }
14 | plugin(:versions) # doesn't remove default_url anymore
15 | end
16 | ```
17 |
--------------------------------------------------------------------------------
/doc/release_notes/2.1.0.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Shrine 2.1.0
3 | ---
4 |
5 | Improvements
6 | ============
7 |
8 | * The versions plugin doesn't require the `:names` option anymore.
9 |
10 | * The restore_cached_data plugin will now download only a small portion of the
11 | file needed for extracting metadata (after which it will terminate the
12 | connection). Previously the whole file was always downloaded. To get this
13 | you need to update your storage gem.
14 |
15 | * When enforcing that the input is an IO-like object Shrine doesn't check
16 | method arity anymore, only if the object responds to that method. This means
17 | that Shrine now properly works with objects like `Rack::Test::UploadedFile`,
18 | which use `#method_missing` to delegate to the underlying object.
19 |
20 | * Fixed a load order bug with parallelize and logging plugins, where loading
21 | them in this order would make the thread pool join *outside* of the logging
22 | block, making the logs show an instantaneous duration instead of the actual
23 | duration.
24 |
25 | * The `file` command from the determine_mime_type plugin is now executed in the
26 | same way for files as for other IO objects.
27 |
28 | * Increase the amount of bytes read from the IO in determine_mime_type when
29 | using `:file` or `:filemagic` analyzers, which might make the recognize some
30 | MIME types that they haven't before.
31 |
32 | Backwards compatibility
33 | =======================
34 |
35 | * Generating versions in `:cache` phase is now deprecated, for better security.
36 |
37 | * The `#cached__data=` method that comes from the
38 | cached_attachment_data plugin is now deprecated. The reason for this is that
39 | the following "recommended" usage actually produces bugs when image is
40 | invalid:
41 |
42 | ```rb
43 | form_for @photo do |f|
44 | f.hidden_field :cached_image_data
45 | f.file_filed :image
46 | end
47 | ```
48 |
49 | The recommended and correct way now to build forms is by keeping the
50 | `#=` setter:
51 |
52 | ```rb
53 | form_for @photo do |f|
54 | f.hidden_field :image, value: @photo.cached_image_data, id: nil
55 | f.file_filed :image
56 | end
57 | ```
58 |
59 | * The restore_cached_data now sends the `Shrine::UploadedFile` to
60 | `#extract_metadata`, instead of the result of `Storage#open`.
61 |
62 | * Storages which use `#stream` should switch to `Down.open` for remote files, or
63 | the generic `Down::ChunkedIO` in other cases.
64 |
--------------------------------------------------------------------------------
/doc/release_notes/2.1.1.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Shrine 2.1.1
3 | ---
4 |
5 | Regressions
6 | ===========
7 |
8 | * `S3#open` has switched from `Down.download` to `Down.open` in the previous
9 | Shrine version, however the Down version 2.3.3 doesn't require `net/http`,
10 | which means that `S3#open` would raise an "undefined constant Net" error
11 | in case `net/http` wasn't required anywhere else. This is fixed by bumping
12 | the required Down version to 2.3.4, which requires net/http.
13 |
--------------------------------------------------------------------------------
/doc/release_notes/2.10.0.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Shrine 2.10.0
3 | ---
4 |
5 | ## New features
6 |
7 | * The `:mini_magick` analyzer has been added to the `store_dimensions` plugin,
8 | which uses the [MiniMagick] gem to extract image dimensions.
9 |
10 | * The `:ruby_vips` analyzer has been added to the `store_dimensions` plugin,
11 | which uses the [ruby-vips] gem to extract image dimensions.
12 |
13 | * The `:fastimage` analyzer has been added to the `determine_mime_type` plugin,
14 | which uses the [FastImage] gem to determine the MIME type of the file.
15 |
16 | * `Shrine::UploadedFile#download` now accepts a block for downloading an
17 | uploaded file temporarily. This is useful when wanting to validate whether an
18 | uploaded image is valid or corrupted, or when generating thumbnails.
19 |
20 | ```rb
21 | uploaded_file.download do |tempfile|
22 | # ...
23 | end # tempfile is deleted
24 | ```
25 |
26 | ## Other improvements
27 |
28 | * It's not required that IO objects respond to `#size` anymore. This is
29 | useful when uploading streams of data where the size is not known.
30 |
31 | * The S3 storage now supports IO objects with unknown size. Under the hood it
32 | will use multipart upload.
33 |
34 | * The logger is now properly inherited and shared between `Shrine` subclasses.
35 |
36 | * The attachment URL generated by `download_endpoint` now stays the same
37 | regardless of the order of elements in the metadata hash. Previously it could
38 | change after the uploaded file data is loaded from the database, because the
39 | order of the metadata hash elements would change, which is not desirable for
40 | caching.
41 |
42 | ## Backwards compatibility
43 |
44 | * The `:rack_mime` extension inferrer has been removed from the
45 | `infer_extension` plugin, due to not having acceptable behaviour. The new
46 | default extension inferrer is `:mime_types`.
47 |
48 | * The `:heroku` formatter in the `logging` plugin has been soft-renamed to
49 | `:logfmt`. The `:heroku` alias will stop being supported in Shrine 3.
50 |
51 | * The `Shrine::IO_METHODS` constant has been depreacted, and will become
52 | private in Shrine 3.
53 |
54 | [MiniMagick]: https://github.com/minimagick/minimagick
55 | [ruby-vips]: https://github.com/libvips/ruby-vips
56 | [FastImage]: https://github.com/sdsykes/fastimage
57 |
--------------------------------------------------------------------------------
/doc/release_notes/2.10.1.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Shrine 2.10.1
3 | ---
4 |
5 | ## Regressions
6 |
7 | * Determining MIME type from file extension (using `:mime_types` or
8 | `:mini_mime` analyzers) when file is empty now works again. This was the
9 | behaviour prior to version 2.7.0, but 2.7.0 introduced a regression where
10 | `nil` was returned for empty files, even if they had an extension.
11 |
--------------------------------------------------------------------------------
/doc/release_notes/2.11.0.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Shrine 2.11.0
3 | ---
4 |
5 | ## New features
6 |
7 | * `Shrine::UploadedFile#stream` has been added for streaming the uploaded
8 | content to a writable destination.
9 |
10 | ```rb
11 | uploaded_file.stream(StringIO.new)
12 | # or
13 | uploaded_file.stream("/path/to/destination")
14 | ```
15 |
16 | * `Shrine.with_file` has been added for temporarily converting an IO-like
17 | object into a file. This is useful when wanting to extract metadata using an
18 | analyzer which requires the source file to be on disk.
19 |
20 | ```rb
21 | add_metadata do |io, context|
22 | movie = Shrine.with_file(io) { |file| FFMPEG::Movie.new(file.path) }
23 |
24 | { "duration" => movie.duration,
25 | "bitrate" => movie.bitrate,
26 | "resolution" => movie.resolution,
27 | "frame_rate" => movie.frame_rate }
28 | end
29 | ```
30 |
31 | * The `upload_endpoint` plugin now accepts the `Content-MD5` request header,
32 | in which case it will verify the provided checksum.
33 |
34 | * `Shrine::Storage::S3#presign` now accepts `method: :put` for changing from a
35 | POST to a PUT presigned URL. PUT presigned URLs are generally preferred as
36 | they support more parameters, such as `:content_md5` for specifying the
37 | checksum.
38 |
39 | ## Other improvements
40 |
41 | * `Shrine::UploadedFile#download` will now reuse an already opened file, and
42 | in this case will simply rewind it after it's finished.
43 |
44 | * The `:mini_magick` and `:ruby_vips` dimensions analyzers now silently fail
45 | on processing errors, to allow validations to be reached when invalid file
46 | is attached.
47 |
48 | * The `#presign` storage method can now return a Hash. This means it's not
49 | required for result to be wrapped in a `Struct` or `OpenStruct` anymore.
50 |
51 | * The `Shrine::Storage::S3#presign` now also returns a `:method` value
52 | indicating the HTTP verb that needs to be used for the direct upload.
53 |
54 | * The bucket name is not removed from S3 URL path anymore when both `:host`
55 | and `:force_path_style` are set in `Shrine::Storage::S3#url`.
56 |
57 | ## Regressions
58 |
59 | * The MIME type is now correctly determined on empty files for `:mime_types`
60 | and `:mini_mime` analyzers. This regression was introduced in Shrine 2.7.0.
61 |
62 | ## Backwards compatibility
63 |
64 | * The `direct_upload` plugin has been deprecated in favour of `upload_endpoint`
65 | and `presign_endpoint` plugins. The `direct_upload` plugin will be removed in
66 | Shrine 3.
67 |
68 | * `Storage#presign` returning an object that doesn't respond to `#to_h` is now
69 | deprecated, the support for these objects will be removed in Shrine 3.
70 |
71 | * `Shrine::Storage::S3#presign` now returns a `Struct` instead of an
72 | `Aws::S3::PresignedPost` object. Any applications relying on any methods
73 | other than `#url` and `#fields` will have to update their code.
74 |
--------------------------------------------------------------------------------
/doc/release_notes/2.12.0.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Shrine 2.12.0
3 | ---
4 |
5 | ## New features
6 |
7 | * The `Shrine::Attacher#assign_remote_url` method has been added, which acts the
8 | same as `#remote_url=` but allows you to also dynamically provide options to
9 | the underlying downloader (`Down.download` by default).
10 |
11 | ```rb
12 | attacher.assign_remote_url("https://example.com/image.jpg", downloader: { 'Cookie' => 'abc123' })
13 | ```
14 |
15 | * The `Shrine::UploadedFile#download_url` method has been added to the
16 | `download_endpoint` plugin for explicitly generating the file URL through
17 | the download endpoint.
18 |
19 | * The `:redirect` option has been added to `download_endpoint` plugin, which
20 | makes download URLs redirect directly to the uploaded file on its storage.
21 | This allows you to avoding streaming the file through your app, and thus not
22 | having it impact your request throughput.
23 |
24 | * The `Shrine::Plugins::ValidationHelpers::PRETTY_FILESIZE` callbable has been
25 | added, which takes the filesize in bytes and returns the representation in
26 | correct unit. This can be used when providing custom validation error messages.
27 |
28 | ```rb
29 | pretty_filesize = Shrine::Plugins::ValidationHelpers::PRETTY_FILESIZE
30 | pretty_filesize.call(1*1024) #=> "1.0 KB"
31 | pretty_filesize.call(1.5*1024*1024) #=> "1.5 MB"
32 | pretty_filesize.call(2*1024*1024*1024) #=> "2.0 GB"
33 | ```
34 |
35 | ## Other improvements
36 |
37 | * The `Shrine::Storage::FileSystem#open` now forwards any additional options to
38 | `File.open`.
39 |
40 | * The `Shrine::Storage::S3#open` method now accepts the `:rewindable` option to
41 | disable caching read content to disk.
42 |
43 | * In validation error messages in filesize validators in the
44 | `validation_helpers` plugin show specified limits in the appropriate unit,
45 | instead of always using megabytes.
46 |
47 | * `Shrine::UploadedFile#open` will now always open a new IO object, and close
48 | the previous one. This way the uploaded file will be reopened if it's closed.
49 |
50 | * Fixed possible encoding issues in `Shrine::Storage::S3#upload` when filesize
51 | is unknown.
52 |
53 | * The registered storage resolvers in the `dynamic_storage` plugin are now
54 | correctly inherited upon subclassing.
55 |
56 | * The `:file` MIME type analyzer from `determine_mime_type` plugin will now
57 | raise an explicit `Shrine::Error` when the subprocess from the shell command
58 | failed to be spawned.
59 |
60 | * `Attacher#data_uri=` and `Attacher#remote_url=` in addition to `""` now also
61 | ignore `nil` values, instead of raising an exception.
62 |
63 | ## Backwards compatibility
64 |
65 | * The `:storages` option has been deprecated in the `download_endpoint` plugin
66 | in favour of the new `Shrine::UploadedFile#download_url` method.
67 |
68 | * The deprecation of assigning cached versions has been undone, as it can be a
69 | useful feature.
70 |
--------------------------------------------------------------------------------
/doc/release_notes/2.16.0.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Shrine 2.16.0
3 | ---
4 |
5 | ## New Features
6 |
7 | * The `:download_options` option has been added to the `download_endpoint`
8 | plugin, for specifying options passed to `Storage#open`.
9 |
10 | ```rb
11 | plugin :download_endpoint,
12 | download_options: {
13 | sse_customer_algorithm: "AES256",
14 | sse_customer_key: "secret_key",
15 | sse_customer_key_md5: "secret_key_md5",
16 | }
17 | ```
18 |
19 | * The `:upload_open_options` option has been added to the `derivation_endpoint`
20 | plugin, for specifying options passed to `Storage#open` when downloading a
21 | cached derivation result.
22 |
23 | ```rb
24 | plugin :download_endpoint,
25 | upload: true,
26 | upload_open_options: { response_content_encoding: "gzip" }
27 | ```
28 |
29 | ## Other improvements
30 |
31 | * The `rack_response` and `derivation_endpoint` plugins now don't return any
32 | `Content-Type` response header if the MIME type could not be determined from
33 | the file extension. Previously it the `Content-Type` header would default to
34 | `application/octet-stream`, which would force the browser to view the file
35 | as generic binary content, as opposed to doing its own MIME type sniffing.
36 |
37 | * Fixed `delete_raw` plugin breaking `derivation_endpoint` when `:upload` was
38 | enabled.
39 |
40 | * Fixed a few things in the `Shrine::Derivation` API:
41 |
42 | * `Derivation#upload` doesn't close the input file anymore
43 | * `Derivation#upload` now requires input file to respond to `#path`
44 | * `Derivation#upload` now deletes the internally generated derivation result
45 | * `Derivation#processed` now works when derivation result is a `File` object
46 |
47 | * The official demo app now shows the `derivation_endpoint` plugin.
48 |
49 | * The `#to_rack_response` method from the `rack_response` plugin now always
50 | opens the `UploadedFile`, and does so upfront. This means if ther are any
51 | download errors, they will bubble up from `#to_rack_response` as opposed to
52 | when the response body is iterated over.
53 |
54 | * When `store_dimensions` plugin was overriding `Shrine#extract_metadata`, it
55 | made the second argument (the `context` hash) mandatory. This has been fixed,
56 | now the second argument is optional again.
57 |
--------------------------------------------------------------------------------
/doc/release_notes/2.3.0.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Shrine 2.3.0
3 | ---
4 |
5 | ## New plugins
6 |
7 | * The `copy` plugin has been added for copying files from one record to
8 | another. Duplicating the model instance will automatically create a copy
9 | of the attachment.
10 |
11 | ```rb
12 | Shrine.plugin :copy
13 | ```
14 | ```rb
15 | photo = Photo.find(photo_id)
16 | duplicated_photo = photo.dup # duplicates the attachment
17 |
18 | photo.image != duplicated_photo.image
19 | ```
20 | ```rb
21 | photo.image_attacher.copy(other_photo.image_attacher)
22 | ```
23 |
24 | ## New Features
25 |
26 | * The `:directory_permissions` option has been added to `Storage::FileSystem`
27 | for specifying UNIX permissions of all directories inside the main directory.
28 |
29 | ## Other improvements
30 |
31 | * Directory permissions are now applied to aoll subdirectories inside the main
32 | directory
33 |
34 | * The default UNIX permissions are now 0644 for files and 0755 for directories.
35 | Previously it relied on defaults of the operating system.
36 |
37 | * The `backgrounding` plugin doesn't require the model instance to have the
38 | `#id=` writer method anymore.
39 |
40 | * The `Attacher#read` method for returning the value of the underlying column
41 | is now public.
42 |
43 | * The `Attacher#context` can now be mutated for an instantiated Attacher object.
44 |
45 | * Fixed `Attacher#swap` method being private after loading `sequel` or
46 | `activerecord` plugin.
47 |
48 | * The `recache` plugin behaviour has been extracted into `Attacher#recache`, so
49 | that it can be used standalone.
50 |
51 | * The `moving` plugin now works correctly with `backup` plugin.
52 |
53 | * The `direct_upload` plugin now prevents the client from caching the presign
54 | response, by returning `Cache-Control: no-store` header.
55 |
--------------------------------------------------------------------------------
/doc/release_notes/2.3.1.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Shrine 2.3.1
3 | ---
4 |
5 | Regressions
6 | ===========
7 |
8 | * The previous version introduced explicit default permissions for created
9 | files and directories in FileSystem storage. However, the current behaviour
10 | was applying directory permissions to the directory the storage is
11 | initialized with. This would raise a `Errno::EPERM` exception for directories
12 | for which the permissions cannot be changed, for example `Dir.tmpdir`. This
13 | is now changed; when FileSystem storage is now initialized with a directory,
14 | the permissions won't be changed if this directory already exists.
15 |
--------------------------------------------------------------------------------
/doc/release_notes/2.4.1.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Shrine 2.4.1
3 | ---
4 |
5 | ## Regressions
6 |
7 | * Prior to version 2.4.0, the conversion of uploaded file data to JSON was
8 | happening in `Attacher#_set`, and `Attacher#write` simply assigned the value
9 | that it received to the data attribute of the model instance. During
10 | refactoring for 2.4.0, the JSON conversion was accidentally moved to
11 | `Attacher#write`, which affects anyone who was using `Attacher#write`
12 | directly and passing it a JSON string, by resulting in a double-encoded
13 | string being assigned to the data attribute. This has now been reverted.
14 |
15 | ## Improvements
16 |
17 | * The `remove_invalid` plugin will now assign back a previous attachment if it
18 | was there, in case the assigned cached file was invalid. Before `nil` was
19 | always assigned. This enables you to display the previous attachment to the
20 | user in case of file validation errors.
21 |
22 | * `UploadedFile#download` will now create the Tempfile using
23 | `UploadedFile#extension`, which handles the case when `#id` doesn't have
24 | extension, but `#original_filename` has (which could be the behaviour of
25 | some custom storages)
26 |
27 | ## Backwards compatibility
28 |
29 | * The `FileSystem#download` method has been deprecated, and will be removed in
30 | Shrine 3. The replacement is to simply use `UploadedFile#download`, which will
31 | use `FileSystem#open` to create a Tempfile in the same way
32 | `FileSystem#download` did previously. This shouldn't affect you unless you
33 | were using `FileSystem#download` directly.
34 |
--------------------------------------------------------------------------------
/doc/release_notes/2.6.1.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Shrine 2.6.1
3 | ---
4 |
5 | ## Bug fixes
6 |
7 | * When `download_endpoint` streams file content into the response body, with
8 | certain storages (FileSystem, SQL) it will use a buffer object when reading
9 | each chunk, which will make that, instead of each read chunk allocating a new
10 | string object, ever new chunk is read into the same buffer string object,
11 | replacing the content from the previous chunk.
12 |
13 | Unfortunately, there are some parts of the Rack/Rails stack which rely on all
14 | chunks co-existing in the memory at the same time (for example the
15 | `Rack::ContentLenth` middleware). For that reason, and to be consistent in
16 | behaviour and memory usage when used with other storages (S3), we modify
17 | `download_endpoint` not to use the buffer object, and have each read chunk be
18 | a new string object.
19 |
--------------------------------------------------------------------------------
/doc/release_notes/2.9.0.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Shrine 2.9.0
3 | ---
4 |
5 | ## New Plugins
6 |
7 | * The `infer_extension` plugin has been added, which automatically infers
8 | the upload location extension from the MIME type if it's unknown. This is
9 | useful when using the `data_uri` plugin (where the extension is always
10 | unknown), or the `remote_url` plugin (where the extension might not be
11 | known).
12 |
13 | ```rb
14 | Shrine.plugin :infer_extension
15 | ```
16 |
17 | ## New Features
18 |
19 | * The `determine_mime_type` plugin has gained built-in support for the [Marcel]
20 | gem, Basecamp's wrapper around the existing [MimeMagic].
21 |
22 | ```rb
23 | Shrine.plugin :determine_mime_type, analyzer: :marcel
24 | ```
25 |
26 | * The `versions` plugin now supports saving arrays of files, as well as nested
27 | structures of arrays and hashes.
28 |
29 | ```rb
30 | class PdfUploader < Shrine
31 | plugin :versions
32 | plugin :processing
33 |
34 | process(:store) do |io, context|
35 | [ file1, file2, ... ]
36 | # or
37 | { name1: [ file1, file2, ... ], ... }
38 | # or
39 | [ { name1: file1 }, ... ]
40 | end
41 | end
42 | ```
43 |
44 | * `UploadedFile#open` can now be called without passing a block. This is useful
45 | if you want to make it explicit when the uploaded file is opened, instead of
46 | it happening automatically when `#read` is called.
47 |
48 | ```rb
49 | uploaded_file.open # opens the local or remote file
50 | uploaded_file.read # read content
51 | uploaded_file.close # close the file
52 | ```
53 |
54 | * The `Model#_attacher` now accepts options which are forwarded to
55 | `Shrine::Attacher.new`.
56 |
57 | ```rb
58 | photo.image_attacher(store: :other_store)
59 | photo.update(image: file) # uploads to :other_store
60 | ```
61 |
62 | ## Other improvements
63 |
64 | * Fixed `backgrounding` plugin not detecting options passed to
65 | `Shrine::Attachment.new`.
66 |
67 | * Fixed S3 storage replacing whitespace with `+` symbols in original filename
68 | when assigning object's `:content_disposition` on upload.
69 |
70 | * If an error occurs when downloading the file in `UploadedFile#download` and
71 | `Storage::S3#download`, the tempfile with partially downloaded content will
72 | now automatically be deleted.
73 |
74 | * Shrine now uses the `frozen-string-literal` feature, which will reduce the
75 | number of string allocations.
76 |
77 | ## Backwards compatibility
78 |
79 | * The `:filename` option of the `data_uri` plugin has been deprecated in favour
80 | of the new `infer_extension` plugin.
81 |
82 | * The `multi_delete` plugin has been deprecated, and the `versions` plugin isn't
83 | loading it by default anymore.
84 |
85 | [Marcel]: https://github.com/basecamp/marcel
86 | [MimeMagic]: https://github.com/minad/mimemagic
87 |
--------------------------------------------------------------------------------
/doc/release_notes/3.0.1.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Shrine 3.0.1
3 | ---
4 |
5 | ## Regressions
6 |
7 | * Fixed `metadata_attributes` plugin raising an exception when the attachment
8 | is removed (assigned to `nil`).
9 |
10 | ## Improvements
11 |
12 | * Simplified `UploadedFile#inspect` output:
13 |
14 | ```rb
15 | # before
16 | uploaded_file.inspect
17 | #=> #nil, "size"=>0, "mime_type"=>nil}>
18 |
19 | # after
20 | uploaded_file.inspect
21 | #=> #nil, "size"=>0, "mime_type"=>nil}>
22 | ```
23 |
--------------------------------------------------------------------------------
/doc/release_notes/3.1.0.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Shrine 3.1.0
3 | ---
4 |
5 | ## New features
6 |
7 | * The `Attacher#create_derivatives` method now accepts a `:storage` option for
8 | specifying the storage to which derivatives should be uploaded.
9 |
10 | ```rb
11 | # with attachment module
12 | photo.image_derivatives!(storage: :other_store)
13 |
14 | # with attacher
15 | attacher.create_derivatives(storage: :other_store)
16 | ```
17 |
18 | * The `Shrine.calculate_signature` now accepts a `:rewind` boolean option for
19 | choosing whether the IO object should be rewinded after reading. This is
20 | useful if you want to calculate signature from non-rewindable IO objects,
21 | such as `IO.pipe`, `Socket`, non-rewindable `Down::ChunkedIO` etc.
22 |
23 | ```rb
24 | Shrine.signature(io, rewind: false)
25 | ```
26 |
27 | ## Improvements
28 |
29 | * The derivatives processor can now be registered with `Attacher.derivatives`,
30 | which is just an alias for `Attacher.derivatives_processor`.
31 |
32 | ```rb
33 | class ImageUploader < Shrine
34 | Attacher.derivatives_processor do |original|
35 | # ...
36 | end
37 | end
38 |
39 | # can now be written as
40 |
41 | class ImageUploader < Shrine
42 | Attacher.derivatives do |original|
43 | # ...
44 | end
45 | end
46 | ```
47 |
48 | * The `Attacher#cached?` and `Attacher#stored?` methods now work correctly if
49 | temporary/permanent storage identifiers were specified as strings.
50 |
51 | * The `store_dimensions` plugin now properly propagates exceptions when loading
52 | the `ruby-vips` gem in `:vips` analyzer.
53 |
54 | * The `add_metadata` plugin now respects inheritance again when defining
55 | metadata methods on the `Shrine::UploadedFile` class. In 2.19.0, the
56 | `add_metadata` plugin was changed to define metadata methods on the internal
57 | `FileMethods` plugin module, which is shared across all uploaders. This
58 | change has now been reverted.
59 |
60 | ## Backwards compatibility
61 |
62 | * The `Attacher#cache_key` and `Attacher#store_key` methods now always return
63 | symbol keys, even if the storage key that was specified was a string key.
64 |
65 | ```rb
66 | attacher = Shrine::Attacher.new(cache: "cache", store: "store")
67 | attacher.cache_key #=> :cache (previously "cache")
68 | attacher.store_key #=> :store (previously "store")
69 | ```
70 |
71 | * The `add_metadata` plugin now defines metadata methods directly on the
72 | `UploadedFile` class, which means that if you happen to have been overriding
73 | these metadata methods and calling `super`, this won't work anymore.
74 |
--------------------------------------------------------------------------------
/doc/release_notes/3.2.1.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Shrine 3.2.1
3 | ---
4 |
5 | ## Ruby 2.7 compatibility
6 |
7 | * Shrine doesn't trigger [Ruby 2.7 warnings for separation of positional and
8 | keyword arguments][kwargs] anymore.
9 |
10 | * Down 5.1.0 has been released, which resolves warnings and a `FrozenError`
11 | exception on Ruby 2.7. Shrine now requires at least this version of Down.
12 |
13 | If you're using `Down::Http`, make sure you're using http.rb 4.3.0 or newer.
14 |
15 | * ImageProcessing 1.10.3 gem has been released which resolves Ruby 2.7 warnings
16 | as well. If you're using it for image processing, make sure to upgrade to
17 | this version:
18 |
19 | ```rb
20 | gem "image_processing", ">= 1.10.3", "< 2"
21 | ```
22 |
23 | ## Rack 2.1 compatibility
24 |
25 | * The `derivation_endpoint` plugin now uses `Rack::Files` on Rack 2.1 or newer.
26 |
27 | ## Other improvements
28 |
29 | * The `S3#open` method now handles empty S3 objects.
30 |
31 | [kwargs]: https://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-keyword-arguments-in-ruby-3-0/
32 |
--------------------------------------------------------------------------------
/doc/release_notes/3.2.2.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Shrine 3.2.2
3 | ---
4 |
5 | ## Bug fixes
6 |
7 | * aws-sdk-core 3.104.0 introduced a backwards incompatible changes that caused
8 | `Shrine::Storage::S3#open` to start raising an exception.
9 |
10 | ```
11 | NoMethodError: undefined method `bytesize' for #
12 | ```
13 |
14 | This has now been fixed.
15 |
--------------------------------------------------------------------------------
/doc/release_notes/3.4.0.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Shrine 3.4.0
3 | ---
4 |
5 | * Passing attacher options to `Shrine.Attachment` method now works on Ruby 3.0.
6 |
7 | * Defining validation errors as an array of I18n key and options in
8 | `activerecord` plugin now works on Ruby 3.0.
9 |
10 | * The `:fastimage` MIME type analyzer now correctly detects SVGs as
11 | `image/svg+html` in the `determine_mime_type` plugin.
12 |
13 | * The `Shrine::Attacher#read` method provided by the `entity` plugin is now
14 | public. This is consistent with `Shrine::Attacher#write` from `model` plugin
15 | being public as well.
16 |
17 | * The `Shrine::Attacher#reload` method now resets attachment's dirty state.
18 | This means that for a model whose `Attacher#changed?` returns `true`, calling
19 | `#reload` on the model will make `Attacher#changed?` return `false`. This was
20 | the behaviour before Shrine 3.3.0.
21 |
22 | ```rb
23 | # before
24 | model.file_attacher.changed? #=> true
25 | model.reload
26 | model.file_attacher.changed? #=> true
27 |
28 | # after
29 | model.file_attacher.changed? #=> true
30 | model.reload
31 | model.file_attacher.changed? #=> false
32 | ```
33 |
34 | * Calling `#reload` on the model will not initialize a `Shrine::Attacher`
35 | instance anymore if one hasn't previously been initialized.
36 |
--------------------------------------------------------------------------------
/doc/release_notes/3.5.0.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Shrine 3.5.0
3 | ---
4 |
5 | ## New features
6 |
7 | * The website has been migrated to Docusaurus v2. :sparkles:
8 |
9 | * The `:signer` option has been added to the `derivation_endpoint` plugin, for when you want to use custom URL signing. This is useful when using `:expires_in`, and wanting to have expiring URLs work with CDN caching.
10 |
11 | ```rb
12 | require "aws-sdk-cloudfront"
13 | signer = Aws::CloudFront::UrlSigner.new(key_pair_id: "...", private_key: "...")
14 |
15 | plugin :derivation_endpoint,
16 | expires_in: 90,
17 | signer: -> (url, expires_in:) do
18 | signer.signed_url(url, expires: Time.now.to_i + expires_in)
19 | end
20 | ```
21 |
22 | * The S3 storage now supports `:max_multipart_parts` option for specifying the maximum number of concurrent parts in which a large file will get uploaded. This number defaults to `10_000`.
23 |
24 | ```rb
25 | Shrine::Storage::S3.new(max_multipart_parts: 1000, ...)
26 | ```
27 |
28 | * The `:encoding` option can now be passed to `S3#open`, which is applied to downloaded chunks.
29 |
30 | ```rb
31 | io = uploaded_file.open(encoding: Encoding::UTF_8)
32 | csv = CSV.new(io)
33 | # ...
34 | ```
35 |
36 | ## Other improvements
37 |
38 | * Passing a boolean value to the `#remove_attachment=` setter now works on Ruby 3.2. Previously this would raise an error, because Shrine would try to call `=~` on it, but `Object#=~` method has been removed in Ruby 3.2.
39 |
40 | * When duplicating a model instance, the duplicated attacher now references the duplicated model instance instead of the original one.
41 |
42 | * The download endpoint now returns a `400 Bad Request` response when the serialized file component is invalid.
43 |
44 | * The `derivatives` plugin now supports passing `mutex: false` option to disable usage of a mutex. This makes the `Shrine::Attacher` object marshallable, which should enable using `Marshal.dump` and `Marshal.load` on model instances with attachments. This should be safe unless you're adding derivatives on the same attacher object concurrently.
45 |
46 | * When loading the `derivatives` plugin with `versions_compatibility: true`, this setting doesn't leak to other uploaders anymore. Previously if other uploaders would load `derivatives` plugin without this option, versions compatibility would still get enabled for them. This change also fixes behavior on JRuby.
47 |
48 | * When S3 storage copies files, the AWS tag are not inherited anymore. This allows passing the `:tagging` upload option when promoting from temporary to permanent storage, and have it take effect.
49 |
50 | * The `UploadedFile#url` method doesn't call the obsolete `URI.regexp` method anymore, which should avoid warnings.
51 |
52 | * The `infer_extension` plugin now defines `infer_extension` instance method (in addition to class method) on the uploader for convenience, so that it can be easily called at the uploader instance level.
53 |
54 | ```rb
55 | class MyUploader < Shrine
56 | plugin :infer_extension
57 |
58 | def generate_location(io, metadata:, **)
59 | extension = infer_extension(metadata["mime_type"])
60 | # ...
61 | end
62 | end
63 | ```
64 |
--------------------------------------------------------------------------------
/doc/release_notes/3.6.0.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Shrine 3.6.0
3 | ---
4 |
5 | ## New features
6 |
7 | * The S3 storage now accepts `:copy_options` when initializing. This can be used for supporting Cloudflare R2 by removing `:tagging_directive` when copying file from temporary to permanent storage.
8 |
9 | ```rb
10 | Shrine::Storage::S3.new(bucket: BUCKET, copy_options: {}, **s3_options)
11 | ```
12 |
13 | ## Other improvements
14 |
15 | * Rack 3 is now supported.
16 |
17 | * When duplicating the attacher, the `Attacher#context` hash is now copied as well, instead of being kept the same between the two attachers.
18 |
19 | * After `UploadedFile#close` was called, `UploadedFile#opened?` will return `false` and the uploaded file can be implicitly re-opened again.
20 |
21 | ## Backwards compatibility
22 |
23 | * Shrine API that is returning a rack response triple will now return headers as an instance of `Rack::Headers` on Rack 3, which is a subclass of `Hash`. This should keep user code that references header names in mixed case working (in addition to lowercase), but could theoretically cause issues for code explicitly requiring headers to be an instance of `Hash`.
24 |
--------------------------------------------------------------------------------
/doc/storage/memory.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Memory
3 | ---
4 |
5 | The Memory storage stores uploaded files in memory, which is suitable for
6 | testing.
7 |
8 | ```rb
9 | Shrine.storages[:store] = Shrine::Storage::Memory.new
10 | ```
11 |
12 | By default, each storage instance uses a new Hash object for storing files,
13 | but you can pass your own:
14 |
15 | ```rb
16 | my_store = Hash.new
17 |
18 | Shrine.storages[:store] = Shrine::Storage::Memory.new(my_store)
19 | ```
20 |
--------------------------------------------------------------------------------
/doc/validation.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: File Validation
3 | ---
4 |
5 | Shrine allows validating assigned files using the [`validation`][validation]
6 | plugin. Validation code is defined inside an `Attacher.validate` block:
7 |
8 | ```rb
9 | Shrine.plugin :validation
10 | ```
11 | ```rb
12 | class ImageUploader < Shrine
13 | Attacher.validate do
14 | # ... perform validation ...
15 | end
16 | end
17 | ```
18 |
19 | The validation block is run when a new file is assigned, and any validation
20 | errors are stored in `Shrine::Attacher#errors`. Persistence plugins like
21 | `sequel` and `activerecord` will automatically merge these validation errors
22 | into the `#errors` hash on the model instance.
23 |
24 | ```rb
25 | photo = Photo.new
26 | photo.image = image_file
27 | photo.valid? #=> false
28 | photo.errors[:image] #=> [...]
29 | ```
30 |
31 | ## Validation helpers
32 |
33 | The [`validation_helpers`][validation_helpers] plugin provides convenient
34 | validators for built-in metadata:
35 |
36 | ```rb
37 | Shrine.plugin :validation_helpers
38 | ```
39 | ```rb
40 | class ImageUploader < Shrine
41 | Attacher.validate do
42 | validate_size 1..5*1024*1024
43 | validate_mime_type %w[image/jpeg image/png image/webp image/tiff]
44 | validate_extension %w[jpg jpeg png webp tiff tif]
45 | end
46 | end
47 | ```
48 |
49 | Note that for secure MIME type validation it's recommended to also load
50 | `determine_mime_type` and `restore_cached_data` plugins.
51 |
52 | See the [`validation_helpers`][validation_helpers] plugin documentation for
53 | more details.
54 |
55 | ## Custom validations
56 |
57 | You can also do your own custom validations:
58 |
59 | ```rb
60 | # Gemfile
61 | gem "streamio-ffmpeg"
62 | ```
63 | ```rb
64 | require "streamio-ffmpeg"
65 |
66 | class VideoUploader < Shrine
67 | plugin :add_metadata
68 |
69 | add_metadata :duration do |io|
70 | movie = Shrine.with_file(io) { |file| FFMPEG::Movie.new(file.path) }
71 | movie.duration
72 | end
73 |
74 | Attacher.validate do
75 | if file.duration > 5*60*60
76 | errors << "duration must not be longer than 5 hours"
77 | end
78 | end
79 | end
80 | ```
81 |
82 | ## Inheritance
83 |
84 | Validations are inherited from superclasses, but you need to call them manually
85 | when defining more validations:
86 |
87 | ```rb
88 | class ApplicationUploader < Shrine
89 | Attacher.validate { validate_max_size 5*1024*1024 }
90 | end
91 | ```
92 | ```rb
93 | class ImageUploader < ApplicationUploader
94 | Attacher.validate do
95 | super() # empty parentheses are required
96 | validate_mime_type %w[image/jpeg image/png image/webp]
97 | end
98 | end
99 | ```
100 |
101 | ## Removing invalid files
102 |
103 | By default, an invalid file will remain assigned after validation failed, but
104 | you can have it automatically removed and deleted by loading the
105 | `remove_invalid` plugin.
106 |
107 | ```rb
108 | Shrine.plugin :remove_invalid # remove and delete files that failed validation
109 | ```
110 |
111 | [validation]: https://shrinerb.com/docs/plugins/validation
112 | [validation_helpers]: https://shrinerb.com/docs/plugins/validation_helpers
113 |
--------------------------------------------------------------------------------
/lib/shrine/attachment.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Shrine
4 | # Core class that provides an attachment interface for a specified attribute
5 | # name, which can be added to model/entity classes. The model/entity plugins
6 | # define the main interface, which delegates to a Shrine::Attacher object.
7 | class Attachment < Module
8 | @shrine_class = ::Shrine
9 |
10 | module ClassMethods
11 | # Returns the Shrine class that this attachment class is
12 | # namespaced under.
13 | attr_accessor :shrine_class
14 |
15 | # Since Attachment is anonymously subclassed when Shrine is subclassed,
16 | # and then assigned to a constant of the Shrine subclass, make inspect
17 | # reflect the likely name for the class.
18 | def inspect
19 | "#{shrine_class.inspect}::Attachment"
20 | end
21 |
22 | # Shorthand for `Attachment.new`.
23 | #
24 | # Shrine::Attachment[:image]
25 | def [](*args, **options)
26 | new(*args, **options)
27 | end
28 | end
29 |
30 | module InstanceMethods
31 | # Instantiates an attachment module for a given attribute name, which
32 | # can then be included to a model class. Second argument will be passed
33 | # to an attacher module.
34 | def initialize(name, **options)
35 | @name = name.to_sym
36 | @options = options
37 | end
38 |
39 | # Returns name of the attachment this module provides.
40 | def attachment_name
41 | @name
42 | end
43 |
44 | # Returns options that are to be passed to the Attacher.
45 | def options
46 | @options
47 | end
48 |
49 | # Returns class name with attachment name included.
50 | #
51 | # Shrine::Attachment.new(:image).to_s #=> "#"
52 | def inspect
53 | "#<#{self.class.inspect}(#{@name})>"
54 | end
55 | alias to_s inspect
56 |
57 | # Returns the Shrine class that this attachment's class is namespaced
58 | # under.
59 | def shrine_class
60 | self.class.shrine_class
61 | end
62 | end
63 |
64 | extend ClassMethods
65 | include InstanceMethods
66 | end
67 | end
68 |
--------------------------------------------------------------------------------
/lib/shrine/plugins.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Shrine
4 | # Module in which all Shrine plugins should be stored. Also contains logic
5 | # for registering and loading plugins.
6 | module Plugins
7 | @plugins = {}
8 |
9 | # If the registered plugin already exists, use it. Otherwise, require it
10 | # and return it. This raises a LoadError if such a plugin doesn't exist,
11 | # or a Shrine::Error if it exists but it does not register itself
12 | # correctly.
13 | def self.load_plugin(name)
14 | unless plugin = @plugins[name]
15 | require "shrine/plugins/#{name}"
16 | raise Error, "plugin #{name} did not register itself correctly in Shrine::Plugins" unless plugin = @plugins[name]
17 | end
18 | plugin
19 | end
20 |
21 | # Delegate call to the plugin in a way that works across Ruby versions.
22 | def self.load_dependencies(plugin, uploader, *args, **kwargs, &block)
23 | return unless plugin.respond_to?(:load_dependencies)
24 |
25 | if kwargs.any?
26 | plugin.load_dependencies(uploader, *args, **kwargs, &block)
27 | else
28 | plugin.load_dependencies(uploader, *args, &block)
29 | end
30 | end
31 |
32 | # Delegate call to the plugin in a way that works across Ruby versions.
33 | def self.configure(plugin, uploader, *args, **kwargs, &block)
34 | return unless plugin.respond_to?(:configure)
35 |
36 | if kwargs.any?
37 | plugin.configure(uploader, *args, **kwargs, &block)
38 | else
39 | plugin.configure(uploader, *args, &block)
40 | end
41 | end
42 |
43 | # Register the given plugin with Shrine, so that it can be loaded using
44 | # `Shrine.plugin` with a symbol. Should be used by plugin files. Example:
45 | #
46 | # Shrine::Plugins.register_plugin(:plugin_name, PluginModule)
47 | def self.register_plugin(name, mod)
48 | @plugins[name] = mod
49 | end
50 | end
51 | end
52 |
--------------------------------------------------------------------------------
/lib/shrine/plugins/_persistence.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Shrine
4 | module Plugins
5 | # Helper plugin that defines persistence methods on the attacher according
6 | # to convention.
7 | #
8 | # plugin :_persistence, plugin: MyPlugin
9 | module Persistence
10 | def self.load_dependencies(uploader, *)
11 | uploader.plugin :atomic_helpers
12 | uploader.plugin :entity
13 | end
14 |
15 | # Using #_persist, #_reload, and #?, defines the
16 | # following methods for a persistence plugin:
17 | #
18 | # * Attacher#persist
19 | # * Attacher#atomic_persist
20 | # * Attacher#atomic_promote
21 | def self.configure(uploader, plugin:)
22 | plugin_name = plugin.to_s.split("::").last.downcase
23 |
24 | plugin::AttacherMethods.module_eval do
25 | define_method :atomic_promote do |**options, &block|
26 | return super(**options, &block) unless send(:"#{plugin_name}?")
27 |
28 | abstract_atomic_promote(
29 | reload: method(:"#{plugin_name}_reload"),
30 | persist: method(:"#{plugin_name}_persist"),
31 | **options, &block
32 | )
33 | end
34 |
35 | define_method :atomic_persist do |*args, **options, &block|
36 | return super(*args, **options, &block) unless send(:"#{plugin_name}?")
37 |
38 | abstract_atomic_persist(
39 | *args,
40 | reload: method(:"#{plugin_name}_reload"),
41 | persist: method(:"#{plugin_name}_persist"),
42 | **options, &block
43 | )
44 | end
45 |
46 | define_method :persist do
47 | return super() unless send(:"#{plugin_name}?")
48 |
49 | send(:"#{plugin_name}_persist")
50 | end
51 |
52 | define_method :hash_attribute? do
53 | return super() unless send(:"#{plugin_name}?")
54 |
55 | respond_to?(:"#{plugin_name}_hash_attribute?", true) &&
56 | send(:"#{plugin_name}_hash_attribute?")
57 | end
58 | private :hash_attribute?
59 | end
60 | end
61 |
62 | module AttacherMethods
63 | def atomic_promote(*)
64 | raise NotImplementedError, "unhandled by a persistence plugin"
65 | end
66 |
67 | def atomic_persist(*)
68 | raise NotImplementedError, "unhandled by a persistence plugin"
69 | end
70 |
71 | def persist(*)
72 | raise NotImplementedError, "unhandled by a persistence plugin"
73 | end
74 |
75 | # Disable attachment data serialization for data attributes that
76 | # accept and return hashes.
77 | def set_entity(*)
78 | super
79 | @column_serializer = nil if hash_attribute?
80 | end
81 |
82 | private
83 |
84 | # Whether the data attribute accepts and returns hashes.
85 | def hash_attribute?
86 | false
87 | end
88 | end
89 | end
90 |
91 | register_plugin(:_persistence, Persistence)
92 | end
93 | end
94 |
--------------------------------------------------------------------------------
/lib/shrine/plugins/_urlsafe_serialization.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "base64"
4 | require "json"
5 |
6 | class Shrine
7 | module Plugins
8 | # This is an internal plugin used by download_endpoint and
9 | # derivation_endpoint plugins.
10 | module UrlsafeSerialization
11 | module ClassMethods
12 | def urlsafe_serialize(hash)
13 | urlsafe_serializer.encode(hash)
14 | end
15 |
16 | def urlsafe_deserialize(string)
17 | urlsafe_serializer.decode(string)
18 | end
19 |
20 | def urlsafe_serializer
21 | Serializer.new
22 | end
23 | end
24 |
25 | module FileMethods
26 | def urlsafe_dump(**options)
27 | self.class.urlsafe_dump(self, **options)
28 | end
29 |
30 | def urlsafe_data(metadata: [])
31 | data = self.data.dup
32 |
33 | if metadata.any?
34 | # order metadata in the specified order
35 | data["metadata"] = metadata
36 | .map { |name| [name, self.metadata[name]] }
37 | .to_h
38 | else
39 | # save precious characters
40 | data.delete("metadata")
41 | end
42 |
43 | data
44 | end
45 | end
46 |
47 | module FileClassMethods
48 | def urlsafe_dump(file, **options)
49 | data = file.urlsafe_data(**options)
50 |
51 | shrine_class.urlsafe_serialize(data)
52 | end
53 |
54 | def urlsafe_load(string)
55 | data = shrine_class.urlsafe_deserialize(string)
56 |
57 | new(data)
58 | end
59 | end
60 |
61 | class Serializer
62 | def encode(data)
63 | base64_encode(json_encode(data))
64 | end
65 |
66 | def decode(data)
67 | json_decode(base64_decode(data))
68 | end
69 |
70 | private
71 |
72 | def json_encode(data)
73 | JSON.generate(data)
74 | end
75 |
76 | def base64_encode(data)
77 | Base64.urlsafe_encode64(data, padding: false)
78 | end
79 |
80 | def base64_decode(data)
81 | Base64.urlsafe_decode64(data)
82 | end
83 |
84 | def json_decode(data)
85 | JSON.parse(data)
86 | end
87 | end
88 | end
89 |
90 | register_plugin(:_urlsafe_serialization, UrlsafeSerialization)
91 | end
92 | end
93 |
--------------------------------------------------------------------------------
/lib/shrine/plugins/add_metadata.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Shrine
4 | module Plugins
5 | # Documentation can be found on https://shrinerb.com/docs/plugins/add_metadata
6 | module AddMetadata
7 | def self.configure(uploader)
8 | uploader.opts[:add_metadata] ||= { definitions: [] }
9 | end
10 |
11 | module ClassMethods
12 | def add_metadata(name = nil, **options, &block)
13 | opts[:add_metadata][:definitions] << [name, options, block]
14 |
15 | metadata_method(name) if name
16 | end
17 |
18 | def metadata_method(*names)
19 | names.each { |name| _metadata_method(name) }
20 | end
21 |
22 | private
23 |
24 | def _metadata_method(name)
25 | self::UploadedFile.send(:define_method, name) do
26 | metadata[name.to_s]
27 | end
28 | end
29 | end
30 |
31 | module InstanceMethods
32 | def extract_metadata(io, **options)
33 | metadata = super
34 |
35 | extract_custom_metadata(io, **options, metadata: metadata)
36 |
37 | metadata
38 | end
39 |
40 | private
41 |
42 | def extract_custom_metadata(io, **options)
43 | opts[:add_metadata][:definitions].each do |name, definition_options, block|
44 | result = instance_exec(io, **options, &block)
45 |
46 | if result.nil? && definition_options[:skip_nil]
47 | # Do not store this metadata
48 | elsif name
49 | options[:metadata].merge! name.to_s => result
50 | else
51 | options[:metadata].merge! result.transform_keys(&:to_s) if result
52 | end
53 |
54 | # rewind between metadata blocks
55 | io.rewind
56 | end
57 | end
58 | end
59 |
60 | module AttacherMethods
61 | def add_metadata(new_metadata, &block)
62 | file!.add_metadata(new_metadata, &block)
63 | set(file) # trigger model write
64 | end
65 | end
66 |
67 | module FileMethods
68 | def add_metadata(new_metadata, &block)
69 | @metadata = @metadata.merge(new_metadata, &block)
70 | end
71 | end
72 | end
73 |
74 | register_plugin(:add_metadata, AddMetadata)
75 | end
76 | end
77 |
--------------------------------------------------------------------------------
/lib/shrine/plugins/cached_attachment_data.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Shrine
4 | module Plugins
5 | # Documentation can be found on https://shrinerb.com/docs/plugins/cached_attachment_data
6 | module CachedAttachmentData
7 | module AttachmentMethods
8 | def define_model_methods(name)
9 | super if defined?(super)
10 |
11 | define_method :"cached_#{name}_data" do
12 | send(:"#{name}_attacher").cached_data
13 | end
14 | end
15 | end
16 |
17 | module AttacherMethods
18 | def cached_data
19 | file.to_json if cached? && changed?
20 | end
21 | end
22 | end
23 |
24 | register_plugin(:cached_attachment_data, CachedAttachmentData)
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/lib/shrine/plugins/default_storage.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Shrine
4 | module Plugins
5 | # Documentation can be found on https://shrinerb.com/docs/plugins/default_storage
6 | module DefaultStorage
7 | def self.configure(uploader, **opts)
8 | uploader.opts[:default_storage] ||= {}
9 | uploader.opts[:default_storage].merge!(opts)
10 | end
11 |
12 | module AttacherClassMethods
13 | def default_cache(value = nil, &block)
14 | default_storage.merge!(cache: value || block)
15 | end
16 |
17 | def default_store(value = nil, &block)
18 | default_storage.merge!(store: value || block)
19 | end
20 |
21 | private
22 |
23 | def default_storage
24 | shrine_class.opts[:default_storage]
25 | end
26 | end
27 |
28 | module AttacherMethods
29 | def initialize(**options)
30 | super(**shrine_class.opts[:default_storage], **options)
31 | end
32 |
33 | def cache_key
34 | if @cache.respond_to?(:call)
35 | if @cache.arity == 2
36 | Shrine.deprecation("Passing record & name argument to default storage block is deprecated and will be removed in Shrine 4. Use a block without arguments instead.")
37 | @cache.call(record, name).to_sym
38 | else
39 | instance_exec(&@cache).to_sym
40 | end
41 | else
42 | super
43 | end
44 | end
45 |
46 | def store_key
47 | if @store.respond_to?(:call)
48 | if @store.arity == 2
49 | Shrine.deprecation("Passing record & name argument to default storage block is deprecated and will be removed in Shrine 4. Use a block without arguments instead.")
50 | @store.call(record, name).to_sym
51 | else
52 | instance_exec(&@store).to_sym
53 | end
54 | else
55 | super
56 | end
57 | end
58 | end
59 | end
60 |
61 | register_plugin(:default_storage, DefaultStorage)
62 | end
63 | end
64 |
--------------------------------------------------------------------------------
/lib/shrine/plugins/default_url.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Shrine
4 | module Plugins
5 | # Documentation can be found on https://shrinerb.com/docs/plugins/default_url
6 | module DefaultUrl
7 | def self.configure(uploader, **opts)
8 | uploader.opts[:default_url] ||= {}
9 | uploader.opts[:default_url].merge!(opts)
10 | end
11 |
12 | module AttacherClassMethods
13 | def default_url(&block)
14 | shrine_class.opts[:default_url][:block] = block
15 | end
16 | end
17 |
18 | module AttacherMethods
19 | def url(**options)
20 | super || default_url(**options)
21 | end
22 |
23 | private
24 |
25 | def default_url(**options)
26 | return unless default_url_block
27 |
28 | url = instance_exec(**options, &default_url_block)
29 |
30 | [*default_url_host, url].join
31 | end
32 |
33 | def default_url_block
34 | shrine_class.opts[:default_url][:block]
35 | end
36 |
37 | def default_url_host
38 | shrine_class.opts[:default_url][:host]
39 | end
40 | end
41 | end
42 |
43 | register_plugin(:default_url, DefaultUrl)
44 | end
45 | end
46 |
--------------------------------------------------------------------------------
/lib/shrine/plugins/default_url_options.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | Shrine.deprecation("The default_url_options plugin has been renamed to url_options, so `plugin :default_url_options` should be replaced with `plugin :url_options`. The default_url_options alias will be removed in Shrine 4.")
4 |
5 | require "shrine/plugins/url_options"
6 |
7 | Shrine::Plugins.register_plugin(:default_url_options, Shrine::Plugins::UrlOptions)
8 |
--------------------------------------------------------------------------------
/lib/shrine/plugins/delete_raw.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | Shrine.deprecation("The delete_raw plugin is deprecated and will be removed in Shrine 4. If you were using it with versions plugin, use the new derivatives plugin instead.")
4 |
5 | class Shrine
6 | module Plugins
7 | # Documentation can be found on https://shrinerb.com/docs/plugins/delete_raw
8 | module DeleteRaw
9 | def self.configure(uploader, **opts)
10 | uploader.opts[:delete_raw] ||= {}
11 | uploader.opts[:delete_raw].merge!(opts)
12 | end
13 |
14 | module InstanceMethods
15 | private
16 |
17 | # Deletes the file that was uploaded, unless it's an UploadedFile.
18 | def _upload(io, delete: delete_raw?, **options)
19 | super(io, delete: delete, **options)
20 | end
21 |
22 | def delete_raw?
23 | opts[:delete_raw][:storages].nil? ||
24 | opts[:delete_raw][:storages].include?(storage_key)
25 | end
26 | end
27 | end
28 |
29 | register_plugin(:delete_raw, DeleteRaw)
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/lib/shrine/plugins/dynamic_storage.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Shrine
4 | module Plugins
5 | # Documentation can be found on https://shrinerb.com/docs/plugins/dynamic_storage
6 | module DynamicStorage
7 | def self.configure(uploader)
8 | uploader.opts[:dynamic_storage] ||= { resolvers: {} }
9 | end
10 |
11 | module ClassMethods
12 | def storage(regex, &block)
13 | opts[:dynamic_storage][:resolvers][regex] = block
14 | end
15 |
16 | def find_storage(name)
17 | resolve_dynamic_storage(name) or super
18 | end
19 |
20 | private
21 |
22 | def resolve_dynamic_storage(name)
23 | opts[:dynamic_storage][:resolvers].each do |regex, block|
24 | if match = name.to_s.match(regex)
25 | return block.call(match)
26 | end
27 | end
28 | nil
29 | end
30 | end
31 | end
32 |
33 | register_plugin(:dynamic_storage, DynamicStorage)
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/lib/shrine/plugins/included.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Shrine
4 | module Plugins
5 | # Documentation can be found on https://shrinerb.com/docs/plugins/included
6 | module Included
7 | def self.configure(uploader, &block)
8 | uploader.opts[:included] ||= {}
9 | uploader.opts[:included][:block] = block
10 | end
11 |
12 | module AttachmentMethods
13 | def included(klass)
14 | super
15 |
16 | klass.instance_exec(@name, &shrine_class.opts[:included][:block])
17 | end
18 | end
19 | end
20 |
21 | register_plugin(:included, Included)
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/lib/shrine/plugins/keep_files.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Shrine
4 | module Plugins
5 | # Documentation can be found on https://shrinerb.com/docs/plugins/keep_files
6 | module KeepFiles
7 | module AttacherMethods
8 | def destroy?
9 | false
10 | end
11 | end
12 | end
13 |
14 | register_plugin(:keep_files, KeepFiles)
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/lib/shrine/plugins/metadata_attributes.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Shrine
4 | module Plugins
5 | # Documentation can be found on https://shrinerb.com/docs/plugins/metadata_attributes
6 | module MetadataAttributes
7 | def self.load_dependencies(uploader, *)
8 | uploader.plugin :entity
9 | end
10 |
11 | def self.configure(uploader, **opts)
12 | uploader.opts[:metadata_attributes] ||= {}
13 | uploader.opts[:metadata_attributes].merge!(opts)
14 | end
15 |
16 | module AttacherClassMethods
17 | def metadata_attributes(mappings = nil)
18 | if mappings
19 | shrine_class.opts[:metadata_attributes].merge!(mappings)
20 | else
21 | shrine_class.opts[:metadata_attributes]
22 | end
23 | end
24 | end
25 |
26 | module AttacherMethods
27 | def column_values
28 | super.merge(metadata_attributes)
29 | end
30 |
31 | private
32 |
33 | def metadata_attributes
34 | values = {}
35 |
36 | self.class.metadata_attributes.each do |source, destination|
37 | metadata_attribute = destination.is_a?(Symbol) ? :"#{name}_#{destination}" : :"#{destination}"
38 |
39 | next unless record.respond_to?(metadata_attribute)
40 |
41 | values[metadata_attribute] = file && file.metadata[source.to_s]
42 | end
43 |
44 | values
45 | end
46 | end
47 | end
48 |
49 | register_plugin(:metadata_attributes, MetadataAttributes)
50 | end
51 | end
52 |
--------------------------------------------------------------------------------
/lib/shrine/plugins/module_include.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | Shrine.deprecation("The module_include plugin is deprecated and will be removed in Shrine 4. Override core classes directly instead.")
4 |
5 | class Shrine
6 | module Plugins
7 | # Documentation can be found on https://shrinerb.com/docs/plugins/module_include
8 | module ModuleInclude
9 | module ClassMethods
10 | def attachment_module(mod = nil, &block)
11 | module_include(self::Attachment, mod, &block)
12 | end
13 |
14 | def attacher_module(mod = nil, &block)
15 | module_include(self::Attacher, mod, &block)
16 | end
17 |
18 | def file_module(mod = nil, &block)
19 | module_include(self::UploadedFile, mod, &block)
20 | end
21 |
22 | private
23 |
24 | def module_include(klass, mod, &block)
25 | mod ||= Module.new(&block)
26 | klass.include(mod)
27 | end
28 | end
29 | end
30 |
31 | register_plugin(:module_include, ModuleInclude)
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/lib/shrine/plugins/multi_cache.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Shrine
4 | module Plugins
5 | # Documentation can be found on https://shrinerb.com/docs/plugins/multi_cache
6 | module MultiCache
7 | def self.configure(uploader, **opts)
8 | uploader.opts[:multi_cache] ||= {}
9 | uploader.opts[:multi_cache].merge!(opts)
10 | end
11 |
12 | module AttacherMethods
13 | def cached?(file = self.file)
14 | super || additional_cache.any? { |key| uploaded?(file, key) }
15 | end
16 |
17 | private
18 |
19 | def additional_cache
20 | Array(shrine_class.opts[:multi_cache][:additional_cache])
21 | end
22 | end
23 | end
24 |
25 | register_plugin(:multi_cache, MultiCache)
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/lib/shrine/plugins/pretty_location.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Shrine
4 | module Plugins
5 | # Documentation can be found on https://shrinerb.com/docs/plugins/pretty_location
6 | module PrettyLocation
7 | def self.configure(uploader, **opts)
8 | uploader.opts[:pretty_location] ||= { identifier: :id }
9 | uploader.opts[:pretty_location].merge!(opts)
10 | end
11 |
12 | module InstanceMethods
13 | def generate_location(io, **options)
14 | pretty_location(io, **options)
15 | end
16 |
17 | def pretty_location(io, name: nil, record: nil, version: nil, derivative: nil, identifier: nil, metadata: {}, **)
18 | if record
19 | namespace = record_namespace(record)
20 | identifier ||= record_identifier(record)
21 | end
22 |
23 | basename = basic_location(io, metadata: metadata)
24 | basename = [*(version || derivative), basename].join("-")
25 |
26 | [*namespace, *identifier, *name, basename].join("/")
27 | end
28 |
29 | private
30 |
31 | def record_identifier(record)
32 | record.public_send(opts[:pretty_location][:identifier])
33 | end
34 |
35 | def transform_class_name(class_name)
36 | if opts[:pretty_location][:class_underscore]
37 | class_name.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2').gsub(/([a-z])([A-Z])/, '\1_\2').downcase
38 | else
39 | class_name.downcase
40 | end
41 | end
42 |
43 | def record_namespace(record)
44 | class_name = record.class.name or return
45 | parts = transform_class_name(class_name).split("::")
46 |
47 | if separator = opts[:pretty_location][:namespace]
48 | parts.join(separator)
49 | else
50 | parts.last
51 | end
52 | end
53 | end
54 | end
55 |
56 | register_plugin(:pretty_location, PrettyLocation)
57 | end
58 | end
59 |
--------------------------------------------------------------------------------
/lib/shrine/plugins/processing.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | Shrine.deprecation("The processing plugin is deprecated and will be removed in Shrine 4. If you were using it with versions plugin, use the new derivatives plugin instead.")
4 |
5 | class Shrine
6 | module Plugins
7 | # Documentation can be found on https://shrinerb.com/docs/plugins/processing
8 | module Processing
9 | def self.configure(uploader)
10 | uploader.opts[:processing] ||= {}
11 | end
12 |
13 | module ClassMethods
14 | def process(action, &block)
15 | opts[:processing][action] ||= []
16 | opts[:processing][action] << block
17 | end
18 | end
19 |
20 | module InstanceMethods
21 | def upload(io, process: true, **options)
22 | if process
23 | input = process(io, **options)
24 | else
25 | input = io
26 | end
27 |
28 | super(input, **options)
29 | end
30 |
31 | private
32 |
33 | def process(io, **options)
34 | pipeline = processing_pipeline(options[:action])
35 | pipeline.inject(io) do |input, processor|
36 | instance_exec(input, **options, &processor) || input
37 | end
38 | end
39 |
40 | def processing_pipeline(key)
41 | opts[:processing][key] || []
42 | end
43 | end
44 | end
45 |
46 | register_plugin(:processing, Processing)
47 | end
48 | end
49 |
--------------------------------------------------------------------------------
/lib/shrine/plugins/rack_file.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "forwardable"
4 |
5 | class Shrine
6 | module Plugins
7 | # Documentation can be found on https://shrinerb.com/docs/plugins/rack_file
8 | module RackFile
9 | module ClassMethods
10 | # Accepts a Rack uploaded file hash and wraps it in an IO object.
11 | def rack_file(hash)
12 | if hash[:filename]
13 | # Rack can sometimes return the filename binary encoded, so we force
14 | # the encoding to utf-8
15 | hash = hash.merge(
16 | filename: hash[:filename].dup.force_encoding(Encoding::UTF_8)
17 | )
18 | end
19 |
20 | Shrine::RackFile.new(hash)
21 | end
22 | end
23 |
24 | module AttacherMethods
25 | # Checks whether a file is a Rack file hash, and in that case wraps the
26 | # hash in an IO-like object.
27 | def assign(value, **options)
28 | if rack_file?(value)
29 | assign shrine_class.rack_file(value), **options
30 | else
31 | super
32 | end
33 | end
34 |
35 | private
36 |
37 | # Returns whether a given value is a Rack uploaded file hash, by
38 | # checking whether it's a hash with `:tempfile` and `:name` keys.
39 | def rack_file?(value)
40 | value.is_a?(Hash) && value.key?(:tempfile) && value.key?(:name)
41 | end
42 | end
43 |
44 | end
45 |
46 | register_plugin(:rack_file, RackFile)
47 | end
48 |
49 | # This is used to wrap the Rack hash into an IO-like object which Shrine
50 | # can upload.
51 | class RackFile
52 | attr_reader :tempfile, :original_filename, :content_type
53 | alias :to_io :tempfile
54 |
55 | def initialize(hash)
56 | @tempfile = hash[:tempfile]
57 | @original_filename = hash[:filename]
58 | @content_type = hash[:type]
59 | end
60 |
61 | def path
62 | @tempfile.path
63 | end
64 |
65 | extend Forwardable
66 | delegate [:read, :size, :rewind, :eof?, :close] => :@tempfile
67 | end
68 | end
69 |
--------------------------------------------------------------------------------
/lib/shrine/plugins/recache.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | Shrine.deprecation("The recache plugin is deprecated and will be removed in Shrine 4. If you were using it with versions plugin, use the new derivatives plugin instead.")
4 |
5 | class Shrine
6 | module Plugins
7 | # Documentation can be found on https://shrinerb.com/docs/plugins/recache
8 | module Recache
9 | module AttacherMethods
10 | def save
11 | recache
12 | super
13 | end
14 |
15 | def recache
16 | if cached?
17 | result = upload(file, cache_key, action: :recache)
18 |
19 | set(result)
20 | end
21 | end
22 | end
23 | end
24 |
25 | register_plugin(:recache, Recache)
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/lib/shrine/plugins/refresh_metadata.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Shrine
4 | module Plugins
5 | # Documentation can be found on https://shrinerb.com/docs/plugins/refresh_metadata
6 | module RefreshMetadata
7 | module AttacherMethods
8 | def refresh_metadata!(**options)
9 | file!.refresh_metadata!(**context, **options)
10 | set(file) # trigger model write
11 | end
12 | end
13 |
14 | module FileMethods
15 | def refresh_metadata!(**options)
16 | return open { refresh_metadata!(**options) } unless opened?
17 |
18 | refreshed_metadata = uploader.send(:get_metadata, self, metadata: true, **options)
19 |
20 | @metadata = @metadata.merge(refreshed_metadata)
21 | end
22 | end
23 | end
24 |
25 | register_plugin(:refresh_metadata, RefreshMetadata)
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/lib/shrine/plugins/remove_attachment.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Shrine
4 | module Plugins
5 | # Documentation can be found on https://shrinerb.com/docs/plugins/remove_attachment
6 | module RemoveAttachment
7 | module AttachmentMethods
8 | def define_model_methods(name)
9 | super if defined?(super)
10 |
11 | define_method :"remove_#{name}=" do |value|
12 | send(:"#{name}_attacher").remove = value
13 | end
14 |
15 | define_method :"remove_#{name}" do
16 | send(:"#{name}_attacher").remove
17 | end
18 | end
19 | end
20 |
21 | module AttacherMethods
22 | # We remove the attachment if the value evaluates to true.
23 | def remove=(value)
24 | @remove = value
25 |
26 | change(nil) if remove?
27 | end
28 |
29 | def remove
30 | @remove
31 | end
32 |
33 | private
34 |
35 | # Don't override previously removed attachment that wasn't yet deleted.
36 | def change?(file)
37 | super && !(changed? && remove?)
38 | end
39 |
40 | # Rails sends "0" or "false" if the checkbox hasn't been ticked.
41 | def remove?
42 | return remove if [true, false].include?(remove)
43 |
44 | remove && remove != "" && remove !~ /\A(0|false)\z/
45 | end
46 | end
47 | end
48 |
49 | register_plugin(:remove_attachment, RemoveAttachment)
50 | end
51 | end
52 |
--------------------------------------------------------------------------------
/lib/shrine/plugins/remove_invalid.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Shrine
4 | module Plugins
5 | # Documentation can be found on https://shrinerb.com/docs/plugins/remove_invalid
6 | module RemoveInvalid
7 | def self.load_dependencies(uploader)
8 | uploader.plugin :validation
9 | end
10 |
11 | module AttacherMethods
12 | def validate(*)
13 | super
14 | ensure
15 | deassign if errors.any?
16 | end
17 |
18 | private
19 |
20 | def deassign
21 | destroy
22 |
23 | if changed?
24 | load_data @previous.data
25 | @previous = nil
26 | else
27 | load_data nil
28 | end
29 | end
30 | end
31 | end
32 |
33 | register_plugin(:remove_invalid, RemoveInvalid)
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/lib/shrine/plugins/restore_cached_data.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Shrine
4 | module Plugins
5 | # Documentation can be found on https://shrinerb.com/docs/plugins/restore_cached_data
6 | module RestoreCachedData
7 | def self.load_dependencies(uploader)
8 | uploader.plugin :refresh_metadata
9 | end
10 |
11 | module AttacherMethods
12 | private
13 |
14 | def cached(value, **options)
15 | cached_file = super
16 |
17 | # TODO: Remove this conditional when we remove the versions plugin
18 | if cached_file.is_a?(Hash) || cached_file.is_a?(Array)
19 | uploaded_file(cached_file) { |file| file.refresh_metadata!(**context, **options) }
20 | else
21 | cached_file.refresh_metadata!(**context, **options)
22 | end
23 |
24 | cached_file
25 | end
26 | end
27 | end
28 |
29 | register_plugin(:restore_cached_data, RestoreCachedData)
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/lib/shrine/plugins/tempfile.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Shrine
4 | module Plugins
5 | # Documentation can be found on https://shrinerb.com/docs/plugins/tempfile
6 | module Tempfile
7 | module ClassMethods
8 | def with_file(io)
9 | if io.is_a?(UploadedFile) && io.opened?
10 | # open a new file descriptor for thread safety
11 | File.open(io.tempfile.path, binmode: true) do |file|
12 | yield file
13 | end
14 | else
15 | super
16 | end
17 | end
18 | end
19 |
20 | module FileMethods
21 | def tempfile
22 | raise Error, "uploaded file must be opened" unless @io
23 |
24 | @tempfile ||= download
25 | @tempfile.rewind
26 | @tempfile
27 | end
28 |
29 | def close
30 | super
31 |
32 | @tempfile.close! if @tempfile
33 | @tempfile = nil
34 | end
35 | end
36 | end
37 |
38 | register_plugin(:tempfile, Tempfile)
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/lib/shrine/plugins/upload_options.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Shrine
4 | module Plugins
5 | # Documentation can be found on https://shrinerb.com/docs/plugins/upload_options
6 | module UploadOptions
7 | def self.configure(uploader, **opts)
8 | uploader.opts[:upload_options] ||= {}
9 | uploader.opts[:upload_options].merge!(opts)
10 | end
11 |
12 | module InstanceMethods
13 | private
14 |
15 | def _upload(io, **options)
16 | upload_options = get_upload_options(io, options)
17 |
18 | super(io, **options, upload_options: upload_options)
19 | end
20 |
21 | def get_upload_options(io, options)
22 | upload_options = opts[:upload_options][storage_key] || {}
23 | upload_options = upload_options.call(io, options) if upload_options.respond_to?(:call)
24 | upload_options = upload_options.merge(options[:upload_options]) if options[:upload_options]
25 | upload_options
26 | end
27 | end
28 | end
29 |
30 | register_plugin(:upload_options, UploadOptions)
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/lib/shrine/plugins/url_options.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Shrine
4 | module Plugins
5 | # Documentation can be found on https://shrinerb.com/docs/plugins/url_options
6 | module UrlOptions
7 | def self.configure(uploader, **opts)
8 | uploader.opts[:url_options] ||= {}
9 | uploader.opts[:url_options].merge!(opts)
10 | end
11 |
12 | module FileMethods
13 | def url(**options)
14 | default_options = url_options(options)
15 |
16 | super(**default_options, **options)
17 | end
18 |
19 | private
20 |
21 | def url_options(options)
22 | default_options = shrine_class.opts[:url_options][storage_key]
23 | default_options = default_options.call(self, options) if default_options.respond_to?(:call)
24 | default_options || {}
25 | end
26 | end
27 | end
28 |
29 | register_plugin(:url_options, UrlOptions)
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/lib/shrine/plugins/validation.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Shrine
4 | module Plugins
5 | # Documentation can be found on https://shrinerb.com/docs/plugins/validation
6 | module Validation
7 | module AttacherClassMethods
8 | # Block that is executed in context of Shrine::Attacher during
9 | # validation. Example:
10 | #
11 | # Shrine::Attacher.validate do
12 | # if file.size > 5*1024*1024
13 | # errors << "is too big (max is 5 MB)"
14 | # end
15 | # end
16 | def validate(&block)
17 | private define_method(:validate_block, &block)
18 | end
19 | end
20 |
21 | module AttacherMethods
22 | # Returns an array of validation errors created on file assignment in
23 | # the `Attacher.validate` block.
24 | attr_reader :errors
25 |
26 | # Initializes validation errors to an empty array.
27 | def initialize(**options)
28 | super
29 | @errors = []
30 | end
31 |
32 | # Performs validations after attaching cached file.
33 | def attach_cached(value, validate: nil, **options)
34 | result = super(value, validate: false, **options)
35 | validation(validate)
36 | result
37 | end
38 |
39 | # Performs validations after attaching file.
40 | def attach(io, validate: nil, **options)
41 | result = super(io, **options)
42 | validation(validate)
43 | result
44 | end
45 |
46 | # Runs the validation defined by `Attacher.validate`.
47 | def validate(**options)
48 | errors.clear
49 | _validate(**options) if attached?
50 | end
51 |
52 | private
53 |
54 | # Calls validation appropriately based on the :validate value.
55 | def validation(argument)
56 | case argument
57 | when Hash then validate(**argument)
58 | when false then errors.clear # skip validation
59 | else validate
60 | end
61 | end
62 |
63 | # Calls #validate_block, passing it accepted parameters.
64 | def _validate(**options)
65 | if method(:validate_block).arity.zero?
66 | validate_block
67 | else
68 | validate_block(**options)
69 | end
70 | end
71 |
72 | # Overridden by the `Attacher.validate` block.
73 | def validate_block(**options)
74 | end
75 | end
76 | end
77 |
78 | register_plugin(:validation, Validation)
79 | end
80 | end
81 |
--------------------------------------------------------------------------------
/lib/shrine/storage/memory.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "shrine"
4 | require "stringio"
5 |
6 | class Shrine
7 | module Storage
8 | class Memory
9 | attr_reader :store
10 |
11 | def initialize(store = {})
12 | @store = store
13 | end
14 |
15 | def upload(io, id, **)
16 | store[id] = io.read
17 | end
18 |
19 | def open(id, **)
20 | io = StringIO.new(store.fetch(id))
21 | io.set_encoding(io.string.encoding) # Ruby 2.7.0 – https://bugs.ruby-lang.org/issues/16497
22 | io
23 | rescue KeyError
24 | raise Shrine::FileNotFound, "file #{id.inspect} not found on storage"
25 | end
26 |
27 | def exists?(id)
28 | store.key?(id)
29 | end
30 |
31 | def url(id, *)
32 | "memory://#{id}"
33 | end
34 |
35 | def delete(id)
36 | store.delete(id)
37 | end
38 |
39 | def delete_prefixed(delete_prefix)
40 | delete_prefix = delete_prefix.chomp("/") + "/"
41 | store.delete_if { |key, _value| key.start_with?(delete_prefix) }
42 | end
43 |
44 | def clear!
45 | store.clear
46 | end
47 | end
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/lib/shrine/version.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Shrine
4 | def self.version
5 | Gem::Version.new VERSION::STRING
6 | end
7 |
8 | module VERSION
9 | MAJOR = 3
10 | MINOR = 6
11 | TINY = 0
12 | PRE = nil
13 |
14 | STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/test/attachment_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | describe Shrine::Attachment do
4 | before do
5 | @shrine = shrine
6 | end
7 |
8 | describe ".[]" do
9 | it "calls .new" do
10 | attachment = @shrine::Attachment[:file, foo: "bar"]
11 |
12 | assert_instance_of @shrine::Attachment, attachment
13 |
14 | assert_equal :file, attachment.attachment_name
15 | assert_equal Hash[foo: "bar"], attachment.options
16 | end
17 | end
18 |
19 | describe "#initialize" do
20 | it "symbolizes attachment name" do
21 | attachment = @shrine::Attachment.new("file")
22 |
23 | assert_equal :file, attachment.attachment_name
24 | end
25 |
26 | it "accepts additional options" do
27 | attachment = @shrine::Attachment.new(:file, foo: "bar")
28 |
29 | assert_equal Hash[foo: "bar"], attachment.options
30 | end
31 | end
32 |
33 | describe "#inspect" do
34 | it "is simplified" do
35 | attachment = @shrine::Attachment.new(:file)
36 |
37 | assert_match "Attachment(file)", attachment.inspect
38 | end
39 | end
40 |
41 | describe "#to_s" do
42 | it "is simplified" do
43 | attachment = @shrine::Attachment.new(:file)
44 |
45 | assert_match "Attachment(file)", attachment.to_s
46 | end
47 | end
48 | end
49 |
--------------------------------------------------------------------------------
/test/fixtures/image.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shrinerb/shrine/1ce6da4be0ca62ecd5776f107fcd0d657b773f28/test/fixtures/image.jpg
--------------------------------------------------------------------------------
/test/integration/sequel_backgrounding_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 | require "./test/support/sequel"
3 |
4 | describe "Sequel Backgrounding" do
5 | before do
6 | @shrine = shrine do
7 | plugin :sequel
8 | plugin :backgrounding
9 | end
10 |
11 | user_class = Class.new(Sequel::Model)
12 | user_class.set_dataset(:users)
13 | user_class.include @shrine::Attachment.new(:avatar)
14 |
15 | @user = user_class.new
16 | end
17 |
18 | specify "promotion" do
19 | # promote immediately to test that the record is saved before background
20 | # job is kicked off
21 | @user.avatar_attacher.promote_block do |attacher|
22 | record = attacher.record.class.with_pk!(attacher.record.id)
23 |
24 | attacher.class
25 | .retrieve(model: record, name: attacher.name, file: attacher.file_data)
26 | .atomic_promote
27 | end
28 |
29 | @user.avatar = fakeio
30 | @user.save
31 |
32 | assert_equal :cache, @user.avatar.storage_key
33 |
34 | @user.reload
35 |
36 | assert_equal :store, @user.avatar.storage_key
37 | end
38 |
39 | specify "replacing" do
40 | @user.avatar_attacher.destroy_block do |attacher|
41 | @job = Fiber.new do
42 | attacher = attacher.class.from_data(attacher.data)
43 | attacher.destroy
44 | end
45 | end
46 |
47 | @user.avatar = fakeio
48 | @user.save
49 |
50 | previous_file = @user.avatar
51 |
52 | @user.update(avatar: fakeio)
53 |
54 | assert previous_file.exists?
55 |
56 | @job.resume
57 |
58 | refute previous_file.exists?
59 | end
60 |
61 | specify "destroying" do
62 | @user.avatar_attacher.destroy_block do |attacher|
63 | @job = Fiber.new do
64 | attacher = attacher.class.from_data(attacher.data)
65 | attacher.destroy
66 | end
67 | end
68 |
69 | @user.avatar = fakeio
70 | @user.save
71 | @user.destroy
72 |
73 | assert @user.avatar.exists?
74 |
75 | @job.resume
76 |
77 | refute @user.avatar.exists?
78 | end
79 |
80 | specify "destroying during promotion" do
81 | @user.avatar_attacher.promote_block do |attacher|
82 | @job = Fiber.new do
83 | record = attacher.record.class.with_pk!(attacher.record.id)
84 |
85 | attacher.class
86 | .retrieve(model: record, name: attacher.name, file: attacher.file_data)
87 | .atomic_promote
88 | end
89 | end
90 |
91 | @user.avatar = fakeio
92 | @user.save
93 |
94 | @user.destroy # doesn't delete cached file
95 |
96 | assert_raises Sequel::NoMatchingRow do
97 | @job.resume
98 | end
99 | end
100 | end
101 |
--------------------------------------------------------------------------------
/test/plugin/cached_attachment_data_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 | require "shrine/plugins/cached_attachment_data"
3 |
4 | describe Shrine::Plugins::CachedAttachmentData do
5 | before do
6 | @attacher = attacher { plugin :cached_attachment_data }
7 | @shrine = @attacher.shrine_class
8 | end
9 |
10 | describe "Attachment" do
11 | before do
12 | @model_class = model_class(:file_data)
13 | end
14 |
15 | describe "#cached__data" do
16 | it "returns cached file data" do
17 | @shrine.plugin :model
18 | @model_class.include @shrine::Attachment.new(:file)
19 |
20 | model = @model_class.new
21 | model.file = fakeio
22 |
23 | assert_equal model.file.to_json, model.cached_file_data
24 | end
25 |
26 | it "is not defined for entity attachments" do
27 | @shrine.plugin :model
28 | @model_class.include @shrine::Attachment.new(:file, model: false)
29 |
30 | refute @model_class.method_defined?(:cached_file_data)
31 | end
32 | end
33 | end
34 |
35 | describe "Attacher" do
36 | describe "#cached_data" do
37 | it "returns data of cached changed file" do
38 | @attacher.attach_cached(fakeio)
39 | assert_equal @attacher.file.to_json, @attacher.cached_data
40 | end
41 |
42 | it "returns nil when changed file is not cached" do
43 | @attacher.attach(fakeio)
44 | assert_nil @attacher.cached_data
45 | end
46 |
47 | it "returns nil when cached file is not changed" do
48 | @attacher.set @shrine.upload(fakeio, :cache)
49 | assert_nil @attacher.cached_data
50 | end
51 |
52 | it "returns nil when no file is attached" do
53 | assert_nil @attacher.cached_data
54 | end
55 | end
56 | end
57 | end
58 |
--------------------------------------------------------------------------------
/test/plugin/default_url_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 | require "shrine/plugins/default_url"
3 |
4 | describe Shrine::Plugins::DefaultUrl do
5 | before do
6 | @attacher = attacher { plugin :default_url }
7 | @shrine = @attacher.shrine_class
8 | end
9 |
10 | describe "Attacher" do
11 | describe "#url" do
12 | it "returns block value when attachment is missing" do
13 | @attacher.class.default_url { "default_url" }
14 |
15 | assert_equal "default_url", @attacher.url
16 | end
17 |
18 | it "returns nil when no block is given and attachment is missing" do
19 | assert_nil @attacher.url
20 | end
21 |
22 | it "returns attachment URL if attachment is present" do
23 | @attacher.class.default_url { "default_url" }
24 |
25 | @attacher.attach(fakeio)
26 |
27 | assert_equal @attacher.file.url, @attacher.url
28 | end
29 |
30 | it "evaluates the block in context of the attacher instance" do
31 | @attacher.class.default_url { to_s }
32 |
33 | assert_equal @attacher.to_s, @attacher.url
34 | end
35 |
36 | it "yields the given URL options to the block" do
37 | @attacher.class.default_url { |**options| options.to_json }
38 |
39 | assert_equal '{"foo":"bar"}', @attacher.url(foo: "bar")
40 | end
41 |
42 | it "accepts :host" do
43 | @shrine.plugin :default_url, host: "https://example.com"
44 | @attacher.class.default_url { "/bar/baz" }
45 | assert_equal "https://example.com/bar/baz", @attacher.url
46 |
47 | @shrine.plugin :default_url, host: "https://example.com/foo"
48 | assert_equal "https://example.com/foo/bar/baz", @attacher.url
49 | end
50 |
51 | it "doesn't override previously set default URL if no block is given" do
52 | @attacher.class.default_url { "default_url" }
53 | @shrine.plugin :default_url
54 |
55 | assert_equal "default_url", @attacher.url
56 | end
57 | end
58 | end
59 | end
60 |
--------------------------------------------------------------------------------
/test/plugin/delete_raw_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 | require "shrine/plugins/delete_raw"
3 |
4 | describe Shrine::Plugins::DeleteRaw do
5 | before do
6 | @uploader = uploader { plugin :delete_raw }
7 | end
8 |
9 | describe "Shrine" do
10 | describe "#upload" do
11 | it "deletes files after upload" do
12 | @uploader.upload(tempfile = Tempfile.new)
13 | refute File.exist?(tempfile.path)
14 | end
15 |
16 | it "deletes tempfiles after upload" do
17 | @uploader.upload(file = File.open(Tempfile.new.path))
18 | refute File.exist?(file.path)
19 | end
20 |
21 | it "deletes IOs that respond to #path after upload" do
22 | io = FakeIO.new("file")
23 | def io.path; (@tempfile ||= Tempfile.new).path; end
24 | @uploader.upload(io)
25 | refute File.exist?(io.path)
26 | end
27 |
28 | it "doesn't raise an error if file is already deleted" do
29 | tempfile = Tempfile.new
30 | File.delete(tempfile.path)
31 | @uploader.upload(tempfile)
32 | end
33 |
34 | it "doesn't attempt to delete non-files" do
35 | @uploader.upload(fakeio)
36 | end
37 |
38 | it "doesn't attempt to delete UploadedFiles" do
39 | uploaded_file = @uploader.upload(fakeio)
40 | @uploader.upload(uploaded_file)
41 | assert uploaded_file.exists?
42 | end
43 |
44 | it "accepts specifying storages" do
45 | @uploader.class.plugin :delete_raw, storages: [:store]
46 | @uploader.class.new(:cache).upload(tempfile = Tempfile.new)
47 | assert File.exist?(tempfile.path)
48 | @uploader.class.new(:store).upload(tempfile = Tempfile.new)
49 | refute File.exist?(tempfile.path)
50 | end
51 |
52 | it "accepts :delete for skipping deletion" do
53 | file = Tempfile.new
54 | @uploader.upload(file, delete: false)
55 | assert File.exist?(file.path)
56 | end
57 | end
58 | end
59 | end
60 |
--------------------------------------------------------------------------------
/test/plugin/dynamic_storage_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 | require "shrine/plugins/dynamic_storage"
3 |
4 | describe Shrine::Plugins::DynamicStorage do
5 | before do
6 | @uploader = uploader { plugin :dynamic_storage }
7 | @shrine = @uploader.class
8 | end
9 |
10 | it "allows registering a storage with a regex" do
11 | @shrine.storage(/store_(\w+)/) { |match| match[1].to_sym }
12 |
13 | assert_equal :foo, @shrine.new(:store_foo).storage
14 | assert_equal :bar, @shrine.new(:store_bar).storage
15 | end
16 |
17 | it "allows saved storage resolvers to be inherited" do
18 | @shrine.storage(/store/) { |match| Shrine::Storage::Memory.new }
19 | subclass = Class.new(@shrine)
20 | refute_equal subclass.storages[:store], subclass.find_storage(:store)
21 | end
22 |
23 | it "doesn't clear registered storage resolvers when reapplying" do
24 | @shrine.storage(/store/) { |match| Shrine::Storage::Memory.new }
25 | @shrine.plugin :dynamic_storage
26 | refute_equal @shrine.storages[:store], @shrine.find_storage(:store)
27 | end
28 |
29 | it "delegates to default behaviour when storage wasn't found" do
30 | assert_instance_of Shrine::Storage::Memory, @uploader.class.new(:store).storage
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/test/plugin/included_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 | require "shrine/plugins/included"
3 |
4 | describe Shrine::Plugins::Included do
5 | it "enables extending the model attachment is being included to" do
6 | @uploader = uploader do
7 | plugin :included do |name|
8 | define_method("#{name}_foo") {}
9 | end
10 | end
11 |
12 | model = Class.new
13 | model.include @uploader.class::Attachment.new(:avatar)
14 | assert_respond_to model.new, :avatar_foo
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/test/plugin/keep_files_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 | require "shrine/plugins/keep_files"
3 |
4 | describe Shrine::Plugins::KeepFiles do
5 | before do
6 | @attacher = attacher { plugin :keep_files }
7 | @shrine = @attacher.shrine_class
8 | end
9 |
10 | describe "Attacher" do
11 | describe "#destroy_attached" do
12 | it "keeps files" do
13 | @attacher.attach(fakeio)
14 | @attacher.destroy_attached
15 |
16 | assert @attacher.file.exists?
17 | end
18 |
19 | it "doesn't spawn a background job" do
20 | @shrine.plugin :backgrounding
21 | @attacher.destroy_block { @called = true }
22 |
23 | @attacher.attach(fakeio)
24 | @attacher.destroy_attached
25 |
26 | assert @attacher.file.exists?
27 | refute @called
28 | end
29 | end
30 |
31 | describe "#destroy_previous" do
32 | it "keep files" do
33 | previous_file = @attacher.attach(fakeio)
34 | @attacher.attach(fakeio)
35 | @attacher.destroy_previous
36 |
37 | assert previous_file.exists?
38 | end
39 |
40 | it "doesn't spawn a background job" do
41 | @shrine.plugin :backgrounding
42 | @attacher.destroy_block { @called = true }
43 |
44 | previous_file = @attacher.attach(fakeio)
45 | @attacher.attach(fakeio)
46 | @attacher.destroy_previous
47 |
48 | assert previous_file.exists?
49 | refute @called
50 | end
51 | end
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/test/plugin/module_include_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 | require "shrine/plugins/module_include"
3 |
4 | describe Shrine::Plugins::ModuleInclude do
5 | before do
6 | @shrine = shrine { plugin :module_include }
7 | end
8 |
9 | describe "Shrine" do
10 | describe ".attachment_module" do
11 | it "includes module into Attachment" do
12 | @shrine.attachment_module { def one; end }
13 | @shrine.attachment_module Module.new { def two; end }
14 | assert_includes @shrine::Attachment.instance_methods, :one
15 | assert_includes @shrine::Attachment.instance_methods, :two
16 | end
17 | end
18 |
19 | describe ".attacher_module" do
20 | it "includes module into Attacher" do
21 | @shrine.attacher_module { def one; end }
22 | @shrine.attacher_module Module.new { def two; end }
23 | assert_includes @shrine::Attacher.instance_methods, :one
24 | assert_includes @shrine::Attacher.instance_methods, :two
25 | end
26 | end
27 |
28 | describe ".file_module" do
29 | it "includes module into UploadedFile" do
30 | @shrine.file_module { def one; end }
31 | @shrine.file_module Module.new { def two; end }
32 | assert_includes @shrine::UploadedFile.instance_methods, :one
33 | assert_includes @shrine::UploadedFile.instance_methods, :two
34 | end
35 | end
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/test/plugin/multi_cache_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 | require "shrine/plugins/multi_cache"
3 |
4 | describe Shrine::Plugins::MultiCache do
5 | before do
6 | @attacher = attacher { plugin :multi_cache, additional_cache: [:other_cache] }
7 | @shrine = @attacher.shrine_class
8 | end
9 |
10 | describe "Attacher" do
11 | describe "#attach_cached" do
12 | it "allows attaching files uploaded to additional cache" do
13 | file = @attacher.upload(fakeio, :other_cache)
14 |
15 | @attacher.attach_cached(file.data)
16 |
17 | assert_equal :other_cache, @attacher.file.storage_key
18 | assert @attacher.cached?
19 | end
20 |
21 | it "can still attach files uploaded to primary cache" do
22 | file = @attacher.upload(fakeio, :cache)
23 |
24 | @attacher.attach_cached(file.data)
25 |
26 | assert_equal :cache, @attacher.file.storage_key
27 | assert @attacher.cached?
28 | end
29 |
30 | it "still uploads files to primary cache" do
31 | @attacher.attach_cached(fakeio)
32 |
33 | assert_equal :cache, @attacher.file.storage_key
34 | assert @attacher.cached?
35 | end
36 | end
37 |
38 | describe "#promote_cached" do
39 | it "promotes files uploaded to additional cache" do
40 | file = @attacher.upload(fakeio, :other_cache)
41 |
42 | @attacher.attach_cached(file.data)
43 | @attacher.promote_cached
44 |
45 | assert_equal :store, @attacher.file.storage_key
46 | assert @attacher.stored?
47 | end
48 |
49 | it "promotes files uploaded to primary cache" do
50 | file = @attacher.upload(fakeio, :cache)
51 |
52 | @attacher.attach_cached(file.data)
53 | @attacher.promote_cached
54 |
55 | assert_equal :store, @attacher.file.storage_key
56 | assert @attacher.stored?
57 | end
58 | end
59 | end
60 | end
61 |
--------------------------------------------------------------------------------
/test/plugin/processing_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 | require "shrine/plugins/processing"
3 |
4 | describe Shrine::Plugins::Processing do
5 | before do
6 | @uploader = uploader { plugin :processing }
7 | @shrine = @uploader.class
8 | end
9 |
10 | describe "Shrine" do
11 | describe "#upload" do
12 | it "executes defined processing" do
13 | @shrine.process(:foo) { |io, **| FakeIO.new(io.read.reverse) }
14 | file = @uploader.upload(fakeio("file"), action: :foo)
15 | assert_equal "elif", file.read
16 | end
17 |
18 | it "executes in context of uploader, and passes right variables" do
19 | minitest = self
20 |
21 | @shrine.process(:foo) do |io, **options|
22 | minitest.assert_kind_of Shrine, self
23 | minitest.assert_respond_to io, :read
24 | minitest.assert_equal :foo, options[:action]
25 |
26 | FakeIO.new(io.read.reverse)
27 | end
28 |
29 | @uploader.upload(fakeio, action: :foo)
30 | end
31 |
32 | it "executes all defined blocks where output of previous is input to next" do
33 | @shrine.process(:foo) { |io, **| FakeIO.new("changed") }
34 | @shrine.process(:foo) { |io, **| FakeIO.new(io.read.reverse) }
35 |
36 | file = @uploader.upload(fakeio, action: :foo)
37 |
38 | assert_equal "degnahc", file.read
39 | end
40 |
41 | it "allows blocks to return nil" do
42 | @shrine.process(:foo) { |io, **| nil }
43 | @shrine.process(:foo) { |io, **| FakeIO.new(io.read.reverse) }
44 |
45 | file = @uploader.upload(fakeio("file"), action: :foo)
46 |
47 | assert_equal "elif", file.read
48 | end
49 |
50 | it "executes defined blocks only if actions match" do
51 | @shrine.process(:foo) { |io, **| FakeIO.new(io.read.reverse) }
52 |
53 | file = @uploader.upload(fakeio("file"))
54 |
55 | assert_equal "file", file.read
56 | end
57 |
58 | it "doesn't overwrite existing definitions when loading the plugin" do
59 | @shrine.process(:foo) { |io, **| FakeIO.new("processed") }
60 | @shrine.plugin :processing
61 |
62 | file = @uploader.upload(fakeio, action: :foo)
63 |
64 | assert_equal "processed", file.read
65 | end
66 |
67 | it "copies processing definitions on subclassing" do
68 | @shrine.process(:foo) { |io, **| FakeIO.new("#{io.read} once") }
69 |
70 | subclass = Class.new(@shrine)
71 | subclass.process(:foo) { |io, **| FakeIO.new("#{io.read} twice") }
72 |
73 | uploaded_by_parent = @uploader.upload(fakeio("file"), action: :foo)
74 | uploaded_by_subclass = subclass.upload(fakeio("file"), :store, action: :foo)
75 |
76 | refute_equal @shrine.opts[:processing], subclass.opts[:processing]
77 | assert_equal 1, @shrine.opts[:processing][:foo].size
78 | assert_equal 2, subclass.opts[:processing][:foo].size
79 |
80 | assert_equal "file once", uploaded_by_parent.read
81 | assert_equal "file once twice", uploaded_by_subclass.read
82 | end
83 | end
84 | end
85 | end
86 |
--------------------------------------------------------------------------------
/test/plugin/rack_file_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 | require "shrine/plugins/rack_file"
3 | require "tempfile"
4 |
5 | describe Shrine::Plugins::RackFile do
6 | before do
7 | @attacher = attacher { plugin :rack_file }
8 | @shrine = @attacher.shrine_class
9 | @rack_hash = {
10 | name: "file",
11 | tempfile: Tempfile.new,
12 | filename: "image.jpg",
13 | type: "image/jpeg",
14 | head: "...",
15 | }
16 | end
17 |
18 | it "allows converting Rack file hash into an IO object" do
19 | rack_file = @shrine.rack_file(@rack_hash)
20 |
21 | assert_equal "", rack_file.read
22 | assert_equal 0, rack_file.size
23 | assert_equal true, rack_file.eof?
24 |
25 | rack_file.rewind
26 | rack_file.close
27 | end
28 |
29 | it "exposes metadata on the IO object" do
30 | rack_file = @shrine.rack_file(@rack_hash)
31 |
32 | assert_equal 0, rack_file.size
33 | assert_equal "image.jpg", rack_file.original_filename
34 | assert_equal "image/jpeg", rack_file.content_type
35 | end
36 |
37 | it "adds methods for accessing the Tempfile" do
38 | rack_file = @shrine.rack_file(@rack_hash)
39 |
40 | assert_equal @rack_hash[:tempfile], rack_file.tempfile
41 | assert_equal @rack_hash[:tempfile], rack_file.to_io
42 | assert_equal @rack_hash[:tempfile].path, rack_file.path
43 | end
44 |
45 | it "accepts hash-like parameters" do
46 | hash_like_class = Class.new(Hash) do
47 | def initialize(hash)
48 | @hash = hash.inject({}) { |h, (key, value)| h.update(key.to_s => value) }
49 | end
50 |
51 | def key?(name)
52 | @hash.key?(name.to_s)
53 | end
54 |
55 | def [](name)
56 | @hash[name.to_s]
57 | end
58 | end
59 |
60 | rack_file = @shrine.rack_file(hash_like_class.new(@rack_hash))
61 |
62 | assert_equal 0, rack_file.size
63 | assert_equal "image.jpg", rack_file.original_filename
64 | assert_equal "image/jpeg", rack_file.content_type
65 | end
66 |
67 | it "converts filename from binary encoding to utf-8" do
68 | @rack_hash[:filename] = "über_pdf_with_1337%_leetness.pdf".b
69 |
70 | rack_file = @shrine.rack_file(@rack_hash)
71 |
72 | assert_equal Encoding::UTF_8, rack_file.original_filename.encoding
73 | assert_equal Encoding::BINARY, @rack_hash[:filename].encoding
74 | end
75 |
76 | it "supports assigning Rack files directly" do
77 | @attacher.assign(@rack_hash)
78 |
79 | assert_equal "", @attacher.get.read
80 | assert_equal 0, @attacher.get.size
81 | assert_equal "image.jpg", @attacher.get.original_filename
82 | assert_equal "image/jpeg", @attacher.get.mime_type
83 | end
84 |
85 | it "accepts assign options" do
86 | @attacher.assign(@rack_hash, metadata: { "foo" => "bar" })
87 | assert_equal "bar", @attacher.file.metadata["foo"]
88 |
89 | @attacher.assign(fakeio, metadata: { "foo" => "bar" })
90 | assert_equal "bar", @attacher.file.metadata["foo"]
91 | end
92 | end
93 |
--------------------------------------------------------------------------------
/test/plugin/recache_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 | require "shrine/plugins/recache"
3 |
4 | describe Shrine::Plugins::Recache do
5 | before do
6 | @attacher = attacher { plugin :recache }
7 | end
8 |
9 | it "recaches cached files" do
10 | file = @attacher.assign(fakeio("original"))
11 |
12 | @attacher.save
13 |
14 | assert_equal :cache, @attacher.file.storage_key
15 | refute_equal file, @attacher.file
16 | assert_equal "original", @attacher.file.read
17 | end
18 |
19 | it "doesn't recache if attachment is missing" do
20 | @attacher.save
21 | end
22 |
23 | it "recaches only cached files" do
24 | file = @attacher.attach(fakeio)
25 |
26 | @attacher.save
27 |
28 | assert_equal file, @attacher.file
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/test/plugin/restore_cached_data_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 | require "shrine/plugins/restore_cached_data"
3 |
4 | describe Shrine::Plugins::RestoreCachedData do
5 | before do
6 | @attacher = attacher { plugin :restore_cached_data }
7 | @shrine = @attacher.shrine_class
8 | end
9 |
10 | describe "Attacher" do
11 | describe "#assign" do
12 | it "doesn't reextract metadata of current cached file" do
13 | file = @attacher.attach_cached(fakeio, metadata: false)
14 |
15 | @shrine.any_instance.expects(:extract_metadata).never
16 |
17 | @attacher.assign({ id: file.id, storage: file.storage_key })
18 | end
19 |
20 | it "doesn't reextract metadata of current stored file" do
21 | file = @attacher.attach(fakeio, metadata: false)
22 |
23 | @shrine.any_instance.expects(:extract_metadata).never
24 |
25 | @attacher.assign({ id: file.id, storage: file.storage_key })
26 | end
27 | end
28 |
29 | describe "#attach_cached" do
30 | it "reextracts metadata of set cached files" do
31 | cached_file = @attacher.upload(fakeio("a" * 1024), :cache)
32 | cached_file.metadata["size"] = 5
33 |
34 | @attacher.attach_cached(cached_file.data)
35 |
36 | assert_equal 1024, @attacher.file.metadata["size"]
37 | end
38 |
39 | it "skips extracting if the file is not cached" do
40 | stored_file = @attacher.upload(fakeio, :store)
41 |
42 | @shrine.any_instance.expects(:extract_metadata).never
43 |
44 | assert_raises(Shrine::Error) do
45 | @attacher.attach_cached(stored_file.data)
46 | end
47 | end
48 |
49 | it "forwards options to uploader" do
50 | cached_file = @attacher.upload(fakeio, :cache)
51 |
52 | @shrine.plugin :add_metadata
53 | metadata_options = nil
54 | @shrine.add_metadata(:foo) { |io, options| metadata_options = options }
55 |
56 | @attacher.attach_cached(cached_file.data, foo: "bar")
57 |
58 | assert_equal "bar", metadata_options[:foo]
59 | end
60 |
61 | it "fowards attacher context to uploader" do
62 | @shrine.plugin :add_metadata
63 | metadata_options = nil
64 | @shrine.add_metadata(:foo) { |io, options| metadata_options = options }
65 |
66 | cached_file = @attacher.upload(fakeio, :cache)
67 | @attacher.context.merge!(foo: "bar")
68 | @attacher.attach_cached(cached_file.data)
69 |
70 | assert_equal "bar", metadata_options[:foo]
71 | end
72 |
73 | it "works with versions plugin" do
74 | @shrine.plugin :versions
75 | cached_file = @attacher.upload(fakeio("a" * 1024), :cache)
76 | cached_file.metadata["size"] = 5
77 |
78 | @attacher.attach_cached({ "version" => cached_file.data })
79 |
80 | assert_equal 1024, @attacher.file[:version].metadata["size"]
81 | end
82 | end
83 | end
84 | end
85 |
--------------------------------------------------------------------------------
/test/plugin/tempfile_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 | require "shrine/plugins/tempfile"
3 |
4 | describe Shrine::Plugins::Tempfile do
5 | before do
6 | @uploader = uploader { plugin :tempfile }
7 | @shrine = @uploader.class
8 | end
9 |
10 | it "downloads the content to tempfile on first call" do
11 | uploaded_file = @uploader.upload(fakeio("content"))
12 | uploaded_file.open do
13 | tempfile = uploaded_file.tempfile
14 | assert_instance_of Tempfile, tempfile
15 | assert_equal "content", tempfile.read
16 | end
17 | end
18 |
19 | it "returns the same tempfile on subsequent calls" do
20 | uploaded_file = @uploader.upload(fakeio("content"))
21 | uploaded_file.open do
22 | assert_equal uploaded_file.tempfile, uploaded_file.tempfile
23 | end
24 | end
25 |
26 | it "deletes the tempfile at the end of the open block" do
27 | uploaded_file = @uploader.upload(fakeio("content"))
28 | tempfile = uploaded_file.open { uploaded_file.tempfile }
29 | assert tempfile.closed?
30 | assert_nil tempfile.path
31 | end
32 |
33 | it "deletes the tempfile when uploaded file is closed explicitly" do
34 | uploaded_file = @uploader.upload(fakeio("content"))
35 | uploaded_file.open
36 | tempfile = uploaded_file.tempfile
37 | uploaded_file.close
38 | assert tempfile.closed?
39 | assert_nil tempfile.path
40 | end
41 |
42 | it "rewinds the tempfile" do
43 | uploaded_file = @uploader.upload(fakeio("content"))
44 | uploaded_file.open do
45 | uploaded_file.tempfile.read
46 | assert_equal 0, uploaded_file.tempfile.pos
47 | end
48 | end
49 |
50 | it "raises an error when uploaded file is not open" do
51 | uploaded_file = @uploader.upload(fakeio("content"))
52 | assert_raises(Shrine::Error) { uploaded_file.tempfile }
53 | end
54 |
55 | it "allows caching the tempfile again" do
56 | uploaded_file = @uploader.upload(fakeio("content"))
57 | uploaded_file.open do
58 | assert_equal "content", uploaded_file.tempfile.read
59 | end
60 | uploaded_file.open do
61 | assert_equal "content", uploaded_file.tempfile.read
62 | end
63 | end
64 |
65 | describe "Shrine.with_file" do
66 | it "yields an open tempfile reference" do
67 | uploaded_file = @uploader.upload(fakeio("content"))
68 | uploaded_file.open do
69 | file = @shrine.with_file(uploaded_file) do |file|
70 | assert_equal uploaded_file.tempfile.path, file.path
71 | refute_equal uploaded_file.tempfile.fileno, file.fileno
72 | assert file.binmode?
73 | refute file.closed?
74 | file
75 | end
76 | assert file.closed?
77 | end
78 |
79 | # calls #download when uploaded file is not opened
80 | path1, path2 = nil
81 | @shrine.with_file(uploaded_file) { |file| path1 = file.path }
82 | @shrine.with_file(uploaded_file) { |file| path2 = file.path }
83 | refute_equal path1, path2
84 | end
85 |
86 | it "works with non-opened uploaded files" do
87 | uploaded_file = @uploader.upload(fakeio("content"))
88 | @shrine.with_file(uploaded_file) do |file|
89 | File.exist?(file.path)
90 | assert_equal "content", file.read
91 | end
92 | end
93 | end
94 | end
95 |
--------------------------------------------------------------------------------
/test/plugin/upload_options_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 | require "shrine/plugins/upload_options"
3 |
4 | describe Shrine::Plugins::UploadOptions do
5 | before do
6 | @uploader = uploader(:store) { plugin :upload_options }
7 | @shrine = @uploader.class
8 | end
9 |
10 | it "accepts a block" do
11 | @shrine.plugin :upload_options, store: -> (io, _) { Hash[foo: "foo"] }
12 | @uploader.storage.expects(:upload).with { |*, o| o[:foo] == "foo" }
13 | @uploader.upload(fakeio)
14 | end
15 |
16 | it "accepts a hash" do
17 | @shrine.plugin :upload_options, store: { foo: "foo" }
18 | @uploader.storage.expects(:upload).with { |*, o| o[:foo] == "foo" }
19 | @uploader.upload(fakeio)
20 | end
21 |
22 | it "only passes upload options to specified storages" do
23 | @shrine.plugin :upload_options, cache: { foo: "foo" }
24 | @uploader.storage.expects(:upload).with { |*, o| !o.key?(:foo) }
25 | @uploader.upload(fakeio)
26 | end
27 |
28 | it "takes lower precedence than :upload_options" do
29 | @shrine.plugin :upload_options, cache: { foo: "foo" }
30 | @uploader.storage.expects(:upload).with { |*, o| o[:foo] == "bar" }
31 | @uploader.upload(fakeio, upload_options: { foo: "bar" })
32 | end
33 |
34 | it "doesn't overwrite existing options when loading the plugin" do
35 | @shrine.plugin :upload_options, cache: { foo: "foo" }
36 | @shrine.plugin :upload_options, store: { bar: "bar" }
37 | assert_equal Hash[cache: { foo: "foo" }, store: { bar: "bar" }], @shrine.opts[:upload_options]
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/test/plugin/url_options_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 | require "shrine/plugins/url_options"
3 |
4 | describe Shrine::Plugins::UrlOptions do
5 | before do
6 | @uploader = uploader { plugin :url_options }
7 | @shrine = @uploader.class
8 | end
9 |
10 | describe "UploadedFile" do
11 | describe "#url" do
12 | it "adds default options statically" do
13 | @shrine.plugin :url_options, store: { foo: "foo" }
14 |
15 | file = @uploader.upload(fakeio)
16 | file.storage.expects(:url).with(file.id, { foo: "foo" })
17 | file.url
18 | end
19 |
20 | it "adds default options dynamically" do
21 | minitest = self
22 |
23 | @uploader.class.plugin :url_options, store: -> (io, options) do
24 | minitest.assert_kind_of Shrine::UploadedFile, io
25 | minitest.assert_equal "bar", options[:bar]
26 |
27 | { foo: "foo" }
28 | end
29 |
30 | file = @uploader.upload(fakeio)
31 | file.storage.expects(:url).with(file.id, { foo: "foo", bar: "bar" })
32 | file.url(bar: "bar")
33 | end
34 |
35 | it "merges default options with direct options" do
36 | @shrine.plugin :url_options, store: { foo: "foo" }
37 |
38 | file = @uploader.upload(fakeio)
39 | file.storage.expects(:url).with(file.id, { foo: "foo", bar: "bar" })
40 | file.url(bar: "bar")
41 | end
42 |
43 | it "allows direct options to override default options" do
44 | @shrine.plugin :url_options, store: { foo: "foo" }
45 |
46 | file = @uploader.upload(fakeio)
47 | file.storage.expects(:url).with(file.id, { foo: "overriden" })
48 | file.url(foo: "overriden")
49 | end
50 |
51 | it "handles nil values" do
52 | @shrine.plugin :url_options, store: nil
53 |
54 | file = @uploader.upload(fakeio)
55 | file.storage.expects(:url).with(file.id, { foo: "foo" })
56 | file.url(foo: "foo")
57 |
58 | @shrine.plugin :url_options, store: -> (io, options) {}
59 |
60 | file = @uploader.upload(fakeio)
61 | file.storage.expects(:url).with(file.id, { foo: "foo" })
62 | file.url(foo: "foo")
63 | end
64 |
65 | it "allows overriding passed options" do
66 | @shrine.plugin :url_options, store: -> (io, options) {
67 | { foo: "#{options.delete(:foo)} bar" }
68 | }
69 |
70 | file = @uploader.upload(fakeio)
71 | file.storage.expects(:url).with(file.id, { foo: "foo bar" })
72 | file.url(foo: "foo")
73 | end
74 | end
75 | end
76 | end
77 |
--------------------------------------------------------------------------------
/test/plugin_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | describe "plugin system" do
4 | describe "Shrine.plugin" do
5 | describe "when called globally" do
6 | before do
7 | @components = [Shrine, Shrine::UploadedFile, Shrine::Attachment, Shrine::Attacher]
8 |
9 | @components.each do |component|
10 | component::InstanceMethods.send(:define_method, :foo) { :foo }
11 | component::ClassMethods.send(:define_method, :foo) { :foo }
12 | end
13 |
14 | module TestPlugin
15 | module ClassMethods; def foo; :plugin_foo; end; end
16 | module InstanceMethods; def foo; :plugin_foo; end; end
17 | module FileClassMethods; def foo; :plugin_foo; end; end
18 | module FileMethods; def foo; :plugin_foo; end; end
19 | module AttacherClassMethods; def foo; :plugin_foo; end; end
20 | module AttacherMethods; def foo; :plugin_foo; end; end
21 | module AttachmentClassMethods; def foo; :plugin_foo; end; end
22 | module AttachmentMethods; def foo; :plugin_foo; end; end
23 | end
24 | end
25 |
26 | after do
27 | @components.each do |component|
28 | component::InstanceMethods.send(:undef_method, :foo)
29 | component::ClassMethods.send(:undef_method, :foo)
30 | end
31 |
32 | TestPlugin.constants.each do |name|
33 | mod = TestPlugin.const_get(name)
34 | mod.send(:undef_method, :foo)
35 | end
36 | end
37 |
38 | it "allows the plugin to override base methods of core classes" do
39 | assert_equal :foo, Shrine.foo
40 | assert_equal :foo, Shrine.allocate.foo
41 | assert_equal :foo, Shrine::UploadedFile.foo
42 | assert_equal :foo, Shrine::UploadedFile.allocate.foo
43 | assert_equal :foo, Shrine::Attacher.foo
44 | assert_equal :foo, Shrine::Attacher.allocate.foo
45 | assert_equal :foo, Shrine::Attachment.foo
46 | assert_equal :foo, Shrine::Attachment.new(:bar).foo
47 |
48 | Shrine.plugin TestPlugin
49 |
50 | assert_equal :plugin_foo, Shrine.foo
51 | assert_equal :plugin_foo, Shrine.allocate.foo
52 | assert_equal :plugin_foo, Shrine::UploadedFile.foo
53 | assert_equal :plugin_foo, Shrine::UploadedFile.allocate.foo
54 | assert_equal :plugin_foo, Shrine::Attacher.foo
55 | assert_equal :plugin_foo, Shrine::Attacher.allocate.foo
56 | assert_equal :plugin_foo, Shrine::Attachment.foo
57 | assert_equal :plugin_foo, Shrine::Attachment.new(:bar).foo
58 | end
59 | end
60 | end
61 | end
62 |
--------------------------------------------------------------------------------
/test/storage/memory_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 | require "shrine/storage/s3"
3 | require "shrine/storage/linter"
4 |
5 | describe Shrine::Storage::Memory do
6 | before do
7 | @memory = Shrine::Storage::Memory.new
8 | end
9 |
10 | it "passes the linter" do
11 | assert Shrine::Storage::Linter.call(@memory)
12 | end
13 |
14 | describe "#upload" do
15 | it "accepts keyword arguments" do
16 | @memory.upload(fakeio, "key", foo: "bar")
17 | end
18 | end
19 |
20 | describe "#open" do
21 | it "accepts keyword arguments" do
22 | @memory.upload(fakeio, "key")
23 | @memory.open("key", foo: "bar")
24 | end
25 | end
26 |
27 | # work around apparent bug in ruby 2.7.0
28 | # https://bugs.ruby-lang.org/issues/16497
29 | it "preserves encoding despite Encoding.default_internal set" do
30 | @memory.upload(fakeio("content".b), "key")
31 |
32 | begin
33 | original_internal = Encoding.default_internal
34 | Encoding.default_internal = Encoding::UTF_8
35 |
36 | assert_equal Encoding::BINARY, @memory.open("key").read.encoding
37 | ensure
38 | Encoding.default_internal = original_internal
39 | end
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/test/support/activerecord.rb:
--------------------------------------------------------------------------------
1 | require "active_record"
2 |
3 | ActiveRecord::Base.establish_connection(
4 | adapter: "sqlite3",
5 | database: ":memory:",
6 | )
7 |
8 | ActiveRecord::Base.connection.create_table(:users) do |t|
9 | t.string :name
10 | t.text :avatar_data
11 | end
12 |
--------------------------------------------------------------------------------
/test/support/fakeio.rb:
--------------------------------------------------------------------------------
1 | require "forwardable"
2 | require "stringio"
3 |
4 | class FakeIO
5 | attr_reader :original_filename, :content_type
6 |
7 | def initialize(content, filename: nil, content_type: nil)
8 | @io = StringIO.new(content)
9 | @original_filename = filename
10 | @content_type = content_type
11 | end
12 |
13 | extend Forwardable
14 | delegate %i[read rewind eof? close size] => :@io
15 | end
16 |
--------------------------------------------------------------------------------
/test/support/file_helper.rb:
--------------------------------------------------------------------------------
1 | require "./test/support/fakeio"
2 |
3 | module FileHelper
4 | extend self
5 |
6 | def fakeio(content = "file", **options)
7 | FakeIO.new(content, **options)
8 | end
9 |
10 | def image
11 | File.open("test/fixtures/image.jpg", binmode: true)
12 | end
13 |
14 | def tempfile(content, basename = "")
15 | tempfile = Tempfile.new(basename, binmode: true)
16 | tempfile.write(content)
17 | tempfile.tap(&:open)
18 | end
19 | end
20 |
21 | Minitest::Test.include FileHelper
22 |
--------------------------------------------------------------------------------
/test/support/logging_helper.rb:
--------------------------------------------------------------------------------
1 | require "stringio"
2 |
3 | module LoggingHelper
4 | def assert_logged(pattern)
5 | result = nil
6 | logged = capture_logged { result = yield }
7 |
8 | assert_match pattern, logged
9 |
10 | result
11 | end
12 |
13 | def refute_logged(pattern)
14 | result = nil
15 | logged = capture_logged { result = yield }
16 |
17 | refute_match pattern, logged
18 |
19 | result
20 | end
21 |
22 | def capture_logged
23 | previous_logger = Shrine.logger
24 | output = StringIO.new
25 | Shrine.logger = Logger.new(output)
26 | Shrine.logger.formatter = -> (*, message) { "#{message}\n" }
27 |
28 | yield
29 |
30 | output.string
31 | ensure
32 | Shrine.logger = previous_logger
33 | end
34 | end
35 |
36 | Minitest::Test.include LoggingHelper
37 |
--------------------------------------------------------------------------------
/test/support/sequel.rb:
--------------------------------------------------------------------------------
1 | require "sequel"
2 |
3 | DB = Sequel.connect("#{"jdbc:" if RUBY_ENGINE == "jruby"}sqlite::memory:")
4 |
5 | DB.create_table :users do
6 | primary_key :id
7 | String :name
8 | String :avatar_data
9 | end
10 |
--------------------------------------------------------------------------------
/test/support/shrine_helper.rb:
--------------------------------------------------------------------------------
1 | require "shrine/storage/memory"
2 |
3 | module ShrineHelper
4 | def shrine(&block)
5 | uploader_class = Class.new(Shrine)
6 | uploader_class.storages[:cache] = Shrine::Storage::Memory.new
7 | uploader_class.storages[:other_cache] = Shrine::Storage::Memory.new
8 | uploader_class.storages[:store] = Shrine::Storage::Memory.new
9 | uploader_class.storages[:other_store] = Shrine::Storage::Memory.new
10 | uploader_class.class_eval(&block) if block
11 | uploader_class
12 | end
13 |
14 | def uploader(storage_key = :store, &block)
15 | uploader_class = shrine(&block)
16 | uploader_class.new(storage_key)
17 | end
18 |
19 | def attacher(**options, &block)
20 | shrine = shrine(&block)
21 | shrine::Attacher.new(**options)
22 | end
23 |
24 | def model_class(*attributes)
25 | klass = entity_class(*attributes)
26 | klass.attr_writer *attributes
27 | klass
28 | end
29 |
30 | def entity_class(*attributes)
31 | klass = Class.new(Struct)
32 | klass.attr_reader *attributes
33 | klass
34 | end
35 |
36 | def entity(attributes)
37 | entity_class = entity_class(*attributes.keys)
38 | entity_class.new(attributes)
39 | end
40 |
41 | def model(attributes)
42 | model_class = model_class(*attributes.keys)
43 | model_class.new(attributes)
44 | end
45 |
46 | class Struct
47 | # These are private on Ruby 2.4 and older, so we make them public.
48 | def self.attr_reader(*names) super(*names) end
49 | def self.attr_writer(*names) super(*names) end
50 |
51 | def initialize(attributes = {})
52 | attributes.each do |name, value|
53 | instance_variable_set(:"@#{name}", value)
54 | end
55 | end
56 | end
57 | end
58 |
59 | Minitest::Test.include ShrineHelper
60 |
--------------------------------------------------------------------------------
/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | Warning[:deprecated] = true if RUBY_VERSION >= "2.7"
2 |
3 | require "bundler/setup"
4 |
5 | if ENV["COVERAGE"]
6 | require "simplecov"
7 | SimpleCov.start do
8 | add_filter "/test/"
9 | add_filter "/lib/shrine/storage/linter.rb"
10 | end
11 | end
12 |
13 | ENV["MT_NO_EXPECTATIONS"] = "1" # disable Minitest's expectations monkey-patches
14 |
15 | require "minitest/autorun"
16 | require "minitest/pride"
17 |
18 | # Mocha still references the old constant
19 | MiniTest = Minitest unless defined?(MiniTest)
20 | require "mocha/minitest"
21 |
22 | require "shrine"
23 |
24 | Shrine.logger = Logger.new(nil) # disable mime_type warnings
25 |
26 | Mocha.configure { |config| config.stubbing_non_existent_method = :prevent }
27 |
28 | require "./test/support/shrine_helper"
29 | require "./test/support/file_helper"
30 | require "./test/support/logging_helper"
31 |
32 | class RubySerializer
33 | def self.dump(data)
34 | data.to_s
35 | end
36 |
37 | def self.load(data)
38 | eval(data)
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/website/docusaurus.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | title: "Shrine",
3 | tagline: "File attachment toolkit for Ruby applications",
4 | url: "https://shrinerb.com",
5 | baseUrl: "/",
6 | organizationName: "shrinerb",
7 | projectName: "shrine",
8 | trailingSlash: false,
9 | favicon: "img/favicon.ico",
10 | customFields: {
11 | projectVersion: "3.4.0",
12 | githubUrl: "https://github.com/shrinerb/shrine",
13 | blogUrl: "https://janko.io",
14 | discourseUrl: "https://discourse.shrinerb.com",
15 | githubDiscussionsUrl: "https://github.com/shrinerb/shrine/discussions",
16 | stackOverflowUrl: "https://stackoverflow.com/questions/tagged/shrine"
17 | },
18 | onBrokenLinks: "log",
19 | onBrokenMarkdownLinks: "log",
20 | presets: [
21 | [
22 | "@docusaurus/preset-classic",
23 | {
24 | docs: {
25 | path: "../doc",
26 | showLastUpdateAuthor: true,
27 | showLastUpdateTime: true,
28 | editUrl: "https://github.com/shrinerb/shrine/edit/master/doc/",
29 | sidebarPath: "sidebars.json",
30 | sidebarCollapsed: false,
31 | breadcrumbs: false,
32 | rehypePlugins: [
33 | [require("rehype-pretty-code"), { theme: "dracula-soft" }]
34 | ]
35 | },
36 | pages: {
37 | rehypePlugins: [
38 | [require("rehype-pretty-code"), { theme: "dracula-soft" }]
39 | ]
40 | },
41 | theme: {
42 | customCss: "src/css/customTheme.css"
43 | }
44 | }
45 | ]
46 | ],
47 | plugins: [],
48 | themeConfig: {
49 | navbar: {
50 | title: "Shrine",
51 | style: "primary",
52 | logo: {
53 | src: "img/logo.png",
54 | alt: "Shrine logo"
55 | },
56 | items: [
57 | {
58 | to: "docs/getting-started",
59 | label: "Guides",
60 | position: "left"
61 | },
62 | {
63 | type: "docSidebar",
64 | sidebarId: "plugins",
65 | label: "Plugins",
66 | position: "left"
67 | },
68 | {
69 | type: "docSidebar",
70 | sidebarId: "external",
71 | label: "External",
72 | position: "left"
73 | },
74 | {
75 | type: "docSidebar",
76 | sidebarId: "release_notes",
77 | label: "Release Notes",
78 | position: "left"
79 | },
80 | {
81 | href: "https://github.com/shrinerb/shrine",
82 | label: "GitHub",
83 | position: "right"
84 | },
85 | {
86 | href: "https://github.com/shrinerb/shrine/discussions",
87 | label: "Discussion",
88 | position: "right"
89 | },
90 | {
91 | href: "https://github.com/shrinerb/shrine/wiki",
92 | label: "Wiki",
93 | position: "right"
94 | }
95 | ]
96 | },
97 | colorMode: {
98 | disableSwitch: true,
99 | },
100 | image: "img/logo.png",
101 | footer: {
102 | copyright: `Copyright © ${new Date().getFullYear()} Janko Marohnić`,
103 | style: "dark",
104 | },
105 | algolia: {
106 | appId: "KBFWBJ5DPX",
107 | apiKey: "f00673f90ed23135519e3c40bae05e17",
108 | contextualSearch: false,
109 | indexName: "shrinerb"
110 | }
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/website/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "scripts": {
3 | "examples": "docusaurus-examples",
4 | "start": "docusaurus start",
5 | "build": "docusaurus build",
6 | "publish-gh-pages": "docusaurus-publish",
7 | "write-translations": "docusaurus-write-translations",
8 | "version": "docusaurus-version",
9 | "rename-version": "docusaurus-rename-version",
10 | "swizzle": "docusaurus swizzle",
11 | "deploy": "docusaurus deploy",
12 | "docusaurus": "docusaurus"
13 | },
14 | "dependencies": {
15 | "@docusaurus/core": "^2.4.1",
16 | "@docusaurus/preset-classic": "^2.4.1",
17 | "clsx": "^1.1.1",
18 | "react": "^17.0.2",
19 | "react-dom": "^17.0.2",
20 | "rehype-pretty-code": "^0.9.6",
21 | "shiki": "^0.14.2"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/website/src/css/customTheme.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --ifm-color-primary-lightest: #D50A13;
3 | --ifm-color-primary-lighter: #BD0911;
4 | --ifm-color-primary-light: #B40910;
5 | --ifm-color-primary: #A4080F;
6 | --ifm-color-primary-dark: #94070D;
7 | --ifm-color-primary-darker: #8B070D;
8 | --ifm-color-primary-darkest: #73060A;
9 |
10 | --ifm-color-danger: var(--ifm-color-primary);
11 | }
12 |
13 | pre {
14 | background: #282A36 !important;
15 | color: #f6f6f4 !important;
16 | }
17 |
18 | pre span {
19 | font-style: normal !important;
20 | }
21 |
--------------------------------------------------------------------------------
/website/src/pages/_demo.md:
--------------------------------------------------------------------------------
1 | You can attach files...
2 |
3 | ```rb
4 | class Photo < Sequel::Model
5 | include Shrine::Attachment(:image)
6 | end
7 | ```
8 | ```rb
9 | photo = Photo.create(image: file)
10 | photo.image #=> #
11 | photo.image_url #=> "https://my-bucket.s3.amazonaws.com/6bfafe89748ee135.jpg"
12 | photo.image.id #=> "6bfafe89748ee135.jpg"
13 | photo.image.storage #=> #
14 | photo.image.metadata #=> { "size" => 749238, "mime_type" => "image/jpeg", "filename" => "nature.jpg" }
15 | ```
16 |
17 | ...and process them eagerly...
18 |
19 | ```rb
20 | Shrine::Attacher.derivatives :thumbnails do |original|
21 | magick = ImageProcessing::MiniMagick.source(original)
22 | { large: magick.resize_to_limit!(800, 800),
23 | medium: magick.resize_to_limit!(500, 500),
24 | small: magick.resize_to_limit!(300, 300) }
25 | end
26 | ```
27 | ```rb
28 | photo.image_derivatives #=>
29 | # { large: #,
30 | # medium: #,
31 | # small: # }
32 | ```
33 |
34 | ...or on-the-fly...
35 |
36 | ```rb
37 | Shrine.derivation :thumbnail do |file, width, height|
38 | magick = ImageProcessing::MiniMagick.source(original)
39 | magick.resize_to_limit!(width.to_i, height.to_i)
40 | end
41 | ```
42 | ```rb
43 | photo.image.derivation_url(:thumbnail, 600, 400)
44 | #=> ".../thumbnail/600/400/eyJpZCI6ImZvbyIsInN0b3JhZ2UiOiJzdG9yZSJ9?signature=..."
45 | ```
46 |
--------------------------------------------------------------------------------
/website/src/pages/_sponsors.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default () => {
4 | const heartEmoji = "https://github.githubassets.com/images/icons/emoji/unicode/1f496.png"
5 |
6 | const sponsors = [
7 | {
8 | "name": "Ventrata",
9 | "avatar": "https://avatars0.githubusercontent.com/u/23163375?s=460&v=4",
10 | "link": "https://ventrata.com/"
11 | },
12 | {
13 | "name": "Scout APM",
14 | "avatar": "https://avatars2.githubusercontent.com/u/458509?s=460&v=4",
15 | "link": "https://scoutapm.com/"
16 | },
17 | {
18 | "name": "Fingerprint",
19 | "avatar": "https://avatars1.githubusercontent.com/u/827952?s=460&v=4",
20 | "link": "https://github.com/fingerprint"
21 | },
22 | {
23 | "name": "Mislav Marohnić",
24 | "avatar": "https://avatars1.githubusercontent.com/u/887?s=460&v=4",
25 | "link": "https://github.com/mislav"
26 | },
27 | {
28 | "name": "Stanko Krtalić",
29 | "avatar": "https://avatars1.githubusercontent.com/u/1655218?s=400&v=4",
30 | "link": "https://github.com/monorkin"
31 | },
32 | {
33 | "name": "Maxence",
34 | "avatar": "https://avatars2.githubusercontent.com/u/6090320?s=460&v=4",
35 | "link": "https://github.com/maxence33"
36 | },
37 | {
38 | "name": "Bryan O'Neal",
39 | "avatar": "https://avatars2.githubusercontent.com/u/13574856?s=460&u=796bc2484b66416c7a6c5c361f66a17e263abc60&v=4",
40 | "link": "https://github.com/1AL"
41 | },
42 | {
43 | "name": "Benjamin Klotz",
44 | "avatar": "https://avatars3.githubusercontent.com/u/675705?s=460&u=e561fcc1ba934317e6bd8742789211bf4d7cae73&v=4",
45 | "link": "https://github.com/tak1n"
46 | },
47 | {
48 | "name": "Igor S. Morozov",
49 | "avatar": "https://avatars3.githubusercontent.com/u/887264?s=460&u=2b573c7479a75cb46b23d3613f0302ae85b3aef3&v=4",
50 | "link": "https://github.com/Morozzzko"
51 | },
52 | {
53 | "name": "Wout",
54 | "avatar": "https://avatars1.githubusercontent.com/u/107324?s=460&u=5ab2e18bf785c061df13c53067cd671e2b74e10a&v=4",
55 | "link": "https://github.com/wout"
56 | },
57 | ]
58 |
59 | return (
60 |
61 |
62 |
Sponsors 
63 |
64 |
76 |
77 |
78 | )
79 | }
80 |
--------------------------------------------------------------------------------
/website/src/pages/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Layout from '@theme/Layout';
3 | import Sponsors from './_sponsors';
4 | import Demo from './_demo.md';
5 |
6 | export default () => {
7 | return (
8 |
9 |
10 |
11 |

12 |
Shrine
13 |
File attachment toolkit for Ruby applications
14 |
25 |
26 |
27 |
28 |
33 |
34 |
35 |
36 | );
37 | };
38 |
--------------------------------------------------------------------------------
/website/static/img/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shrinerb/shrine/1ce6da4be0ca62ecd5776f107fcd0d657b773f28/website/static/img/favicon.ico
--------------------------------------------------------------------------------
/website/static/img/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shrinerb/shrine/1ce6da4be0ca62ecd5776f107fcd0d657b773f28/website/static/img/logo.png
--------------------------------------------------------------------------------