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.
- checksums.yaml +7 -0
- data/README.md +0 -0
- data/lib/generators/templates/uploader.rb.erb +9 -0
- data/lib/generators/uploader_generator.rb +7 -0
- data/lib/salebot_uploader/compatibility/paperclip.rb +104 -0
- data/lib/salebot_uploader/downloader/base.rb +101 -0
- data/lib/salebot_uploader/downloader/remote_file.rb +68 -0
- data/lib/salebot_uploader/error.rb +8 -0
- data/lib/salebot_uploader/locale/en.yml +17 -0
- data/lib/salebot_uploader/mount.rb +446 -0
- data/lib/salebot_uploader/mounter.rb +255 -0
- data/lib/salebot_uploader/orm/activerecord.rb +68 -0
- data/lib/salebot_uploader/processing/mini_magick.rb +194 -0
- data/lib/salebot_uploader/processing/rmagick.rb +402 -0
- data/lib/salebot_uploader/processing/vips.rb +284 -0
- data/lib/salebot_uploader/processing.rb +3 -0
- data/lib/salebot_uploader/sanitized_file.rb +357 -0
- data/lib/salebot_uploader/storage/abstract.rb +41 -0
- data/lib/salebot_uploader/storage/file.rb +124 -0
- data/lib/salebot_uploader/storage/fog.rb +547 -0
- data/lib/salebot_uploader/storage.rb +3 -0
- data/lib/salebot_uploader/test/matchers.rb +398 -0
- data/lib/salebot_uploader/uploader/cache.rb +223 -0
- data/lib/salebot_uploader/uploader/callbacks.rb +33 -0
- data/lib/salebot_uploader/uploader/configuration.rb +184 -0
- data/lib/salebot_uploader/uploader/content_type_allowlist.rb +61 -0
- data/lib/salebot_uploader/uploader/content_type_denylist.rb +62 -0
- data/lib/salebot_uploader/uploader/default_url.rb +17 -0
- data/lib/salebot_uploader/uploader/dimension.rb +66 -0
- data/lib/salebot_uploader/uploader/download.rb +24 -0
- data/lib/salebot_uploader/uploader/extension_allowlist.rb +63 -0
- data/lib/salebot_uploader/uploader/extension_denylist.rb +64 -0
- data/lib/salebot_uploader/uploader/file_size.rb +43 -0
- data/lib/salebot_uploader/uploader/mountable.rb +44 -0
- data/lib/salebot_uploader/uploader/processing.rb +125 -0
- data/lib/salebot_uploader/uploader/proxy.rb +99 -0
- data/lib/salebot_uploader/uploader/remove.rb +21 -0
- data/lib/salebot_uploader/uploader/serialization.rb +28 -0
- data/lib/salebot_uploader/uploader/store.rb +142 -0
- data/lib/salebot_uploader/uploader/url.rb +44 -0
- data/lib/salebot_uploader/uploader/versions.rb +350 -0
- data/lib/salebot_uploader/uploader.rb +53 -0
- data/lib/salebot_uploader/utilities/file_name.rb +47 -0
- data/lib/salebot_uploader/utilities/uri.rb +26 -0
- data/lib/salebot_uploader/utilities.rb +7 -0
- data/lib/salebot_uploader/validations/active_model.rb +76 -0
- data/lib/salebot_uploader/version.rb +3 -0
- data/lib/salebot_uploader.rb +62 -0
- 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,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"
|