refile 0.2.2

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 (55) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +27 -0
  3. data/.rspec +1 -0
  4. data/.travis.yml +8 -0
  5. data/Gemfile +3 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +476 -0
  8. data/Rakefile +11 -0
  9. data/app/assets/javascripts/refile.js +50 -0
  10. data/app/helpers/attachment_helper.rb +52 -0
  11. data/config.ru +8 -0
  12. data/config/locales/en.yml +5 -0
  13. data/config/routes.rb +3 -0
  14. data/lib/refile.rb +72 -0
  15. data/lib/refile/app.rb +97 -0
  16. data/lib/refile/attachment.rb +89 -0
  17. data/lib/refile/attachment/active_record.rb +24 -0
  18. data/lib/refile/backend/file_system.rb +70 -0
  19. data/lib/refile/backend/s3.rb +129 -0
  20. data/lib/refile/file.rb +65 -0
  21. data/lib/refile/image_processing.rb +73 -0
  22. data/lib/refile/rails.rb +36 -0
  23. data/lib/refile/random_hasher.rb +5 -0
  24. data/lib/refile/version.rb +3 -0
  25. data/refile.gemspec +34 -0
  26. data/spec/refile/app_spec.rb +151 -0
  27. data/spec/refile/attachment_spec.rb +141 -0
  28. data/spec/refile/backend/file_system_spec.rb +30 -0
  29. data/spec/refile/backend/s3_spec.rb +11 -0
  30. data/spec/refile/backend_examples.rb +215 -0
  31. data/spec/refile/features/direct_upload_spec.rb +29 -0
  32. data/spec/refile/features/normal_upload_spec.rb +36 -0
  33. data/spec/refile/features/presigned_upload_spec.rb +29 -0
  34. data/spec/refile/fixtures/hello.txt +1 -0
  35. data/spec/refile/fixtures/large.txt +44 -0
  36. data/spec/refile/spec_helper.rb +58 -0
  37. data/spec/refile/test_app.rb +46 -0
  38. data/spec/refile/test_app/app/assets/javascripts/application.js +40 -0
  39. data/spec/refile/test_app/app/controllers/application_controller.rb +2 -0
  40. data/spec/refile/test_app/app/controllers/direct_posts_controller.rb +15 -0
  41. data/spec/refile/test_app/app/controllers/home_controller.rb +4 -0
  42. data/spec/refile/test_app/app/controllers/normal_posts_controller.rb +19 -0
  43. data/spec/refile/test_app/app/controllers/presigned_posts_controller.rb +30 -0
  44. data/spec/refile/test_app/app/models/post.rb +5 -0
  45. data/spec/refile/test_app/app/views/direct_posts/new.html.erb +16 -0
  46. data/spec/refile/test_app/app/views/home/index.html.erb +1 -0
  47. data/spec/refile/test_app/app/views/layouts/application.html.erb +14 -0
  48. data/spec/refile/test_app/app/views/normal_posts/new.html.erb +20 -0
  49. data/spec/refile/test_app/app/views/normal_posts/show.html.erb +9 -0
  50. data/spec/refile/test_app/app/views/presigned_posts/new.html.erb +16 -0
  51. data/spec/refile/test_app/config/database.yml +7 -0
  52. data/spec/refile/test_app/config/routes.rb +17 -0
  53. data/spec/refile/test_app/public/favicon.ico +0 -0
  54. data/spec/refile_spec.rb +35 -0
  55. metadata +294 -0
@@ -0,0 +1,29 @@
1
+ require "refile/test_app"
2
+
3
+ feature "Direct HTTP post file uploads", :js do
4
+ scenario "Successfully upload a file" do
5
+ visit "/presigned/posts/new"
6
+ fill_in "Title", with: "A cool post"
7
+ attach_file "Document", path("hello.txt")
8
+
9
+ expect(page).to have_content("Upload started")
10
+ expect(page).to have_content("Upload complete token accepted")
11
+ expect(page).to have_content("Upload success token accepted")
12
+
13
+ click_button "Create"
14
+
15
+ expect(page).to have_selector("h1", text: "A cool post")
16
+ result = Net::HTTP.get_response(URI(find_link("Document")[:href])).body.chomp
17
+ expect(result).to eq("hello")
18
+ end
19
+
20
+ scenario "Fail to upload a file that is too large" do
21
+ visit "/presigned/posts/new"
22
+ fill_in "Title", with: "A cool post"
23
+ attach_file "Document", path("large.txt")
24
+
25
+ expect(page).to have_content("Upload started")
26
+ expect(page).to have_content("Upload failure too large")
27
+ end
28
+ end
29
+
@@ -0,0 +1 @@
1
+ hello
@@ -0,0 +1,44 @@
1
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec massa arcu,
2
+ convallis a tellus vitae, placerat rhoncus nisi. Nullam molestie volutpat
3
+ turpis vitae viverra. Integer nulla nisl, dictum et rutrum quis, auctor ut dui.
4
+ Nulla imperdiet ante a lorem molestie tempor. Suspendisse pharetra in mauris
5
+ mollis dapibus. Nullam pretium fringilla justo, faucibus condimentum elit
6
+ rutrum ac. Vivamus sem justo, congue id lobortis nec, viverra ac lacus. Donec
7
+ eu vestibulum risus, at efficitur nisl. Maecenas sed elit a dui malesuada
8
+ ultricies at eget est. Duis lobortis tincidunt pellentesque.
9
+
10
+ Quisque in nisl felis. Quisque a neque nec diam posuere mattis. Donec a nibh
11
+ cursus, tempus est iaculis, gravida odio. Pellentesque eleifend enim eget
12
+ placerat dignissim. Cum sociis natoque penatibus et magnis dis parturient
13
+ montes, nascetur ridiculus mus. In non ex in augue feugiat convallis. Duis
14
+ varius at sapien vitae molestie. Suspendisse sit amet aliquet quam, quis
15
+ condimentum nibh.
16
+
17
+ Donec a quam quis ipsum imperdiet semper. In ac scelerisque elit, non fermentum
18
+ nisl. Nulla dapibus velit eget ullamcorper blandit. Maecenas justo diam,
19
+ porttitor eget nunc dictum, sollicitudin lacinia lectus. Pellentesque non augue
20
+ urna. In felis ipsum, posuere vitae sapien non, aliquet maximus nibh.
21
+ Suspendisse potenti. Aenean venenatis euismod congue. Praesent quis ullamcorper
22
+ sapien. In hac habitasse platea dictumst. Morbi semper augue dapibus, posuere
23
+ justo sit amet, convallis felis. Vivamus condimentum elementum ex, quis rhoncus
24
+ purus pulvinar et. Nunc vel risus sem. Suspendisse porttitor convallis massa,
25
+ molestie sollicitudin metus semper a. Donec ac cursus tortor. Nam felis nulla,
26
+ pretium eu tempus at, euismod eu erat.
27
+
28
+ Donec vel tempus augue. Pellentesque sit amet ante in odio malesuada facilisis
29
+ nec sit amet turpis. Donec vitae iaculis mauris. Aenean venenatis interdum
30
+ quam, nec tincidunt mauris aliquam vel. Nunc ultrices arcu euismod velit
31
+ ultricies, id laoreet arcu venenatis. Donec euismod scelerisque magna, nec
32
+ interdum lorem ornare nec. Morbi blandit volutpat velit, consequat maximus
33
+ justo mattis non. In pellentesque malesuada consectetur. Cras convallis mi ut
34
+ nibh rutrum ullamcorper. Nam finibus consequat erat a feugiat. Donec laoreet
35
+ risus eget enim interdum dictum. Duis orci lectus, scelerisque tempus volutpat
36
+ eget, ullamcorper eu felis. Aliquam tempor in dui sit amet dapibus. Nulla sed
37
+ metus vestibulum, dignissim odio et, dapibus eros. Praesent semper arcu ut
38
+ augue suscipit, ac ornare tortor auctor. Suspendisse a velit dui.
39
+
40
+ Etiam nec est ut ex laoreet iaculis vel id enim. Vivamus ac hendrerit leo.
41
+ Vestibulum auctor nibh nec arcu rhoncus, a condimentum quam accumsan. Ut justo
42
+ augue, laoreet at bibendum vel, aliquet vitae est. Aliquam accumsan ac diam nec
43
+ pretium. Nulla dictum velit nec elementum mollis. Interdum et malesuada fames
44
+ ac ante ipsum primis in faucibus.
@@ -0,0 +1,58 @@
1
+ require "pry"
2
+ require "refile"
3
+ require "refile/backend_examples"
4
+
5
+ tmp_path = Dir.mktmpdir
6
+
7
+ at_exit do
8
+ FileUtils.remove_entry_secure(tmp_path)
9
+ end
10
+
11
+ Refile.store = Refile::Backend::FileSystem.new(File.expand_path("default_store", tmp_path))
12
+ Refile.cache = Refile::Backend::FileSystem.new(File.expand_path("default_cache", tmp_path))
13
+
14
+ class FakePresignBackend < Refile::Backend::FileSystem
15
+ Signature = Struct.new(:as, :id, :url, :fields)
16
+
17
+ def presign
18
+ id = Refile::RandomHasher.new.hash
19
+ Signature.new("file", id, "/presigned/posts/upload", { token: "xyz123", id: id })
20
+ end
21
+ end
22
+
23
+ Refile.backends["limited_cache"] = FakePresignBackend.new(File.expand_path("default_cache", tmp_path), max_size: 100)
24
+
25
+ Refile.direct_upload = ["cache", "limited_cache"]
26
+
27
+ class Refile::FileDouble
28
+ def initialize(data)
29
+ @io = StringIO.new(data)
30
+ end
31
+
32
+ def read(*args)
33
+ @io.read(*args)
34
+ end
35
+
36
+ def size
37
+ @io.size
38
+ end
39
+
40
+ def eof?
41
+ @io.eof?
42
+ end
43
+
44
+ def close
45
+ @io.close
46
+ end
47
+ end
48
+
49
+ module PathHelper
50
+ def path(filename)
51
+ File.expand_path(File.join("fixtures", filename), File.dirname(__FILE__))
52
+ end
53
+ end
54
+
55
+ RSpec.configure do |config|
56
+ config.include PathHelper
57
+ end
58
+
@@ -0,0 +1,46 @@
1
+ require "rails/all"
2
+
3
+ require "refile"
4
+ require "refile/rails"
5
+ require "jquery/rails"
6
+
7
+ module Refile
8
+ class TestApp < Rails::Application
9
+ config.secret_token = '6805012ab1750f461ef3c531bdce84c0'
10
+ config.session_store :cookie_store, :key => '_refile_session'
11
+ config.active_support.deprecation = :log
12
+ config.eager_load = false
13
+ config.action_dispatch.show_exceptions = false
14
+ config.consider_all_requests_local = true
15
+ config.root = ::File.expand_path("test_app", ::File.dirname(__FILE__))
16
+ end
17
+
18
+ Rails.backtrace_cleaner.remove_silencers!
19
+ TestApp.initialize!
20
+ end
21
+
22
+ class TestMigration < ActiveRecord::Migration
23
+ def self.up
24
+ create_table :posts, :force => true do |t|
25
+ t.column :title, :string
26
+ t.column :image_id, :string
27
+ t.column :document_id, :string
28
+ end
29
+ end
30
+ end
31
+
32
+ quietly do
33
+ TestMigration.up
34
+ end
35
+
36
+ require "rspec"
37
+ require "rspec/rails"
38
+ require "capybara/rails"
39
+ require "capybara/rspec"
40
+ require "refile/spec_helper"
41
+
42
+ Capybara.configure do |config|
43
+ config.server_port = 56120
44
+ end
45
+
46
+ Refile.host = "//127.0.0.1:56120"
@@ -0,0 +1,40 @@
1
+ //= require jquery
2
+ //= require refile
3
+
4
+ "use strict";
5
+
6
+ document.addEventListener("DOMContentLoaded", function() {
7
+ var form = document.querySelector("form#direct");
8
+
9
+ if(form) {
10
+ var input = document.querySelector("#post_document");
11
+
12
+ form.addEventListener("upload:start", function() {
13
+ var p = document.createElement("p");
14
+ p.textContent = "Upload started";
15
+ form.appendChild(p);
16
+ });
17
+
18
+ form.addEventListener("upload:complete", function(e) {
19
+ var p = document.createElement("p");
20
+ p.textContent = "Upload complete " + e.detail;
21
+ form.appendChild(p);
22
+ });
23
+
24
+ form.addEventListener("upload:progress", function(e) {
25
+ var p = document.createElement("p");
26
+ p.textContent = "Upload progress " + e.detail.loaded + " " + e.detail.total;
27
+ form.appendChild(p);
28
+ });
29
+
30
+ form.addEventListener("upload:failure", function(e) {
31
+ var p = document.createElement("p");
32
+ p.textContent = "Upload failure " + e.detail
33
+ form.appendChild(p);
34
+ });
35
+ }
36
+ });
37
+
38
+ $(document).on("upload:success", "form#direct", function(e) {
39
+ $("<p></p>").text("Upload success " + e.originalEvent.detail).appendTo(this);
40
+ });
@@ -0,0 +1,2 @@
1
+ class ApplicationController < ActionController::Base
2
+ end
@@ -0,0 +1,15 @@
1
+ class DirectPostsController < ApplicationController
2
+ def new
3
+ @post = Post.new
4
+ end
5
+
6
+ def create
7
+ @post = Post.new(params.require(:post).permit(:title, :document_cache_id))
8
+
9
+ if @post.save
10
+ redirect_to [:normal, @post]
11
+ else
12
+ render :new
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,4 @@
1
+ class HomeController < ApplicationController
2
+ def index
3
+ end
4
+ end
@@ -0,0 +1,19 @@
1
+ class NormalPostsController < ApplicationController
2
+ def new
3
+ @post = Post.new
4
+ end
5
+
6
+ def show
7
+ @post = Post.find(params[:id])
8
+ end
9
+
10
+ def create
11
+ @post = Post.new(params.require(:post).permit(:title, :image, :image_cache_id, :document, :document_cache_id))
12
+
13
+ if @post.save
14
+ redirect_to [:normal, @post]
15
+ else
16
+ render :new
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,30 @@
1
+ class PresignedPostsController < ApplicationController
2
+ def new
3
+ @post = Post.new
4
+ end
5
+
6
+ def create
7
+ @post = Post.new(params.require(:post).permit(:title, :document_cache_id))
8
+
9
+ if @post.save
10
+ redirect_to [:normal, @post]
11
+ else
12
+ render :new
13
+ end
14
+ end
15
+
16
+ def upload
17
+ if params[:token] == "xyz123"
18
+ if params[:file].size < 100
19
+ File.open(File.join(Refile.backends["limited_cache"].directory, params[:id]), "wb") do |file|
20
+ file.write(params[:file].read)
21
+ end
22
+ render text: "token accepted"
23
+ else
24
+ render text: "too large", status: 413
25
+ end
26
+ else
27
+ render text: "token rejected", status: 403
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,5 @@
1
+ class Post < ActiveRecord::Base
2
+ attachment :image
3
+ attachment :document, cache: :limited_cache
4
+ validates_presence_of :title
5
+ end
@@ -0,0 +1,16 @@
1
+ <%= form_for [:direct, @post], html: { id: "direct" } do |form| %>
2
+ <p>
3
+ <%= @post.errors.full_messages.to_sentence %>
4
+ </p>
5
+ <p>
6
+ <%= form.label :title %>
7
+ <%= form.text_field :title %>
8
+ </p>
9
+ <p>
10
+ <%= form.label :document %>
11
+ <%= form.attachment_field :document, direct: true %>
12
+ </p>
13
+ <p>
14
+ <%= form.submit "Create" %>
15
+ </p>
16
+ <% end %>
@@ -0,0 +1 @@
1
+ <h1>Hello world</h1>
@@ -0,0 +1,14 @@
1
+ <!doctype html>
2
+
3
+ <html>
4
+ <head>
5
+ <%= javascript_include_tag "application" %>
6
+
7
+ </head>
8
+
9
+ <body>
10
+ <h1>Refile Test App</h1>
11
+
12
+ <%= yield %>
13
+ </body>
14
+ </html>
@@ -0,0 +1,20 @@
1
+ <%= form_for [:normal, @post] do |form| %>
2
+ <p>
3
+ <%= @post.errors.full_messages.to_sentence %>
4
+ </p>
5
+ <p>
6
+ <%= form.label :title %>
7
+ <%= form.text_field :title %>
8
+ </p>
9
+ <p>
10
+ <%= form.label :image %>
11
+ <%= form.attachment_field :image %>
12
+ </p>
13
+ <p>
14
+ <%= form.label :document %>
15
+ <%= form.attachment_field :document %>
16
+ </p>
17
+ <p>
18
+ <%= form.submit "Create" %>
19
+ </p>
20
+ <% end %>
@@ -0,0 +1,9 @@
1
+ <h1><%= @post.title %></h1<>
2
+
3
+ <% if @post.image %>
4
+ <%= attachment_image_tag(@post, :image) %>
5
+ <% end %>
6
+
7
+ <% if @post.document %>
8
+ <%= link_to "Document", attachment_url(@post, :document) %>
9
+ <% end %>
@@ -0,0 +1,16 @@
1
+ <%= form_for [:presigned, @post], html: { id: "direct" } do |form| %>
2
+ <p>
3
+ <%= @post.errors.full_messages.to_sentence %>
4
+ </p>
5
+ <p>
6
+ <%= form.label :title %>
7
+ <%= form.text_field :title %>
8
+ </p>
9
+ <p>
10
+ <%= form.label :document %>
11
+ <%= form.attachment_field :document, presigned: true %>
12
+ </p>
13
+ <p>
14
+ <%= form.submit "Create" %>
15
+ </p>
16
+ <% end %>
@@ -0,0 +1,7 @@
1
+ development: &default
2
+ adapter: "sqlite3"
3
+ database: "db/db.sqlite"
4
+ verbosity: "quiet"
5
+
6
+ test:
7
+ <<: *default
@@ -0,0 +1,17 @@
1
+ Refile::TestApp.routes.draw do
2
+ root to: "home#index"
3
+
4
+ scope path: "normal", as: "normal" do
5
+ resources :posts, only: [:new, :create, :show], controller: "normal_posts"
6
+ end
7
+
8
+ scope path: "direct", as: "direct" do
9
+ resources :posts, only: [:new, :create], controller: "direct_posts"
10
+ end
11
+
12
+ scope path: "presigned", as: "presigned" do
13
+ resources :posts, only: [:new, :create], controller: "presigned_posts" do
14
+ post :upload, on: :collection
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,35 @@
1
+ require "refile"
2
+
3
+ RSpec.describe Refile do
4
+ let(:io) { StringIO.new("hello") }
5
+
6
+ describe ".verify_uploadable" do
7
+ it "works if it conforms to required API" do
8
+ expect(Refile.verify_uploadable(double(size: 444, read: io, eof?: true, close: nil), nil)).to be_truthy
9
+ end
10
+
11
+ it "raises ArgumentError if argument does not respond to `size`" do
12
+ expect { Refile.verify_uploadable(double(read: io, eof?: true, close: nil), nil) }.to raise_error(ArgumentError)
13
+ end
14
+
15
+ it "raises ArgumentError if argument does not respond to `read`" do
16
+ expect { Refile.verify_uploadable(double(size: 444, eof?: true, close: nil), nil) }.to raise_error(ArgumentError)
17
+ end
18
+
19
+ it "raises ArgumentError if argument does not respond to `eof?`" do
20
+ expect { Refile.verify_uploadable(double(size: 444, read: true, close: nil), nil) }.to raise_error(ArgumentError)
21
+ end
22
+
23
+ it "raises ArgumentError if argument does not respond to `close`" do
24
+ expect { Refile.verify_uploadable(double(size: 444, read: true, eof?: true), nil) }.to raise_error(ArgumentError)
25
+ end
26
+
27
+ it "returns true if size is respeced" do
28
+ expect(Refile.verify_uploadable(Refile::FileDouble.new("hello"), 8)).to be_truthy
29
+ end
30
+
31
+ it "raises Refile::Invalid if size is exceeded" do
32
+ expect { Refile.verify_uploadable(Refile::FileDouble.new("hello world"), 8) }.to raise_error(Refile::Invalid)
33
+ end
34
+ end
35
+ end