refile 0.2.5 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 66457e73d63efd2eafa3b150152d7615fc4c940a
4
- data.tar.gz: e7e6744055b448670fd328a29fa97d122a332b4a
3
+ metadata.gz: bd423a66e8cd66e1dbb128aad4724806341335ea
4
+ data.tar.gz: 8eef6a2838a39c3d74b87fe03467905ea548145e
5
5
  SHA512:
6
- metadata.gz: bf353ab538163c2716a2114f887855e87b083493186911dc61ca064a971d96b1b72a7282ec3531530da66187964f15243243d8b027655c5a192336e61e0a50c6
7
- data.tar.gz: ebd0c248bdccaa5ad714764df5fc2dcac65922a0148019a9a897fc676bf1cabde9cb74794a7a5620c23d238e2f56f697d8b3ee4ebc163395deaf01ba3ab80fea
6
+ metadata.gz: 7b2b30128fd4f7980a5b3e1b490fa47f17acf38c7bc458b0eec8a117856c90d087606341055ff1af77b9b37138849020a219c9cd59d6640ff30334595d4d6162
7
+ data.tar.gz: 4a01443db981b687b7933463cfa5a1a1837f8e16470cfdbf4d4fb3bf9f609d2334afebbb803c8735cf1836626ef1e3e0ace656ceae19bbce49132fbea873b2af
data/History.md CHANGED
@@ -1,3 +1,9 @@
1
+ # 0.3.0
2
+
3
+ Release date: 2014-12-14
4
+
5
+ - [ADDED] Can upload files via URL
6
+
1
7
  # 0.2.5
2
8
 
3
9
  Release date: 2014-12-12
data/README.md CHANGED
@@ -506,6 +506,34 @@ end
506
506
  Now when you check this checkbox and submit the form, the previously attached
507
507
  file will be removed.
508
508
 
509
+ ## Fetching remote files by URL
510
+
511
+ You might want to give you users the option of uploading a file by its URL.
512
+ This could be either just via a textfield or through some other interface.
513
+ Refile makes it easy to fetch this file and upload it. Just add a field like
514
+ this:
515
+
516
+ ``` erb
517
+ <%= form_for @user do |form| %>
518
+ <%= form.label :profile_image, "Attach image" %>
519
+ <%= form.attachment_field :profile_image %>
520
+
521
+ <%= form.label :remote_profile_image_url, "Or specify URL" %>
522
+ <%= form.text_field :remote_profile_image_url %>
523
+ <% end %>
524
+ ```
525
+
526
+ Then permit this field in your controller:
527
+
528
+ ``` ruby
529
+ def user_params
530
+ params.require(:user).permit(:profile_image, :profile_image_cache_id, :remote_profile_image_url)
531
+ end
532
+ ```
533
+
534
+ Refile will now fetch the file from the given URL, following redirects if
535
+ needed.
536
+
509
537
  ## Cache expiry
510
538
 
511
539
  Files will accumulate in your cache, and you'll probably want to remove them
data/Rakefile CHANGED
@@ -3,6 +3,11 @@ $LOAD_PATH.unshift(File.expand_path("spec", File.dirname(__FILE__)))
3
3
  require "bundler/gem_tasks"
4
4
  require "refile/test_app"
5
5
  require "rspec/core/rake_task"
6
+ require "yard"
7
+
8
+ YARD::Rake::YardocTask.new do |t|
9
+ t.files = ["README.md", "lib/**/*.rb"]
10
+ end
6
11
 
7
12
  RSpec::Core::RakeTask.new(:spec)
8
13
 
@@ -3,3 +3,4 @@ en:
3
3
  errors:
4
4
  messages:
5
5
  too_large: "is too large"
6
+ download_failed: "could not be downloaded"
data/lib/refile.rb CHANGED
@@ -1,47 +1,140 @@
1
1
  require "uri"
2
2
  require "fileutils"
3
3
  require "tempfile"
4
+ require "rest_client"
4
5
 
5
6
  module Refile
6
7
  class Invalid < StandardError; end
7
8
 
8
9
  class << self
9
- attr_accessor :read_chunk_size, :app, :host, :direct_upload
10
- attr_writer :store, :cache
11
10
 
11
+ # The number of bytes to read when files are streamed. Refile
12
+ # uses this in a couple of places where files should be streamed
13
+ # in a memory efficient way instead of reading the entire file into
14
+ # memory at once. The default value of this is `3000`.
15
+ #
16
+ # @return [Fixnum]
17
+ attr_accessor :read_chunk_size
18
+
19
+ # A shortcut to the instance of the Rack application. This should be
20
+ # set when the application is initialized. `refile/rails` sets this
21
+ # value.
22
+ #
23
+ # @return [Refile::App, nil]
24
+ attr_accessor :app
25
+
26
+ # The host name that the Rack application can be reached at. If not set,
27
+ # Refile will use an absolute URL without hostname. It is strongly
28
+ # recommended to run Refile behind a CDN and to set this to the hostname of
29
+ # the CDN distribution. A protocol relative URL is recommended for this
30
+ # value.
31
+ #
32
+ # @return [String, nil]
33
+ attr_accessor :host
34
+
35
+ # A list of names which identify backends in the global backend registry.
36
+ # The Rack application allows POST requests to only the backends specified
37
+ # in this config option. This defaults to `["cache"]`, only allowing direct
38
+ # uploads to the cache backend.
39
+ #
40
+ # @return [Array[String]]
41
+ attr_accessor :direct_upload
42
+
43
+ # A global registry of backends.
44
+ #
45
+ # @return [Hash{String => Backend}]
12
46
  def backends
13
47
  @backends ||= {}
14
48
  end
15
49
 
50
+ # A global registry of processors. These will be used by the Rack
51
+ # application to manipulate files prior to serving them up to the user,
52
+ # based on options sent trough the URL. This can be used for example to
53
+ # resize images or to convert files to another file format.
54
+ #
55
+ # @return [Hash{String => Proc}]
16
56
  def processors
17
57
  @processors ||= {}
18
58
  end
19
59
 
60
+ # Adds a processor. The processor must respond to `call`, both receiving
61
+ # and returning an IO-like object. Alternatively a block can be given to
62
+ # this method which also receives and returns an IO-like object.
63
+ #
64
+ # An IO-like object is recommended to be an instance of the `IO` class or
65
+ # one of its subclasses, like `File` or a `StringIO`, or a `Refile::File`.
66
+ # It can also be any other object which responds to `size`, `read`, `eof`?
67
+ # and `close` and mimics the behaviour of IO objects for these methods.
68
+ #
69
+ # @example With processor class
70
+ # class Reverse
71
+ # def call(file)
72
+ # StringIO.new(file.read.reverse)
73
+ # en
74
+ # end
75
+ # Refile.processor(:reverse, Reverse)
76
+ #
77
+ # @example With block
78
+ # Refile.processor(:reverse) do |file|
79
+ # StringIO.new(file.read.reverse)
80
+ # end
81
+ #
82
+ # @param [#to_s] name The name of the processor
83
+ # @param [Proc, nil] processor The processor, must respond to `call` and.
84
+ # @yield [Refile::File] The file to modify
85
+ # @yieldreturn [IO] An IO-like object representing the processed file
20
86
  def processor(name, processor = nil, &block)
21
87
  processor ||= block
22
88
  processors[name.to_s] = processor
23
89
  end
24
90
 
91
+ # A shortcut to retrieving the backend named "store" from the global
92
+ # registry.
93
+ #
94
+ # @return [Backend]
25
95
  def store
26
96
  backends["store"]
27
97
  end
28
98
 
99
+ # A shortcut to setting the backend named "store" in the global registry.
100
+ #
101
+ # @param [Backend] backend
29
102
  def store=(backend)
30
103
  backends["store"] = backend
31
104
  end
32
105
 
106
+ # A shortcut to retrieving the backend named "cache" from the global
107
+ # registry.
108
+ #
109
+ # @return [Backend]
33
110
  def cache
34
111
  backends["cache"]
35
112
  end
36
113
 
114
+ # A shortcut to setting the backend named "cache" in the global registry.
115
+ #
116
+ # @param [Backend] backend
37
117
  def cache=(backend)
38
118
  backends["cache"] = backend
39
119
  end
40
120
 
121
+ # Yield the Refile module as a convenience for configuring multiple
122
+ # config options at once.
123
+ #
124
+ # @yield Refile
41
125
  def configure
42
126
  yield self
43
127
  end
44
128
 
129
+ # Verify that the given uploadable is indeed a valid uploadable. This
130
+ # method is used by backends as a sanity check, you should not have to use
131
+ # this method unless you are writing a backend.
132
+ #
133
+ # @param [IO] uploadable The uploadable object to verify
134
+ # @param [Fixnum] max_size The maximum size of the uploadable object
135
+ # @raise [ArgumentError] If the uploadable is not an IO-like object
136
+ # @raise [Refile::Invalid] If the uploadable's size is too large
137
+ # @return [true] Always returns true if it doesn't raise
45
138
  def verify_uploadable(uploadable, max_size)
46
139
  [:size, :read, :eof?, :close].each do |m|
47
140
  unless uploadable.respond_to?(m)
data/lib/refile/app.rb CHANGED
@@ -9,6 +9,7 @@ module Refile
9
9
  @allow_origin = allow_origin
10
10
  end
11
11
 
12
+ # @api private
12
13
  class Proxy
13
14
  def initialize(peek, file)
14
15
  @peek = peek
@@ -1,9 +1,8 @@
1
1
  module Refile
2
2
  module Attachment
3
- IMAGE_TYPES = %w[jpg jpeg gif png]
4
-
3
+ # @api private
5
4
  class Attachment
6
- attr_reader :record, :name, :cache, :store, :cache_id, :options
5
+ attr_reader :record, :name, :cache, :store, :cache_id, :options, :errors
7
6
  attr_accessor :remove
8
7
 
9
8
  def initialize(record, name, **options)
@@ -24,7 +23,7 @@ module Refile
24
23
  end
25
24
 
26
25
  def file
27
- if cache_id and not cache_id == ""
26
+ if cached?
28
27
  cache.get(cache_id)
29
28
  elsif id and not id == ""
30
29
  store.get(id)
@@ -40,6 +39,16 @@ module Refile
40
39
  raise if @options[:raise_errors]
41
40
  end
42
41
 
42
+ def download(url)
43
+ if url and not url == ""
44
+ raw_response = RestClient::Request.new(method: :get, url: url, raw_response: true).execute
45
+ self.file = raw_response.file
46
+ end
47
+ rescue RestClient::Exception
48
+ @errors = [:download_failed]
49
+ raise if @options[:raise_errors]
50
+ end
51
+
43
52
  def cache_id=(id)
44
53
  @cache_id = id unless @cache_file
45
54
  end
@@ -65,20 +74,28 @@ module Refile
65
74
  end
66
75
 
67
76
  def remove?
68
- remove.present? and remove !~ /\A0|false$\z/
77
+ remove and remove != "" and remove !~ /\A0|false$\z/
69
78
  end
70
79
 
71
- def errors
72
- @errors
73
- end
74
-
75
- private
80
+ private
76
81
 
77
82
  def cached?
78
83
  cache_id and not cache_id == ""
79
84
  end
80
85
  end
81
86
 
87
+ # Macro which generates accessors for the given column which make it
88
+ # possible to upload and retrieve previously uploaded files through the
89
+ # generated accessors.
90
+ #
91
+ # The +raise_errors+ option controls whether assigning an invalid file
92
+ # should immediately raise an error, or save the error and defer handling
93
+ # it until later.
94
+ #
95
+ # @param [String] name Name of the column which accessor are generated for
96
+ # @param [#to_s] cache Name of a backend in +Refile.backends+ to use as transient cache
97
+ # @param [#to_s] store Name of a backend in +Refile.backends+ to use as permanent store
98
+ # @param [true, false] raise_errors Whether to raise errors in case an invalid file is assigned
82
99
  def attachment(name, cache: :cache, store: :store, raise_errors: true)
83
100
  attachment = :"#{name}_attachment"
84
101
 
@@ -112,6 +129,13 @@ module Refile
112
129
  define_method "remove_#{name}" do
113
130
  send(attachment).remove
114
131
  end
132
+
133
+ define_method "remote_#{name}_url=" do |url|
134
+ send(attachment).download(url)
135
+ end
136
+
137
+ define_method "remote_#{name}_url" do
138
+ end
115
139
  end
116
140
  end
117
141
  end
@@ -3,6 +3,9 @@ module Refile
3
3
  module Attachment
4
4
  include Refile::Attachment
5
5
 
6
+ # Attachment method which hooks into ActiveRecord models
7
+ #
8
+ # @see Refile::Attachment#attachment
6
9
  def attachment(name, cache: :cache, store: :store, raise_errors: false)
7
10
  super
8
11
 
@@ -2,7 +2,10 @@ require "aws-sdk"
2
2
 
3
3
  module Refile
4
4
  module Backend
5
+
6
+ # A refile backend which stores files in Amazon S3
5
7
  class S3
8
+
6
9
  # Emulates an IO-object like interface on top of S3Object#read. To avoid
7
10
  # memory allocations and unnecessary complexity, this treats the `length`
8
11
  # parameter to read as a boolean flag instead. If given, it will read the
data/lib/refile/rails.rb CHANGED
@@ -2,17 +2,6 @@ require "refile"
2
2
  require "refile/rails/attachment_helper"
3
3
 
4
4
  module Refile
5
- module Controller
6
- def show
7
- file = Refile.backends.fetch(params[:backend_name]).get(params[:id])
8
-
9
- options = { disposition: "inline" }
10
- options[:type] = Mime::Type.lookup_by_extension(params[:format]).to_s if params[:format]
11
-
12
- send_data file.read, options
13
- end
14
- end
15
-
16
5
  module AttachmentFieldHelper
17
6
  def attachment_field(method, options = {})
18
7
  self.multipart = true
@@ -2,6 +2,7 @@ module Refile
2
2
  module AttachmentHelper
3
3
  def attachment_url(record, name, *args, filename: nil, format: nil)
4
4
  file = record.send(name)
5
+ return unless file
5
6
 
6
7
  filename ||= name.to_s
7
8
 
@@ -1,4 +1,9 @@
1
+ # A file hasher which ignores the file contents and always returns a random string.
1
2
  class Refile::RandomHasher
3
+
4
+ # Generate a random string
5
+ #
6
+ # @return [String]
2
7
  def hash(uploadable=nil)
3
8
  SecureRandom.hex(30)
4
9
  end
@@ -1,3 +1,3 @@
1
1
  module Refile
2
- VERSION = "0.2.5"
2
+ VERSION = "0.3.0"
3
3
  end
data/refile.gemspec CHANGED
@@ -18,7 +18,9 @@ Gem::Specification.new do |spec|
18
18
  spec.require_paths = ["lib", "spec"]
19
19
 
20
20
  spec.required_ruby_version = ">= 2.1.0"
21
+ spec.add_dependency "rest-client", "~> 1.7.2"
21
22
 
23
+ spec.add_development_dependency "webmock", "~> 1.20.4"
22
24
  spec.add_development_dependency "bundler", "~> 1.6"
23
25
  spec.add_development_dependency "rake"
24
26
  spec.add_development_dependency "rspec", "~> 3.0"
@@ -31,4 +33,5 @@ Gem::Specification.new do |spec|
31
33
  spec.add_development_dependency "rails", "~> 4.1.8"
32
34
  spec.add_development_dependency "sqlite3"
33
35
  spec.add_development_dependency "selenium-webdriver"
36
+ spec.add_development_dependency "yard"
34
37
  end
@@ -31,6 +31,64 @@ describe Refile::Attachment do
31
31
  end
32
32
  end
33
33
 
34
+ describe "remote_:name_url=" do
35
+ it "does nothign when nil is assigned" do
36
+ instance.remote_document_url = nil
37
+ expect(instance.document).to be_nil
38
+ end
39
+
40
+ it "does nothign when empty string is assigned" do
41
+ instance.remote_document_url = nil
42
+ expect(instance.document).to be_nil
43
+ end
44
+
45
+ context "without redirects" do
46
+ before(:each) do
47
+ stub_request(:get, "http://www.example.com/some_file").to_return(status: 200, body: "abc", headers: { "Content-Length" => 3 })
48
+ end
49
+
50
+ it "downloads file, caches it and sets the _id parameter" do
51
+ instance.remote_document_url = "http://www.example.com/some_file"
52
+ expect(Refile.cache.get(instance.document.id).read).to eq("abc")
53
+ expect(Refile.cache.get(instance.document_cache_id).read).to eq("abc")
54
+ end
55
+ end
56
+
57
+ context "with redirects" do
58
+ before(:each) do
59
+ stub_request(:get, "http://www.example.com/1").to_return(status: 302, headers: { "Location" => "http://www.example.com/2" })
60
+ stub_request(:get, "http://www.example.com/2").to_return(status: 200, body: "woop", headers: { "Content-Length" => 4 })
61
+ stub_request(:get, "http://www.example.com/loop").to_return(status: 302, headers: { "Location" => "http://www.example.com/loop" })
62
+ end
63
+
64
+ it "follows redirects and fetches the file, caches it and sets the _id parameter" do
65
+ instance.remote_document_url = "http://www.example.com/1"
66
+ expect(Refile.cache.get(instance.document.id).read).to eq("woop")
67
+ expect(Refile.cache.get(instance.document_cache_id).read).to eq("woop")
68
+ end
69
+
70
+ context "when errors enabled" do
71
+ let(:options) { { raise_errors: true } }
72
+ it "handles redirect loops by trowing errors" do
73
+ expect do
74
+ instance.remote_document_url = "http://www.example.com/loop"
75
+ end.to raise_error(RestClient::MaxRedirectsReached)
76
+ end
77
+ end
78
+
79
+ context "when errors disabled" do
80
+ let(:options) { { raise_errors: false } }
81
+ it "handles redirect loops by setting generic download error" do
82
+ expect do
83
+ instance.remote_document_url = "http://www.example.com/loop"
84
+ end.not_to raise_error
85
+ expect(instance.document_attachment.errors).to eq([:download_failed])
86
+ expect(instance.document).to be_nil
87
+ end
88
+ end
89
+ end
90
+ end
91
+
34
92
  describe ":name_cache_id" do
35
93
  it "doesn't overwrite a cached file" do
36
94
  instance.document = Refile::FileDouble.new("hello")
@@ -60,4 +60,17 @@ feature "Normal HTTP Post file uploads" do
60
60
  expect(page).to have_selector("h1", text: "A cool post")
61
61
  expect(page).to_not have_selector(:link, "Document")
62
62
  end
63
+
64
+ scenario "Upload a file from a remote URL" do
65
+ stub_request(:get, "http://www.example.com/some_file").to_return(status: 200, body: "abc", headers: { "Content-Length" => 3 })
66
+
67
+ visit "/normal/posts/new"
68
+ fill_in "Title", with: "A cool post"
69
+ fill_in "Remote document url", with: "http://www.example.com/some_file"
70
+ click_button "Create"
71
+
72
+ expect(page).to have_selector("h1", text: "A cool post")
73
+ click_link("Document")
74
+ expect(page.source.chomp).to eq("abc")
75
+ end
63
76
  end
@@ -1,5 +1,6 @@
1
1
  require "refile"
2
2
  require "refile/backend_examples"
3
+ require "webmock/rspec"
3
4
 
4
5
  tmp_path = Dir.mktmpdir
5
6
 
@@ -15,13 +16,13 @@ class FakePresignBackend < Refile::Backend::FileSystem
15
16
 
16
17
  def presign
17
18
  id = Refile::RandomHasher.new.hash
18
- Signature.new("file", id, "/presigned/posts/upload", { token: "xyz123", id: id })
19
+ Signature.new("file", id, "/presigned/posts/upload", token: "xyz123", id: id)
19
20
  end
20
21
  end
21
22
 
22
23
  Refile.backends["limited_cache"] = FakePresignBackend.new(File.expand_path("default_cache", tmp_path), max_size: 100)
23
24
 
24
- Refile.direct_upload = ["cache", "limited_cache"]
25
+ Refile.direct_upload = %w(cache limited_cache)
25
26
 
26
27
  Refile.processor(:reverse) do |file|
27
28
  StringIO.new(file.read.reverse)
@@ -48,7 +49,6 @@ Refile.processor(:convert_case) do |file, format:|
48
49
  end
49
50
  end
50
51
 
51
-
52
52
  class Refile::FileDouble
53
53
  def initialize(data)
54
54
  @io = StringIO.new(data)
@@ -79,5 +79,7 @@ end
79
79
 
80
80
  RSpec.configure do |config|
81
81
  config.include PathHelper
82
+ config.before(:all) do
83
+ WebMock.disable_net_connect!(allow_localhost: true)
84
+ end
82
85
  end
83
-
@@ -34,6 +34,6 @@ class NormalPostsController < ApplicationController
34
34
  private
35
35
 
36
36
  def post_params
37
- params.require(:post).permit(:title, :image, :image_cache_id, :document, :document_cache_id, :remove_document)
37
+ params.require(:post).permit(:title, :image, :image_cache_id, :document, :document_cache_id, :remove_document, :remote_document_url)
38
38
  end
39
39
  end
@@ -18,6 +18,10 @@
18
18
  <%= form.label :remove_document %>
19
19
  <%= form.check_box :remove_document %>
20
20
  </p>
21
+ <p>
22
+ <%= form.label :remote_document_url %>
23
+ <%= form.text_field :remote_document_url %>
24
+ </p>
21
25
  <p>
22
26
  <%= form.submit %>
23
27
  </p>
metadata CHANGED
@@ -1,15 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: refile
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.5
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jonas Nicklas
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-12-12 00:00:00.000000000 Z
11
+ date: 2014-12-14 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rest-client
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 1.7.2
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 1.7.2
27
+ - !ruby/object:Gem::Dependency
28
+ name: webmock
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 1.20.4
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 1.20.4
13
41
  - !ruby/object:Gem::Dependency
14
42
  name: bundler
15
43
  requirement: !ruby/object:Gem::Requirement
@@ -178,6 +206,20 @@ dependencies:
178
206
  - - ">="
179
207
  - !ruby/object:Gem::Version
180
208
  version: '0'
209
+ - !ruby/object:Gem::Dependency
210
+ name: yard
211
+ requirement: !ruby/object:Gem::Requirement
212
+ requirements:
213
+ - - ">="
214
+ - !ruby/object:Gem::Version
215
+ version: '0'
216
+ type: :development
217
+ prerelease: false
218
+ version_requirements: !ruby/object:Gem::Requirement
219
+ requirements:
220
+ - - ">="
221
+ - !ruby/object:Gem::Version
222
+ version: '0'
181
223
  description:
182
224
  email:
183
225
  - jonas.nicklas@gmail.com
@@ -298,3 +340,4 @@ test_files:
298
340
  - spec/refile/test_app/config/routes.rb
299
341
  - spec/refile/test_app/public/favicon.ico
300
342
  - spec/refile_spec.rb
343
+ has_rdoc: