shrine 2.14.0 → 2.15.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of shrine might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.md +384 -374
- data/README.md +132 -63
- data/doc/advantages.md +191 -109
- data/doc/attacher.md +1 -1
- data/doc/carrierwave.md +4 -4
- data/doc/creating_storages.md +2 -2
- data/doc/design.md +2 -2
- data/doc/direct_s3.md +3 -3
- data/doc/metadata.md +1 -1
- data/doc/multiple_files.md +2 -2
- data/doc/paperclip.md +3 -3
- data/doc/plugins/activerecord.md +92 -0
- data/doc/plugins/add_metadata.md +93 -0
- data/doc/plugins/backgrounding.md +148 -0
- data/doc/plugins/backup.md +29 -0
- data/doc/plugins/cached_attachment_data.md +23 -0
- data/doc/plugins/copy.md +22 -0
- data/doc/plugins/data_uri.md +92 -0
- data/doc/plugins/default_storage.md +16 -0
- data/doc/plugins/default_url.md +33 -0
- data/doc/plugins/default_url_options.md +22 -0
- data/doc/plugins/delete_promoted.md +10 -0
- data/doc/plugins/delete_raw.md +16 -0
- data/doc/plugins/derivation_endpoint.md +747 -0
- data/doc/plugins/determine_mime_type.md +64 -0
- data/doc/plugins/direct_upload.md +170 -0
- data/doc/plugins/download_endpoint.md +83 -0
- data/doc/plugins/dynamic_storage.md +20 -0
- data/doc/plugins/hooks.md +56 -0
- data/doc/plugins/included.md +15 -0
- data/doc/plugins/infer_extension.md +57 -0
- data/doc/plugins/keep_files.md +20 -0
- data/doc/plugins/logging.md +39 -0
- data/doc/plugins/metadata_attribues.md +43 -0
- data/doc/plugins/migration_helpers.md +58 -0
- data/doc/plugins/module_include.md +40 -0
- data/doc/plugins/moving.md +17 -0
- data/doc/plugins/multi_delete.md +18 -0
- data/doc/plugins/parallelize.md +14 -0
- data/doc/plugins/parsed_json.md +9 -0
- data/doc/plugins/presign_endpoint.md +133 -0
- data/doc/plugins/pretty_location.md +29 -0
- data/doc/plugins/processing.md +68 -0
- data/doc/plugins/rack_file.md +49 -0
- data/doc/plugins/rack_response.md +96 -0
- data/doc/plugins/recache.md +27 -0
- data/doc/plugins/refresh_metadata.md +31 -0
- data/doc/plugins/remote_url.md +104 -0
- data/doc/plugins/remove_attachment.md +16 -0
- data/doc/plugins/remove_invalid.md +9 -0
- data/doc/plugins/restore_cached_data.md +14 -0
- data/doc/plugins/sequel.md +64 -0
- data/doc/plugins/signature.md +49 -0
- data/doc/plugins/store_dimensions.md +68 -0
- data/doc/plugins/tempfile.md +40 -0
- data/doc/plugins/upload_endpoint.md +123 -0
- data/doc/plugins/upload_options.md +28 -0
- data/doc/plugins/validation_helpers.md +129 -0
- data/doc/plugins/versions.md +179 -0
- data/doc/processing.md +217 -247
- data/doc/refile.md +3 -3
- data/doc/release_notes/1.0.0.md +143 -0
- data/doc/release_notes/1.1.0.md +184 -0
- data/doc/release_notes/1.2.0.md +37 -0
- data/doc/release_notes/1.3.0.md +90 -0
- data/doc/release_notes/1.4.0.md +167 -0
- data/doc/release_notes/1.4.1.md +9 -0
- data/doc/release_notes/1.4.2.md +20 -0
- data/doc/release_notes/2.0.0.md +173 -0
- data/doc/release_notes/2.0.1.md +12 -0
- data/doc/release_notes/2.1.0.md +59 -0
- data/doc/release_notes/2.1.1.md +8 -0
- data/doc/release_notes/2.10.0.md +52 -0
- data/doc/release_notes/2.10.1.md +6 -0
- data/doc/release_notes/2.11.0.md +69 -0
- data/doc/release_notes/2.12.0.md +65 -0
- data/doc/release_notes/2.13.0.md +146 -0
- data/doc/release_notes/2.14.0.md +278 -0
- data/doc/release_notes/2.15.0.md +82 -0
- data/doc/release_notes/2.2.0.md +98 -0
- data/doc/release_notes/2.3.0.md +50 -0
- data/doc/release_notes/2.3.1.md +10 -0
- data/doc/release_notes/2.4.0.md +87 -0
- data/doc/release_notes/2.4.1.md +29 -0
- data/doc/release_notes/2.5.0.md +130 -0
- data/doc/release_notes/2.6.0.md +254 -0
- data/doc/release_notes/2.6.1.md +14 -0
- data/doc/release_notes/2.7.0.md +180 -0
- data/doc/release_notes/2.8.0.md +95 -0
- data/doc/release_notes/2.9.0.md +82 -0
- data/doc/retrieving_uploads.md +1 -1
- data/doc/storage/file_system.md +96 -0
- data/doc/storage/s3.md +293 -0
- data/doc/validation.md +1 -1
- data/lib/shrine/plugins/_urlsafe_serialization.rb +33 -125
- data/lib/shrine/plugins/activerecord.rb +0 -78
- data/lib/shrine/plugins/add_metadata.rb +0 -80
- data/lib/shrine/plugins/backgrounding.rb +0 -134
- data/lib/shrine/plugins/backup.rb +0 -22
- data/lib/shrine/plugins/cached_attachment_data.rb +0 -15
- data/lib/shrine/plugins/copy.rb +0 -14
- data/lib/shrine/plugins/data_uri.rb +0 -73
- data/lib/shrine/plugins/default_storage.rb +0 -11
- data/lib/shrine/plugins/default_url.rb +0 -25
- data/lib/shrine/plugins/default_url_options.rb +0 -16
- data/lib/shrine/plugins/delete_promoted.rb +0 -6
- data/lib/shrine/plugins/delete_raw.rb +0 -10
- data/lib/shrine/plugins/derivation_endpoint.rb +652 -0
- data/lib/shrine/plugins/determine_mime_type.rb +1 -85
- data/lib/shrine/plugins/direct_upload.rb +0 -155
- data/lib/shrine/plugins/download_endpoint.rb +11 -73
- data/lib/shrine/plugins/dynamic_storage.rb +0 -17
- data/lib/shrine/plugins/hooks.rb +0 -48
- data/lib/shrine/plugins/included.rb +0 -12
- data/lib/shrine/plugins/infer_extension.rb +0 -49
- data/lib/shrine/plugins/keep_files.rb +0 -19
- data/lib/shrine/plugins/logging.rb +0 -39
- data/lib/shrine/plugins/metadata_attributes.rb +0 -35
- data/lib/shrine/plugins/migration_helpers.rb +0 -50
- data/lib/shrine/plugins/module_include.rb +0 -32
- data/lib/shrine/plugins/moving.rb +0 -12
- data/lib/shrine/plugins/multi_delete.rb +0 -13
- data/lib/shrine/plugins/parallelize.rb +0 -8
- data/lib/shrine/plugins/parsed_json.rb +0 -5
- data/lib/shrine/plugins/presign_endpoint.rb +2 -117
- data/lib/shrine/plugins/pretty_location.rb +0 -22
- data/lib/shrine/plugins/processing.rb +0 -55
- data/lib/shrine/plugins/rack_file.rb +0 -39
- data/lib/shrine/plugins/rack_response.rb +0 -81
- data/lib/shrine/plugins/recache.rb +0 -21
- data/lib/shrine/plugins/refresh_metadata.rb +0 -24
- data/lib/shrine/plugins/remote_url.rb +0 -85
- data/lib/shrine/plugins/remove_attachment.rb +0 -10
- data/lib/shrine/plugins/remove_invalid.rb +0 -6
- data/lib/shrine/plugins/restore_cached_data.rb +0 -10
- data/lib/shrine/plugins/sequel.rb +0 -54
- data/lib/shrine/plugins/signature.rb +0 -37
- data/lib/shrine/plugins/store_dimensions.rb +0 -63
- data/lib/shrine/plugins/tempfile.rb +4 -35
- data/lib/shrine/plugins/upload_endpoint.rb +2 -109
- data/lib/shrine/plugins/upload_options.rb +0 -20
- data/lib/shrine/plugins/validation_helpers.rb +0 -36
- data/lib/shrine/plugins/versions.rb +0 -156
- data/lib/shrine/storage/file_system.rb +0 -77
- data/lib/shrine/storage/s3.rb +0 -249
- data/lib/shrine/version.rb +1 -1
- data/shrine.gemspec +2 -2
- metadata +86 -6
data/README.md
CHANGED
@@ -6,7 +6,7 @@ Shrine is a toolkit for file attachments in Ruby applications. Some highlights:
|
|
6
6
|
* **Memory friendly** – streaming uploads and downloads make it work great with large files
|
7
7
|
* **Cloud storage** – store files on [disk][FileSystem], [AWS S3][S3], [Google Cloud][GCS], [Cloudinary] and others
|
8
8
|
* **ORM integrations** – works with [Sequel][sequel plugin], [ActiveRecord][activerecord plugin], [Hanami::Model][hanami plugin] and [Mongoid][mongoid plugin]
|
9
|
-
* **Flexible processing** – generate thumbnails
|
9
|
+
* **Flexible processing** – generate thumbnails [on upload] or [on-the-fly] using [ImageMagick][ImageProcessing::MiniMagick] or [libvips][ImageProcessing::Vips]
|
10
10
|
* **Metadata validation** – [validate files][validation_helpers plugin] based on [extracted metadata][Extracting Metadata]
|
11
11
|
* **Direct uploads** – upload asynchronously [to your app][upload_endpoint plugin] or [to the cloud][presign_endpoint plugin] using [Uppy]
|
12
12
|
* **Resumable uploads** – make large file uploads [resumable][tus] by pointing [Uppy][uppy tus] to a [resumable endpoint][tus-ruby-server]
|
@@ -16,12 +16,14 @@ If you're curious how it compares to other file attachment libraries, see the [A
|
|
16
16
|
|
17
17
|
## Resources
|
18
18
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
19
|
+
| Resource | URL |
|
20
|
+
| :------- | :--- |
|
21
|
+
| Website | [shrinerb.com](https://shrinerb.com) |
|
22
|
+
| Demo code | [Roda][roda demo] / [Rails][rails demo] |
|
23
|
+
| Source | [github.com/shrinerb/shrine](https://github.com/shrinerb/shrine) |
|
24
|
+
| Wiki | [github.com/shrinerb/shrine/wiki](https://github.com/shrinerb/shrine/wiki) |
|
25
|
+
| Bugs | [github.com/shrinerb/shrine/issues](https://github.com/shrinerb/shrine/issues) |
|
26
|
+
| Help & Discussion | [groups.google.com/group/ruby-shrine](https://groups.google.com/forum/#!forum/ruby-shrine) |
|
25
27
|
|
26
28
|
## Quick start
|
27
29
|
|
@@ -509,13 +511,15 @@ uploaded_file.metadata["foo"] #=> "bar"
|
|
509
511
|
|
510
512
|
## Processing
|
511
513
|
|
512
|
-
Shrine
|
513
|
-
|
514
|
+
Shrine allows you to processing attached files either "on upload" or
|
515
|
+
"on-the-fly". For example, if your app is accepting image uploads, you can
|
516
|
+
generate a pre-defined set of of thumbnails as soon as the image is attached to
|
517
|
+
a record ("on upload"), or you can generate necessary thumbnails dynamically as
|
518
|
+
they're needed ("on-the-fly").
|
514
519
|
|
515
|
-
|
516
|
-
It
|
517
|
-
|
518
|
-
also need to load the `versions` plugin to be able to save multiple files.
|
520
|
+
In both cases, for image processing you can use the **[ImageProcessing]** gem.
|
521
|
+
It provides a convenient unified API for processing with [ImageMagick] or
|
522
|
+
[libvips]. Here is an example of generating a thumbnail with ImageProcessing:
|
519
523
|
|
520
524
|
```sh
|
521
525
|
$ brew install imagemagick
|
@@ -524,6 +528,28 @@ $ brew install imagemagick
|
|
524
528
|
# Gemfile
|
525
529
|
gem "image_processing", "~> 1.0"
|
526
530
|
```
|
531
|
+
```rb
|
532
|
+
require "image/mini_magick"
|
533
|
+
|
534
|
+
thumbnail = ImageProcessing::MiniMagick
|
535
|
+
.source(original_image)
|
536
|
+
.resize_to_limit!(600, 400)
|
537
|
+
|
538
|
+
thumbnail #=> #<Tempfile:...> (thumbnail limited to 600x400)
|
539
|
+
```
|
540
|
+
|
541
|
+
### On upload
|
542
|
+
|
543
|
+
Shrine allows you intercept when a cached file is being uploaded to permanent
|
544
|
+
storage, and perform any file processing you might want. The result of
|
545
|
+
processing can also be multiple files, such as thumbnails of various
|
546
|
+
dimensions. This processing can additionaly be delayed into a [background
|
547
|
+
job](#backgrounding).
|
548
|
+
|
549
|
+
The promotion hook is provided by the `processing` plugin, while the ability
|
550
|
+
to save multiple files is provided by the `versions` plugin. Let's set up our
|
551
|
+
uploader to generate some thumbnails from the attached image:
|
552
|
+
|
527
553
|
```rb
|
528
554
|
require "image_processing/mini_magick"
|
529
555
|
|
@@ -553,6 +579,8 @@ After these files have been uploaded, their data will all be saved to the
|
|
553
579
|
of `Shrine::UploadedFile` objects.
|
554
580
|
|
555
581
|
```rb
|
582
|
+
photo = Photo.create(image: image)
|
583
|
+
|
556
584
|
photo.image_data #=>
|
557
585
|
# '{
|
558
586
|
# "original": {"id":"9sd84.jpg", "storage":"store", "metadata":{...}},
|
@@ -581,8 +609,53 @@ The `versions` plugin also expands `#<attachment>_url` to accept version names:
|
|
581
609
|
photo.image_url(:large) #=> "https://..."
|
582
610
|
```
|
583
611
|
|
584
|
-
For more details
|
585
|
-
|
612
|
+
For more details see the [File Processing] guide.
|
613
|
+
|
614
|
+
### On-the-fly
|
615
|
+
|
616
|
+
On-the-fly processing is provided by the
|
617
|
+
[`derivation_endpoint`][derivation_endpoint plugin] plugin. It provides a
|
618
|
+
mountable Rack app, which on request will call the processing we defined.
|
619
|
+
|
620
|
+
We start by loading the plugin with a secret key and a path prefix to where
|
621
|
+
we'll mount the Rack app, and defining a "derivation" we want the app to call:
|
622
|
+
|
623
|
+
```rb
|
624
|
+
require "image_processing/mini_magick"
|
625
|
+
|
626
|
+
class ImageUploader < Shrine
|
627
|
+
plugin :derivation_endpoint,
|
628
|
+
secret_key: "<YOUR SECRET KEY>",
|
629
|
+
prefix: "derivations/image"
|
630
|
+
|
631
|
+
derivation :thumbnail do |file, width, height|
|
632
|
+
ImageProcessing::MiniMagick
|
633
|
+
.source(file)
|
634
|
+
.resize_to_limit!(width.to_i, height.to_i)
|
635
|
+
end
|
636
|
+
end
|
637
|
+
```
|
638
|
+
|
639
|
+
Then we can mount the Rack app into our router (for web frameworks other than
|
640
|
+
Rails see [Mounting Endpoints] wiki):
|
641
|
+
|
642
|
+
```rb
|
643
|
+
# config/routes.rb (Rails)
|
644
|
+
Rails.application.routes.draw do
|
645
|
+
mount ImageUploader.derivation_endpoint => "derivations/image"
|
646
|
+
end
|
647
|
+
```
|
648
|
+
|
649
|
+
Now we can generate URLs from attached files that on request will call the
|
650
|
+
processing we defined:
|
651
|
+
|
652
|
+
```rb
|
653
|
+
photo.image.derivation_url(:thumbnail, "600", "400")
|
654
|
+
#=> "/derivations/image/thumbnail/600/400/eyJpZCI6ImZvbyIsInN0b3JhZ2UiOiJzdG9yZSJ9?signature=..."
|
655
|
+
```
|
656
|
+
|
657
|
+
The `derivation_endpoint` plugin is highly customizable, for more details see
|
658
|
+
its [documentation][derivation_endpoint plugin].
|
586
659
|
|
587
660
|
## Context
|
588
661
|
|
@@ -638,8 +711,8 @@ user.valid? #=> false
|
|
638
711
|
user.errors.to_hash #=> {:cv=>["is too large (max is 5 MB)"]}
|
639
712
|
```
|
640
713
|
|
641
|
-
See the [File Validation] guide and `validation_helpers`
|
642
|
-
for more details.
|
714
|
+
See the [File Validation] guide and [`validation_helpers`][validation_helpers
|
715
|
+
plugin] plugin documentation for more details.
|
643
716
|
|
644
717
|
## Location
|
645
718
|
|
@@ -699,8 +772,8 @@ server and attached to a record, just like with raw files. The only difference
|
|
699
772
|
is that they won't be additionally uploaded to temporary storage on assignment,
|
700
773
|
as they were already uploaded on the client side. Note that by default **Shrine
|
701
774
|
won't extract metadata from directly uploaded files**, instead it will just copy
|
702
|
-
metadata that was extacted on the client side; see [this section][metadata
|
703
|
-
for the rationale and instructions on how to opt in.
|
775
|
+
metadata that was extacted on the client side; see [this section][metadata
|
776
|
+
direct uploads] for the rationale and instructions on how to opt in.
|
704
777
|
|
705
778
|
For handling client side uploads it's recommended to use **[Uppy]**. Uppy is a
|
706
779
|
very flexible modern JavaScript file upload library, which happens to integrate
|
@@ -711,22 +784,16 @@ nicely with Shrine.
|
|
711
784
|
The simplest approach is creating an upload endpoint in your app that will
|
712
785
|
receive uploads and forward them to the specified storage. You can use the
|
713
786
|
`upload_endpoint` Shrine plugin to create a Rack app that handles uploads,
|
714
|
-
and mount it inside your application
|
787
|
+
and mount it inside your application (for web frameworks other than Rails
|
788
|
+
see [Mounting Endpoints] wiki).
|
715
789
|
|
716
790
|
```rb
|
717
791
|
Shrine.plugin :upload_endpoint
|
718
792
|
```
|
719
793
|
```rb
|
720
|
-
# config.ru (Rack)
|
721
|
-
map "/images/upload" do
|
722
|
-
run ImageUploader.upload_endpoint(:cache)
|
723
|
-
end
|
724
|
-
|
725
|
-
# OR
|
726
|
-
|
727
794
|
# config/routes.rb (Rails)
|
728
795
|
Rails.application.routes.draw do
|
729
|
-
mount ImageUploader.upload_endpoint(:cache) => "
|
796
|
+
mount ImageUploader.upload_endpoint(:cache) => "images/upload"
|
730
797
|
end
|
731
798
|
```
|
732
799
|
|
@@ -759,24 +826,20 @@ end
|
|
759
826
|
If you want to free your app from receiving file uploads, you can also upload
|
760
827
|
files directly to the cloud (AWS S3, Google Cloud etc). In this flow the client
|
761
828
|
is required to first fetch upload parameters from the server, and then use these
|
762
|
-
parameters to make the upload.
|
763
|
-
|
764
|
-
|
829
|
+
parameters to make the upload.
|
830
|
+
|
831
|
+
You can use the `presign_endpoint` Shrine plugin to create a Rack app that
|
832
|
+
generates these upload parameters (provided that the underlying storage
|
833
|
+
implements `#presign`), and mount it inside your application (for web
|
834
|
+
frameworks other than Rails see [Mounting Endpoints] wiki):
|
765
835
|
|
766
836
|
```rb
|
767
837
|
Shrine.plugin :presign_endpoint
|
768
838
|
```
|
769
839
|
```rb
|
770
|
-
# config.ru (Rack)
|
771
|
-
map "/s3/params" do
|
772
|
-
run Shrine.presign_endpoint(:cache)
|
773
|
-
end
|
774
|
-
|
775
|
-
# OR
|
776
|
-
|
777
840
|
# config/routes.rb (Rails)
|
778
841
|
Rails.application.routes.draw do
|
779
|
-
mount Shrine.presign_endpoint(:cache) => "
|
842
|
+
mount Shrine.presign_endpoint(:cache) => "s3/params"
|
780
843
|
end
|
781
844
|
```
|
782
845
|
|
@@ -969,54 +1032,60 @@ The gem is available as open source under the terms of the [MIT License].
|
|
969
1032
|
|
970
1033
|
[Shrine]: https://shrinerb.com
|
971
1034
|
[plugin system]: https://twin.github.io/the-plugin-system-of-sequel-and-roda/
|
972
|
-
[FileSystem]:
|
973
|
-
[S3]:
|
1035
|
+
[FileSystem]: /doc/storage/file_system.md#readme
|
1036
|
+
[S3]: /doc/storage/s3.md#readme
|
974
1037
|
[GCS]: https://github.com/renchap/shrine-google_cloud_storage
|
975
1038
|
[Cloudinary]: https://github.com/shrinerb/shrine-cloudinary
|
976
1039
|
[Transloadit]: https://github.com/shrinerb/shrine-transloadit
|
977
|
-
[activerecord plugin]:
|
978
|
-
[sequel plugin]:
|
1040
|
+
[activerecord plugin]: /doc/plugins/activerecord.md#readme
|
1041
|
+
[sequel plugin]: /doc/plugins/sequel.md#readme
|
979
1042
|
[hanami plugin]: https://github.com/katafrakt/hanami-shrine
|
980
1043
|
[mongoid plugin]: https://github.com/shrinerb/shrine-mongoid
|
981
|
-
[
|
982
|
-
[
|
1044
|
+
[ImageProcessing]: https://github.com/janko/image_processing
|
1045
|
+
[on upload]: #on-upload
|
1046
|
+
[on-the-fly]: #on-the-fly
|
1047
|
+
[ImageProcessing::MiniMagick]: https://github.com/janko/image_processing/blob/master/doc/minimagick.md#readme
|
1048
|
+
[ImageProcessing::Vips]: https://github.com/janko/image_processing/blob/master/doc/vips.md#readme
|
1049
|
+
[ImageMagick]: https://imagemagick.org/
|
983
1050
|
[libvips]: http://libvips.github.io/libvips/
|
984
|
-
[
|
985
|
-
[
|
986
|
-
[
|
1051
|
+
[derivation_endpoint plugin]: /doc/plugins/derivation_endpoint.md#readme
|
1052
|
+
[validation_helpers plugin]: /doc/plugins/validation_helpers.md#readme
|
1053
|
+
[upload_endpoint plugin]: /doc/plugins/upload_endpoint.md#readme
|
1054
|
+
[presign_endpoint plugin]: /doc/plugins/presign_endpoint.md#readme
|
987
1055
|
[Uppy]: https://uppy.io
|
988
1056
|
[tus]: https://tus.io
|
989
1057
|
[uppy tus]: https://uppy.io/docs/tus/
|
990
|
-
[tus-ruby-server]: https://github.com/janko
|
991
|
-
[backgrounding plugin]:
|
992
|
-
[Advantages of Shrine]:
|
1058
|
+
[tus-ruby-server]: https://github.com/janko/tus-ruby-server
|
1059
|
+
[backgrounding plugin]: /doc/plugins/backgrounding.md#readme
|
1060
|
+
[Advantages of Shrine]: /doc/advantages.md#readme
|
993
1061
|
[external storages]: https://shrinerb.com/#external
|
994
|
-
[creating storage]:
|
995
|
-
[creating plugin]:
|
996
|
-
[Retrieving Uploads]:
|
997
|
-
[Using Attacher]:
|
1062
|
+
[creating storage]: /doc/creating_storages.md#readme
|
1063
|
+
[creating plugin]: /doc/creating_plugins.md#readme
|
1064
|
+
[Retrieving Uploads]: /doc/retrieving_uploads.md#readme
|
1065
|
+
[Using Attacher]: /doc/attacher.md#readme
|
998
1066
|
[plugins]: https://shrinerb.com/#plugins
|
999
1067
|
[`file`]: http://linux.die.net/man/1/file
|
1000
|
-
[Extracting Metadata]:
|
1001
|
-
[File Processing]:
|
1002
|
-
[File Validation]:
|
1003
|
-
[metadata direct uploads]:
|
1068
|
+
[Extracting Metadata]: /doc/metadata.md#readme
|
1069
|
+
[File Processing]: /doc/processing.md#readme
|
1070
|
+
[File Validation]: /doc/validation.md#readme
|
1071
|
+
[metadata direct uploads]: /doc/metadata.md#direct-uploads
|
1004
1072
|
[uppy xhr upload]: https://uppy.io/docs/xhr-upload/
|
1005
1073
|
[direct uploads walkthrough]: https://github.com/shrinerb/shrine/wiki/Adding-Direct-App-Uploads
|
1006
1074
|
[uppy aws s3]: https://uppy.io/docs/aws-s3/
|
1007
1075
|
[direct S3 uploads walkthrough]: https://github.com/shrinerb/shrine/wiki/Adding-Direct-S3-Uploads<Paste>
|
1008
|
-
[direct S3 uploads guide]:
|
1076
|
+
[direct S3 uploads guide]: /doc/direct_s3.md#readme
|
1009
1077
|
[roda demo]: https://github.com/shrinerb/shrine/tree/master/demo
|
1010
1078
|
[rails demo]: https://github.com/erikdahlstrand/shrine-rails-example
|
1011
1079
|
[shrine-tus]: https://github.com/shrinerb/shrine-tus
|
1012
1080
|
[uppy aws s3 multipart]: https://uppy.io/docs/aws-s3-multipart/
|
1013
|
-
[uppy-s3_multipart]: https://github.com/janko
|
1081
|
+
[uppy-s3_multipart]: https://github.com/janko/uppy-s3_multipart
|
1014
1082
|
[resumable uploads walkthrough]: https://github.com/shrinerb/shrine/wiki/Adding-Resumable-Uploads
|
1015
1083
|
[resumable demo]: https://github.com/shrinerb/shrine-tus-demo
|
1016
|
-
[backgrounding libraries]: https://github.com/shrinerb/shrine/wiki/Backgrounding-
|
1084
|
+
[backgrounding libraries]: https://github.com/shrinerb/shrine/wiki/Backgrounding-Libraries
|
1085
|
+
[Mounting Endpoints]: https://github.com/shrinerb/shrine/wiki/Mounting-Endpoints
|
1017
1086
|
[S3 lifecycle Console]: http://docs.aws.amazon.com/AmazonS3/latest/UG/lifecycle-configuration-bucket-no-versioning.html
|
1018
1087
|
[S3 lifecycle API]: https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Client.html#put_bucket_lifecycle_configuration-instance_method
|
1019
1088
|
[Roda]: https://github.com/jeremyevans/roda
|
1020
1089
|
[Refile]: https://github.com/refile/refile
|
1021
|
-
[CoC]:
|
1090
|
+
[CoC]: CODE_OF_CONDUCT.md
|
1022
1091
|
[MIT License]: http://opensource.org/licenses/MIT
|
data/doc/advantages.md
CHANGED
@@ -26,6 +26,7 @@ provided as plugins.
|
|
26
26
|
Shrine.plugin :upload_endpoint
|
27
27
|
Shrine.plugin :presign_endpoint
|
28
28
|
Shrine.plugin :download_endpoint
|
29
|
+
Shrine.plugin :derivation_endpoint
|
29
30
|
Shrine.plugin :rack_response
|
30
31
|
Shrine.plugin :rack_file
|
31
32
|
|
@@ -42,7 +43,7 @@ Shrine was designed with simplicity in mind. Where other solutions favour
|
|
42
43
|
complex class-level DSLs, Shrine chooses simple instance-level interfaces where
|
43
44
|
you can write regular Ruby code.
|
44
45
|
|
45
|
-
There are no `CarrierWave::Uploader::Base` and `Paperclip::Attachment` [
|
46
|
+
There are no `CarrierWave::Uploader::Base` and `Paperclip::Attachment` [god
|
46
47
|
objects], Shrine has several core classes each with clear responsibilities:
|
47
48
|
|
48
49
|
* Storage classes encapsulate file operations for the underlying service
|
@@ -64,31 +65,53 @@ minimal amount of code.
|
|
64
65
|
## Modularity
|
65
66
|
|
66
67
|
Shrine uses a [plugin system] that allows you to pick and choose the features
|
67
|
-
that you want
|
68
|
-
|
69
|
-
very fast.
|
68
|
+
that you want. Moreover, you're only loading the code for features that you
|
69
|
+
use, which means that Shrine will generally load very fast.
|
70
70
|
|
71
71
|
```rb
|
72
|
-
Shrine.plugin :logging
|
72
|
+
Shrine.plugin :logging
|
73
|
+
|
74
|
+
# translates to
|
75
|
+
|
76
|
+
require "shrine/plugins/logging"
|
77
|
+
Shrine.plugin Shrine::Plugins::Logging
|
73
78
|
```
|
74
79
|
|
75
80
|
Shrine comes with a complete attachment functionality, but it also exposes many
|
76
81
|
low level APIs that can be used for building your own customized attachment
|
77
|
-
flow.
|
82
|
+
flow. For example, if you prefer the `Attachment`/`Blob` architecture Active
|
83
|
+
Storage provides, you can ditch the Shrine's attachment implementation and use
|
84
|
+
uploaders and uploaded files that are decoupled from attachment:
|
85
|
+
|
86
|
+
```rb
|
87
|
+
uploader = ImageUploader.new(:store)
|
88
|
+
uploaded_file = uploader.upload(image) # metadata extraction, upload location generation
|
89
|
+
|
90
|
+
uploaded_file.id #=> "44ccafc10ce6a4ff22829e8f579ee6b9.jpg"
|
91
|
+
uplaoded_file.metadata #=> { ... extracted metadata ... }
|
92
|
+
|
93
|
+
data = uploaded_file.to_json # serialization
|
94
|
+
# ...
|
95
|
+
uploaded_file = ImageUploader.uploaded_file(data) # deserialization
|
96
|
+
|
97
|
+
uploaded_file.url #=> "https://..."
|
98
|
+
uploaded_file.download { |tempfile| ... } # streaming download
|
99
|
+
uploaded_file.delete
|
100
|
+
```
|
78
101
|
|
79
102
|
### Dependencies
|
80
103
|
|
81
|
-
Shrine is very diligent when it comes to dependencies. It has
|
82
|
-
|
83
|
-
Shrine plugins require additional dependencies,
|
84
|
-
if you're using those plugins.
|
104
|
+
Shrine is very diligent when it comes to dependencies. It has two mandatory
|
105
|
+
dependencies – [Down] and [ContentDisposition] – which are loaded only by
|
106
|
+
components that need them. Some Shrine plugins require additional dependencies,
|
107
|
+
but you only need to load them if you're using those plugins.
|
85
108
|
|
86
|
-
Moreover, Shrine often
|
87
|
-
for doing the same task. For example, the
|
88
|
-
you to choose between the [`file`] command,
|
89
|
-
[MimeMagic], or [Marcel] gem for determining the MIME
|
90
|
-
`store_dimensions` plugin can extract dimensions using
|
91
|
-
[MiniMagick], or [ruby-vips] gem.
|
109
|
+
Moreover, Shrine often gives you the ability choose between multiple
|
110
|
+
alternative dependencies for doing the same task. For example, the
|
111
|
+
`determine_mime_type` plugin allows you to choose between the [`file`] command,
|
112
|
+
[FileMagic], [FastImage], [MimeMagic], or [Marcel] gem for determining the MIME
|
113
|
+
type, while the `store_dimensions` plugin can extract dimensions using
|
114
|
+
[FastImage], [MiniMagick], or [ruby-vips] gem.
|
92
115
|
|
93
116
|
```rb
|
94
117
|
Shrine.plugin :determine_mime_type, analyzer: :marcel
|
@@ -104,9 +127,9 @@ Shrine is designed to handle any types of files. If you're accepting uploads of
|
|
104
127
|
multiple types of files, such as videos and images, chances are that the logic
|
105
128
|
for handling them will be very different:
|
106
129
|
|
107
|
-
* images can be processed on-the-fly,
|
108
|
-
*
|
109
|
-
*
|
130
|
+
* small images can be processed on-the-fly, but large files should be processed in a background job
|
131
|
+
* which storage service is most suitable might depend on the filetype (images, documents, audios, videos)
|
132
|
+
* different filetypes have different metadata to extract which require different tools
|
110
133
|
|
111
134
|
With Shrine you can create isolated uploaders for each type of file. Plugins
|
112
135
|
that you want to be applied to both uploaders can be applied globally, while
|
@@ -129,12 +152,63 @@ end
|
|
129
152
|
|
130
153
|
## Processing
|
131
154
|
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
155
|
+
Most file attachment libraries give you the ability to process files either "on
|
156
|
+
upload" (Paperclip, CarrierWave) or "on-the-fly" (Dragonfly, Refile, Active
|
157
|
+
Storage). Having only one option is not ideal, because some type of files
|
158
|
+
it's more suitable to process on-the-fly (image thumbnails, document previews),
|
159
|
+
while other types of files should be processed in a background job (video
|
160
|
+
transcoding, raw images)
|
161
|
+
|
162
|
+
Shrine is the first file attachment library that has support for both
|
163
|
+
processing on upload and on-the-fly. So, if you're handling image uploads, you
|
164
|
+
can choose to either generate a set of pre-defined image thumbnails in a
|
165
|
+
background job:
|
166
|
+
|
167
|
+
```rb
|
168
|
+
class ImageUploader < Shrine
|
169
|
+
process(:store) do |io|
|
170
|
+
versions = { original: io }
|
171
|
+
|
172
|
+
io.download do |original|
|
173
|
+
pipeline = ImageProcessing::MiniMagick.source(original)
|
174
|
+
|
175
|
+
versions[:large] = pipeline.resize_to_limit!(800, 800)
|
176
|
+
versions[:medium] = pipeline.resize_to_limit!(500, 500)
|
177
|
+
versions[:small] = pipeline.resize_to_limit!(300, 300)
|
178
|
+
end
|
179
|
+
|
180
|
+
versions
|
181
|
+
end
|
182
|
+
end
|
183
|
+
```
|
184
|
+
|
185
|
+
or generate thumbnails on-demand:
|
186
|
+
|
187
|
+
```rb
|
188
|
+
class ImageUploader < Shrine
|
189
|
+
derivation :thumbnail do |file, width, height|
|
190
|
+
ImageProcessing::MiniMagick
|
191
|
+
.source(file)
|
192
|
+
.resize_to_limit!(width.to_i, height.to_i)
|
193
|
+
end
|
194
|
+
end
|
195
|
+
```
|
196
|
+
```rb
|
197
|
+
photo.image.derivation_url(:thumbnail, "600", "400")
|
198
|
+
#=> ".../thumbnail/600/400/eyJpZCI6ImZvbyIsInN0b3JhZ2UiOiJzdG9yZSJ9?signature=..."
|
199
|
+
```
|
200
|
+
|
201
|
+
### Image processing
|
202
|
+
|
203
|
+
Many file attachment libraries, such as CarrierWave, Paperclip, Dragonfly and
|
204
|
+
Refile, implement their own image processing macros. Instead of creating
|
205
|
+
yet another in-house implementation, the **[ImageProcessing]** gem was created.
|
206
|
+
|
207
|
+
Even though the ImageProcessing gem was created for Shrine, it's completely
|
208
|
+
generic and can be used standalone, or in any other file upload library (e.g.
|
209
|
+
Active Storage uses it now as well). It takes care of many details for you,
|
210
|
+
such as [auto orienting] the input image and [sharpening] the thumbnails after
|
211
|
+
they are resized.
|
138
212
|
|
139
213
|
```rb
|
140
214
|
require "image_processing"
|
@@ -147,14 +221,15 @@ thumbnail = ImageProcessing::MiniMagick
|
|
147
221
|
thumbnail #=> #<Tempfile:/var/folders/.../image_processing20180316-18446-1j247h6.png>
|
148
222
|
```
|
149
223
|
|
150
|
-
|
224
|
+
#### libvips
|
151
225
|
|
152
226
|
Probably the biggest ImageProcessing feature is the support for **[libvips]**.
|
153
|
-
libvips is
|
154
|
-
|
155
|
-
memory usage (see [Why is libvips quick]).
|
156
|
-
|
157
|
-
|
227
|
+
libvips is a full-featured image processing library like ImageMagick, with
|
228
|
+
impressive performance characteristics – it's often **multiple times faster**
|
229
|
+
than ImageMagick and has low memory usage (see [Why is libvips quick]).
|
230
|
+
|
231
|
+
The `ImageProcessing::Vips` backend implements the same API as
|
232
|
+
`ImageProcessing::MiniMagick`, so you can easily swap one for the other.
|
158
233
|
|
159
234
|
```rb
|
160
235
|
require "image_processing/mini_magick"
|
@@ -172,28 +247,35 @@ ImageProcessing::Vips.resize_to_fit(800, 800).call(original)
|
|
172
247
|
|
173
248
|
### Other processors
|
174
249
|
|
175
|
-
|
176
|
-
|
177
|
-
|
250
|
+
Both processing "on upload" and "on-the-fly" work in a way that you define a
|
251
|
+
Ruby block, which accepts a source file and is expected to return a processed
|
252
|
+
file. How you're going to do the processing is entirely up to you.
|
253
|
+
|
254
|
+
This allows you to use any tool you want. For example, you could use the
|
255
|
+
[image_optim] gem to perform additional image optimizations:
|
178
256
|
|
179
257
|
```rb
|
180
258
|
class VideoUploader < Shrine
|
181
|
-
|
259
|
+
derivation :thumbnail do |file, width, height|
|
260
|
+
thumbnail = ImageProcessing::MiniMagick
|
261
|
+
.source(file)
|
262
|
+
.resize_to_limit!(width, height)
|
263
|
+
|
264
|
+
image_optim = ImageOptim.new
|
265
|
+
image_optim.optimize_image!(thumbnail.path)
|
182
266
|
|
183
|
-
|
184
|
-
# define your processing
|
267
|
+
thumbnail
|
185
268
|
end
|
186
269
|
end
|
187
270
|
```
|
188
271
|
|
189
|
-
## Metadata
|
272
|
+
## Metadata & Validation
|
190
273
|
|
191
274
|
Shrine automatically [extracts metadata][metadata] from each uploaded file,
|
192
|
-
including
|
275
|
+
including derivatives like image thumbnails, and saves them into the database
|
193
276
|
column. In addition to filename, filesize, and MIME type that are extracted by
|
194
277
|
default, you can also extract [image dimensions][store_dimensions], or your own
|
195
|
-
[custom metadata][add_metadata].
|
196
|
-
[validated][validation].
|
278
|
+
[custom metadata][add_metadata].
|
197
279
|
|
198
280
|
```rb
|
199
281
|
photo.image.metadata #=>
|
@@ -207,61 +289,61 @@ photo.image.metadata #=>
|
|
207
289
|
# }
|
208
290
|
```
|
209
291
|
|
210
|
-
|
292
|
+
For common metadata there are already [validation macros][validation_helpers],
|
293
|
+
but you can also [validate any custom metadata][custom validations].
|
211
294
|
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
295
|
+
```rb
|
296
|
+
class DocumentUploader < Shrine
|
297
|
+
Attacher.validate do
|
298
|
+
# validation macros
|
299
|
+
validate_max_size 10*1024*1024
|
300
|
+
validate_mime_type_inclusion %W[application/pdf]
|
301
|
+
|
302
|
+
# custom validations
|
303
|
+
if get.metadata["page_count"] > 30
|
304
|
+
errors << "has too many pages (max is 30)"
|
305
|
+
end
|
306
|
+
end
|
307
|
+
end
|
308
|
+
```
|
226
309
|
|
227
310
|
## Backgrounding
|
228
311
|
|
229
312
|
In most file upload solutions background processing was an afterthought, which
|
230
313
|
resulted in complex implementations. Shrine was designed with backgrounding
|
231
|
-
feature in mind from day one. It is supported via the
|
232
|
-
and can be used with [any backgrounding
|
233
|
-
|
234
|
-
## Large Files
|
314
|
+
feature in mind from day one. It is supported via the
|
315
|
+
[`backgrounding`][backgrounding] plugin and can be used with [any backgrounding
|
316
|
+
library][backgrounding libraries].
|
235
317
|
|
236
|
-
|
237
|
-
go out of the way to make this as resilient and performant as possible.
|
238
|
-
|
239
|
-
### Streaming
|
318
|
+
## Direct Uploads
|
240
319
|
|
241
|
-
Shrine
|
242
|
-
|
243
|
-
|
320
|
+
Shrine doesn't come with a plug-and-play JavaScript solution for client-side
|
321
|
+
uploads like Refile and Active Storage, but instead it adopts **[Uppy]**. Uppy
|
322
|
+
is a modern JavaScript file upload library, which offers support for uploading
|
323
|
+
to [AWS S3][Uppy AwsS3], to a [custom endpoint][Uppy XHRUpload], or even to a
|
324
|
+
[resumable endpoint][Uppy Tus]. It comes with a set of UI components, ranging
|
325
|
+
from a simple [status bar][Uppy StatusBar] to a full-featured [dashboard][Uppy
|
326
|
+
Dashboard]. Since Uppy is maintained by the wide JavaScript community, it's
|
327
|
+
generally a better choice than any homegrown solution.
|
244
328
|
|
245
|
-
Shrine
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
file, so it's enough to download just the first few kilobytes of the file.
|
329
|
+
Shrine provides Rack components for uploads that integrate nicely with Uppy.
|
330
|
+
So, whether you want Uppy to upload directly [to your app][upload_endpoint], or
|
331
|
+
you want to authorize direct uploads [to the cloud][presign_endpoint], Shrine
|
332
|
+
has it streamlined.
|
250
333
|
|
251
334
|
### Resumable uploads
|
252
335
|
|
253
|
-
|
254
|
-
to
|
255
|
-
|
256
|
-
|
257
|
-
|
336
|
+
If your users are uploading large files, flaky internet connections can cause
|
337
|
+
uploads to fail halfway, which can be a frustrating user experience. To fix
|
338
|
+
this problem, [Transloadit] company has created an open HTTP-based protocol for
|
339
|
+
resumable uploads – **[tus]**. There are already countless client and server
|
340
|
+
[implementations][tus implementations] of the protocol in various languages.
|
258
341
|
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
attach uploaded files using the handy [Shrine integration][shrine-tus].
|
342
|
+
So, if you're expecting large file uploads, you can use Uppy as a [JavaScript
|
343
|
+
client][Uppy Tus] and have it upload to [Ruby server][tus-ruby-server], then
|
344
|
+
attach uploaded files using the handy [Shrine integration][shrine-tus]. Shrine
|
345
|
+
handles uploads and downloads in a streaming fashion, so you can expect low
|
346
|
+
memory usage.
|
265
347
|
|
266
348
|
Alternatively, you can have [resumable multipart uploads directly to
|
267
349
|
S3][uppy-s3_multipart].
|
@@ -282,11 +364,8 @@ than relying on the `Content-Type` request header), preventing exploits like
|
|
282
364
|
|
283
365
|
The `remote_url` plugin requires specifying a `:max_size` option, which limits
|
284
366
|
the maximum allowed size of the remote file. The [Down] gem which the
|
285
|
-
`remote_url` plugin uses will
|
286
|
-
|
287
|
-
chunked responses (where `Content-Length` header is absent) the download will
|
288
|
-
will be terminated as soon as the received content surpasses the specified
|
289
|
-
limit.
|
367
|
+
`remote_url` plugin uses will [terminate the download early][Down max size]
|
368
|
+
when it realizes it's too large.
|
290
369
|
|
291
370
|
[Paperclip]: https://github.com/thoughtbot/paperclip
|
292
371
|
[CarrierWave]: https://github.com/carrierwaveuploader/carrierwave
|
@@ -303,7 +382,8 @@ limit.
|
|
303
382
|
[ROM]: http://rom-rb.org
|
304
383
|
[Hanami::Model]: https://github.com/hanami/model
|
305
384
|
[plugin system]: https://twin.github.io/the-plugin-system-of-sequel-and-roda/
|
306
|
-
[Down]: https://github.com/janko
|
385
|
+
[Down]: https://github.com/janko/down
|
386
|
+
[ContentDisposition]: https://github.com/shrinerb/content_disposition
|
307
387
|
[`file`]: http://linux.die.net/man/1/file
|
308
388
|
[FileMagic]: https://github.com/blackwinter/ruby-filemagic
|
309
389
|
[FastImage]: https://github.com/sdsykes/fastimage
|
@@ -313,36 +393,38 @@ limit.
|
|
313
393
|
[mini_mime]: https://github.com/discourse/mini_mime
|
314
394
|
[MiniMagick]: https://github.com/minimagick/minimagick
|
315
395
|
[ruby-vips]: https://github.com/libvips/ruby-vips
|
316
|
-
[
|
396
|
+
[god objects]: https://en.wikipedia.org/wiki/God_object
|
317
397
|
[ImageMagick]: https://www.imagemagick.org
|
318
398
|
[refile-mini_magick]: https://github.com/refile/refile-mini_magick
|
319
|
-
[ImageProcessing]: https://github.com/janko
|
399
|
+
[ImageProcessing]: https://github.com/janko/image_processing
|
320
400
|
[auto orienting]: https://www.imagemagick.org/script/command-line-options.php#auto-orient
|
321
401
|
[sharpening]: https://photography.tutsplus.com/tutorials/what-is-image-sharpening--cms-26627
|
322
402
|
[libvips]: http://libvips.github.io/libvips/
|
323
403
|
[Why is libvips quick]: https://github.com/libvips/libvips/wiki/Why-is-libvips-quick
|
324
|
-
[metadata]:
|
325
|
-
[store_dimensions]:
|
326
|
-
[add_metadata]:
|
327
|
-
[validation]:
|
328
|
-
[upload_endpoint]:
|
329
|
-
[presign_endpoint]:
|
404
|
+
[metadata]: /doc/metadata.md#readme
|
405
|
+
[store_dimensions]: /doc/plugins/store_dimensions.md#readme
|
406
|
+
[add_metadata]: /doc/plugins/add_metadata.md#readme
|
407
|
+
[validation]: /doc/validation.md#readme
|
408
|
+
[upload_endpoint]: /doc/plugins/upload_endpoint.md#readme
|
409
|
+
[presign_endpoint]: /doc/plugins/presign_endpoint.md#readme
|
330
410
|
[Uppy]: https://uppy.io
|
331
|
-
[XHRUpload]: https://uppy.io/docs/xhrupload/
|
332
|
-
[AwsS3]: https://uppy.io/docs/aws-s3/
|
333
|
-
[Tus]: https://uppy.io/docs/tus/
|
334
|
-
[StatusBar]: https://uppy.io/examples/statusbar/
|
335
|
-
[Dashboard]: https://uppy.io/examples/dashboard/
|
336
|
-
[
|
337
|
-
[backgrounding libraries]: https://github.com/shrinerb/shrine/wiki/Backgrounding-
|
338
|
-
[Down streaming]: https://github.com/janko
|
411
|
+
[Uppy XHRUpload]: https://uppy.io/docs/xhrupload/
|
412
|
+
[Uppy AwsS3]: https://uppy.io/docs/aws-s3/
|
413
|
+
[Uppy Tus]: https://uppy.io/docs/tus/
|
414
|
+
[Uppy StatusBar]: https://uppy.io/examples/statusbar/
|
415
|
+
[Uppy Dashboard]: https://uppy.io/examples/dashboard/
|
416
|
+
[backgrounding]: /doc/plugins/backgrounding.md#readme
|
417
|
+
[backgrounding libraries]: https://github.com/shrinerb/shrine/wiki/Backgrounding-Libraries
|
418
|
+
[Down streaming]: https://github.com/janko/down#streaming
|
339
419
|
[Transloadit]: https://transloadit.com
|
340
420
|
[tus]: https://tus.io
|
341
421
|
[tus implementations]: https://tus.io/implementations.html
|
342
|
-
[tus-
|
343
|
-
[uppy tus]: https://uppy.io/docs/tus/
|
344
|
-
[tus-ruby-server]: https://github.com/janko-m/tus-ruby-server
|
422
|
+
[tus-ruby-server]: https://github.com/janko/tus-ruby-server
|
345
423
|
[shrine-tus]: https://github.com/shrinerb/shrine-tus
|
346
424
|
[ImageTragick]: https://imagetragick.com
|
347
|
-
[uppy-s3_multipart]: https://github.com/janko
|
425
|
+
[uppy-s3_multipart]: https://github.com/janko/uppy-s3_multipart
|
348
426
|
[OWASP]: https://www.owasp.org/index.php/Unrestricted_File_Upload
|
427
|
+
[image_optim]: https://github.com/toy/image_optim
|
428
|
+
[validation_helpers]: /doc/plugins/validation_helpers.md#readme
|
429
|
+
[custom validations]: /doc/validation.md#custom-validations
|
430
|
+
[Down max size]: https://github.com/janko/down#maximum-size
|