refile 0.2.5 → 0.3.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 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: