salebot_uploader 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +0 -0
  3. data/lib/generators/templates/uploader.rb.erb +9 -0
  4. data/lib/generators/uploader_generator.rb +7 -0
  5. data/lib/salebot_uploader/compatibility/paperclip.rb +104 -0
  6. data/lib/salebot_uploader/downloader/base.rb +101 -0
  7. data/lib/salebot_uploader/downloader/remote_file.rb +68 -0
  8. data/lib/salebot_uploader/error.rb +8 -0
  9. data/lib/salebot_uploader/locale/en.yml +17 -0
  10. data/lib/salebot_uploader/mount.rb +446 -0
  11. data/lib/salebot_uploader/mounter.rb +255 -0
  12. data/lib/salebot_uploader/orm/activerecord.rb +68 -0
  13. data/lib/salebot_uploader/processing/mini_magick.rb +194 -0
  14. data/lib/salebot_uploader/processing/rmagick.rb +402 -0
  15. data/lib/salebot_uploader/processing/vips.rb +284 -0
  16. data/lib/salebot_uploader/processing.rb +3 -0
  17. data/lib/salebot_uploader/sanitized_file.rb +357 -0
  18. data/lib/salebot_uploader/storage/abstract.rb +41 -0
  19. data/lib/salebot_uploader/storage/file.rb +124 -0
  20. data/lib/salebot_uploader/storage/fog.rb +547 -0
  21. data/lib/salebot_uploader/storage.rb +3 -0
  22. data/lib/salebot_uploader/test/matchers.rb +398 -0
  23. data/lib/salebot_uploader/uploader/cache.rb +223 -0
  24. data/lib/salebot_uploader/uploader/callbacks.rb +33 -0
  25. data/lib/salebot_uploader/uploader/configuration.rb +184 -0
  26. data/lib/salebot_uploader/uploader/content_type_allowlist.rb +61 -0
  27. data/lib/salebot_uploader/uploader/content_type_denylist.rb +62 -0
  28. data/lib/salebot_uploader/uploader/default_url.rb +17 -0
  29. data/lib/salebot_uploader/uploader/dimension.rb +66 -0
  30. data/lib/salebot_uploader/uploader/download.rb +24 -0
  31. data/lib/salebot_uploader/uploader/extension_allowlist.rb +63 -0
  32. data/lib/salebot_uploader/uploader/extension_denylist.rb +64 -0
  33. data/lib/salebot_uploader/uploader/file_size.rb +43 -0
  34. data/lib/salebot_uploader/uploader/mountable.rb +44 -0
  35. data/lib/salebot_uploader/uploader/processing.rb +125 -0
  36. data/lib/salebot_uploader/uploader/proxy.rb +99 -0
  37. data/lib/salebot_uploader/uploader/remove.rb +21 -0
  38. data/lib/salebot_uploader/uploader/serialization.rb +28 -0
  39. data/lib/salebot_uploader/uploader/store.rb +142 -0
  40. data/lib/salebot_uploader/uploader/url.rb +44 -0
  41. data/lib/salebot_uploader/uploader/versions.rb +350 -0
  42. data/lib/salebot_uploader/uploader.rb +53 -0
  43. data/lib/salebot_uploader/utilities/file_name.rb +47 -0
  44. data/lib/salebot_uploader/utilities/uri.rb +26 -0
  45. data/lib/salebot_uploader/utilities.rb +7 -0
  46. data/lib/salebot_uploader/validations/active_model.rb +76 -0
  47. data/lib/salebot_uploader/version.rb +3 -0
  48. data/lib/salebot_uploader.rb +62 -0
  49. metadata +392 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 2f8f0cc7d88364cc565369c94e3d803fa1fa07698e6f387bb4c032bfb83eccd2
4
+ data.tar.gz: 3ba9ec518e3cacac6a77f371b1cdcf3fe6c8410f0625c6b6f1289b6c786b9f77
5
+ SHA512:
6
+ metadata.gz: 6f4f66bff7588946e9ecffa6793d33ce19f898d138c5a40827691c59d414c7dee4a951d8e61d363aba2c192a17a3dd515adb3557dcdc6f358df522e0f45821b9
7
+ data.tar.gz: ed89430e4376646d1351dd9f5ad5b8731231bdf0016001c2ade8c9e07e0539cc4d5bd2a89cbec4277975bf0533a30b8c376f4c656ce6bad87ec4f3423a26320f
data/README.md ADDED
File without changes
@@ -0,0 +1,9 @@
1
+ class <%= class_name %>Uploader < SalebotUploader::Uploader::Base
2
+
3
+ storage :file
4
+ # Переопределить каталог, в котором будут храниться загруженные файлы.
5
+ # Это разумное значение по умолчанию для загрузчиков, которые предназначены для монтирования:
6
+ def store_dir
7
+ "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
8
+ end
9
+ end
@@ -0,0 +1,7 @@
1
+ class UploaderGenerator < Rails::Generators::NamedBase
2
+ source_root File.expand_path('templates', __dir__)
3
+
4
+ def create_uploader_file
5
+ template "uploader.rb.erb", File.join('app/uploaders', class_path, "#{file_name}_uploader.rb")
6
+ end
7
+ end
@@ -0,0 +1,104 @@
1
+ module SalebotUploader
2
+ module Compatibility
3
+
4
+ ##
5
+ # Mix this module into an Uploader to make it mimic Paperclip's storage paths
6
+ # This will make your Uploader use the same default storage path as paperclip
7
+ # does. If you need to override it, you can override the +paperclip_path+ method
8
+ # and provide a Paperclip style path:
9
+ #
10
+ # class MyUploader < SalebotUploader::Uploader::Base
11
+ # include SalebotUploader::Compatibility::Paperclip
12
+ #
13
+ # def paperclip_path
14
+ # ":rails_root/public/uploads/:id/:attachment/:style_:basename.:extension"
15
+ # end
16
+ # end
17
+ #
18
+ # ---
19
+ #
20
+ # This file contains code taken from Paperclip
21
+ #
22
+ # LICENSE
23
+ #
24
+ # The MIT License
25
+ #
26
+ # Copyright (c) 2008 Jon Yurek and thoughtbot, inc.
27
+ #
28
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
29
+ # of this software and associated documentation files (the "Software"), to deal
30
+ # in the Software without restriction, including without limitation the rights
31
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
32
+ # copies of the Software, and to permit persons to whom the Software is
33
+ # furnished to do so, subject to the following conditions:
34
+ #
35
+ # The above copyright notice and this permission notice shall be included in
36
+ # all copies or substantial portions of the Software.
37
+ #
38
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
39
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
40
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
41
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
42
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
43
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
44
+ # THE SOFTWARE.
45
+ #
46
+ module Paperclip
47
+ extend ActiveSupport::Concern
48
+
49
+ DEFAULT_MAPPINGS = {
50
+ :rails_root => lambda{|u, f| Rails.root.to_s },
51
+ :rails_env => lambda{|u, f| Rails.env },
52
+ :id_partition => lambda{|u, f| ('%09d' % u.model.id).scan(/\d{3}/).join('/')},
53
+ :id => lambda{|u, f| u.model.id },
54
+ :attachment => lambda{|u, f| u.mounted_as.to_s.downcase.pluralize },
55
+ :style => lambda{|u, f| u.paperclip_style },
56
+ :basename => lambda{|u, f| u.filename.gsub(/#{File.extname(u.filename)}$/, '') },
57
+ :extension => lambda{|u, d| File.extname(u.filename).gsub(/^\.+/, '')},
58
+ :class => lambda{|u, f| u.model.class.name.underscore.pluralize}
59
+ }
60
+
61
+ included do
62
+ attr_accessor :filename
63
+
64
+ class_attribute :mappings
65
+ self.mappings ||= DEFAULT_MAPPINGS.dup
66
+ end
67
+
68
+ def store_path(for_file=filename)
69
+ path = paperclip_path
70
+ self.filename = for_file
71
+ path ||= File.join(*[store_dir, paperclip_style.to_s, for_file].compact)
72
+ interpolate_paperclip_path(path)
73
+ end
74
+
75
+ def store_dir
76
+ ':rails_root/public/system/:attachment/:id'
77
+ end
78
+
79
+ def paperclip_default_style
80
+ :original
81
+ end
82
+
83
+ def paperclip_path; end
84
+
85
+ def paperclip_style
86
+ version_name || paperclip_default_style
87
+ end
88
+
89
+ module ClassMethods
90
+ def interpolate(sym, &block)
91
+ mappings[sym] = block
92
+ end
93
+ end
94
+
95
+ private
96
+
97
+ def interpolate_paperclip_path(path)
98
+ mappings.each_pair.inject(path) do |agg, pair|
99
+ agg.gsub(":#{pair[0]}") { pair[1].call(self, self.paperclip_style).to_s }
100
+ end
101
+ end
102
+ end # Paperclip
103
+ end # Compatibility
104
+ end # SalebotUploader
@@ -0,0 +1,101 @@
1
+ require 'open-uri'
2
+ require 'ssrf_filter'
3
+ require 'addressable'
4
+ require 'salebot_uploader/downloader/remote_file'
5
+
6
+ module SalebotUploader
7
+ module Downloader
8
+ class Base
9
+ include SalebotUploader::Utilities::Uri
10
+
11
+ attr_reader :uploader
12
+
13
+ def initialize(uploader)
14
+ @uploader = uploader
15
+ end
16
+
17
+ ##
18
+ # Downloads a file from given URL and returns a RemoteFile.
19
+ #
20
+ # === Parameters
21
+ #
22
+ # [url (String)] The URL where the remote file is stored
23
+ # [remote_headers (Hash)] Request headers
24
+ #
25
+ def download(url, remote_headers = {})
26
+ @current_download_retry_count = 0
27
+ headers = remote_headers.
28
+ reverse_merge('User-Agent' => "SalebotUploader/#{SalebotUploader::VERSION}")
29
+ uri = process_uri(url.to_s)
30
+ begin
31
+ if skip_ssrf_protection?(uri)
32
+ response = OpenURI.open_uri(process_uri(url.to_s), headers)
33
+ else
34
+ request = nil
35
+ if ::SsrfFilter::VERSION.to_f < 1.1
36
+ response = SsrfFilter.get(uri, headers: headers) do |req|
37
+ request = req
38
+ end
39
+ else
40
+ response = SsrfFilter.get(uri, headers: headers, request_proc: ->(req) { request = req }) do |res|
41
+ res.body # ensure to read body
42
+ end
43
+ end
44
+ response.uri = request.uri
45
+ response.value
46
+ end
47
+ rescue StandardError => e
48
+ if @current_download_retry_count < @uploader.download_retry_count
49
+ @current_download_retry_count += 1
50
+ sleep @uploader.download_retry_wait_time
51
+ retry
52
+ else
53
+ raise SalebotUploader::DownloadError, "could not download file: #{e.message}"
54
+ end
55
+ end
56
+ SalebotUploader::Downloader::RemoteFile.new(response)
57
+ end
58
+
59
+ ##
60
+ # Processes the given URL by parsing it, and escaping if necessary. Public to allow overriding.
61
+ #
62
+ # === Parameters
63
+ #
64
+ # [url (String)] The URL where the remote file is stored
65
+ #
66
+ def process_uri(source)
67
+ uri = Addressable::URI.parse(source)
68
+ uri.host = uri.normalized_host
69
+ # Perform decode first, as the path is likely to be already encoded
70
+ uri.path = encode_path(decode_uri(uri.path)) if uri.path =~ SalebotUploader::Utilities::Uri::PATH_UNSAFE
71
+ uri.query = encode_non_ascii(uri.query) if uri.query
72
+ uri.fragment = encode_non_ascii(uri.fragment) if uri.fragment
73
+ URI.parse(uri.to_s)
74
+ rescue URI::InvalidURIError, Addressable::URI::InvalidURIError
75
+ raise SalebotUploader::DownloadError, "couldn't parse URL: #{source}"
76
+ end
77
+
78
+ ##
79
+ # If this returns true, SSRF protection will be bypassed.
80
+ # You can override this if you want to allow accessing specific local URIs that are not SSRF exploitable.
81
+ #
82
+ # === Parameters
83
+ #
84
+ # [uri (URI)] The URI where the remote file is stored
85
+ #
86
+ # === Examples
87
+ #
88
+ # class SalebotUploader::Downloader::CustomDownloader < SalebotUploader::Downloader::Base
89
+ # def skip_ssrf_protection?(uri)
90
+ # uri.hostname == 'localhost' && uri.port == 80
91
+ # end
92
+ # end
93
+ #
94
+ # my_uploader.downloader = SalebotUploader::Downloader::CustomDownloader
95
+ #
96
+ def skip_ssrf_protection?(uri)
97
+ @uploader.skip_ssrf_protection
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,68 @@
1
+ module SalebotUploader
2
+ module Downloader
3
+ class RemoteFile
4
+ attr_reader :file, :uri
5
+
6
+ def initialize(file)
7
+ case file
8
+ when String
9
+ @file = StringIO.new(file)
10
+ when Net::HTTPResponse
11
+ body = file.body
12
+ raise SalebotUploader::DownloadError, 'could not download file: No Content' if body.nil?
13
+
14
+ @file = StringIO.new(body)
15
+ @content_type = file.content_type
16
+ @headers = file
17
+ @uri = file.uri
18
+ else
19
+ @file = file
20
+ @content_type = file.content_type
21
+ @headers = file.meta
22
+ @uri = file.base_uri
23
+ end
24
+ end
25
+
26
+ def content_type
27
+ @content_type || 'application/octet-stream'
28
+ end
29
+
30
+ def headers
31
+ @headers || {}
32
+ end
33
+
34
+ def original_filename
35
+ filename = filename_from_header || filename_from_uri
36
+ mime_type = Marcel::TYPES[content_type]
37
+ unless File.extname(filename).present? || mime_type.blank?
38
+ extension = mime_type[0].first
39
+ filename = "#{filename}.#{extension}"
40
+ end
41
+ filename
42
+ end
43
+
44
+ private
45
+
46
+ def filename_from_header
47
+ return nil unless headers['content-disposition']
48
+
49
+ match = headers['content-disposition'].match(/filename=(?:"([^"]+)"|([^";]+))/)
50
+ return nil unless match
51
+
52
+ match[1].presence || match[2].presence
53
+ end
54
+
55
+ def filename_from_uri
56
+ CGI.unescape(File.basename(uri.path))
57
+ end
58
+
59
+ def method_missing(*args, &block)
60
+ file.send(*args, &block)
61
+ end
62
+
63
+ def respond_to_missing?(*args)
64
+ super || file.respond_to?(*args)
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,8 @@
1
+ module SalebotUploader
2
+ class UploadError < StandardError; end
3
+ class IntegrityError < UploadError; end
4
+ class InvalidParameter < UploadError; end
5
+ class ProcessingError < UploadError; end
6
+ class DownloadError < UploadError; end
7
+ class UnknownStorageError < StandardError; end
8
+ end
@@ -0,0 +1,17 @@
1
+ en:
2
+ errors:
3
+ messages:
4
+ salebot_uploader_processing_error: failed to be processed
5
+ salebot_uploader_integrity_error: is not of an allowed file type
6
+ salebot_uploader_download_error: could not be downloaded
7
+ extension_allowlist_error: "You are not allowed to upload %{extension} files, allowed types: %{allowed_types}"
8
+ extension_denylist_error: "You are not allowed to upload %{extension} files, prohibited types: %{prohibited_types}"
9
+ content_type_allowlist_error: "You are not allowed to upload %{content_type} files, allowed types: %{allowed_types}"
10
+ content_type_denylist_error: "You are not allowed to upload %{content_type} files"
11
+ processing_error: "Failed to manipulate, maybe it is not an image?"
12
+ min_size_error: "File size should be greater than %{min_size}"
13
+ max_size_error: "File size should be less than %{max_size}"
14
+ min_width_error: "Image width should be greater than %{min_width}px"
15
+ max_width_error: "Image width should be less than %{max_width}px"
16
+ min_height_error: "Image height should be greater than %{min_height}px"
17
+ max_height_error: "Image height should be less than %{max_height}px"