salebot_uploader 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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"
|