defile 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +27 -0
- data/.rspec +1 -0
- data/.travis.yml +8 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +466 -0
- data/Rakefile +11 -0
- data/app/assets/javascripts/defile.js +50 -0
- data/app/helpers/attachment_helper.rb +50 -0
- data/config.ru +8 -0
- data/config/locales/en.yml +5 -0
- data/config/routes.rb +3 -0
- data/defile.gemspec +34 -0
- data/lib/defile.rb +72 -0
- data/lib/defile/app.rb +97 -0
- data/lib/defile/attachment.rb +89 -0
- data/lib/defile/attachment/active_record.rb +24 -0
- data/lib/defile/backend/file_system.rb +70 -0
- data/lib/defile/backend/s3.rb +129 -0
- data/lib/defile/file.rb +65 -0
- data/lib/defile/image_processing.rb +73 -0
- data/lib/defile/rails.rb +36 -0
- data/lib/defile/random_hasher.rb +5 -0
- data/lib/defile/version.rb +3 -0
- data/spec/defile/app_spec.rb +151 -0
- data/spec/defile/attachment_spec.rb +141 -0
- data/spec/defile/backend/file_system_spec.rb +30 -0
- data/spec/defile/backend/s3_spec.rb +11 -0
- data/spec/defile/backend_examples.rb +215 -0
- data/spec/defile/features/direct_upload_spec.rb +29 -0
- data/spec/defile/features/normal_upload_spec.rb +36 -0
- data/spec/defile/features/presigned_upload_spec.rb +29 -0
- data/spec/defile/fixtures/hello.txt +1 -0
- data/spec/defile/fixtures/large.txt +44 -0
- data/spec/defile/spec_helper.rb +58 -0
- data/spec/defile/test_app.rb +46 -0
- data/spec/defile/test_app/app/assets/javascripts/application.js +40 -0
- data/spec/defile/test_app/app/controllers/application_controller.rb +2 -0
- data/spec/defile/test_app/app/controllers/direct_posts_controller.rb +15 -0
- data/spec/defile/test_app/app/controllers/home_controller.rb +4 -0
- data/spec/defile/test_app/app/controllers/normal_posts_controller.rb +19 -0
- data/spec/defile/test_app/app/controllers/presigned_posts_controller.rb +30 -0
- data/spec/defile/test_app/app/models/post.rb +5 -0
- data/spec/defile/test_app/app/views/direct_posts/new.html.erb +16 -0
- data/spec/defile/test_app/app/views/home/index.html.erb +1 -0
- data/spec/defile/test_app/app/views/layouts/application.html.erb +14 -0
- data/spec/defile/test_app/app/views/normal_posts/new.html.erb +20 -0
- data/spec/defile/test_app/app/views/normal_posts/show.html.erb +9 -0
- data/spec/defile/test_app/app/views/presigned_posts/new.html.erb +16 -0
- data/spec/defile/test_app/config/database.yml +7 -0
- data/spec/defile/test_app/config/routes.rb +17 -0
- data/spec/defile/test_app/public/favicon.ico +0 -0
- data/spec/defile_spec.rb +35 -0
- metadata +294 -0
@@ -0,0 +1,29 @@
|
|
1
|
+
require "defile/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 "defile"
|
3
|
+
require "defile/backend_examples"
|
4
|
+
|
5
|
+
tmp_path = Dir.mktmpdir
|
6
|
+
|
7
|
+
at_exit do
|
8
|
+
FileUtils.remove_entry_secure(tmp_path)
|
9
|
+
end
|
10
|
+
|
11
|
+
Defile.store = Defile::Backend::FileSystem.new(File.expand_path("default_store", tmp_path))
|
12
|
+
Defile.cache = Defile::Backend::FileSystem.new(File.expand_path("default_cache", tmp_path))
|
13
|
+
|
14
|
+
class FakePresignBackend < Defile::Backend::FileSystem
|
15
|
+
Signature = Struct.new(:as, :id, :url, :fields)
|
16
|
+
|
17
|
+
def presign
|
18
|
+
id = Defile::RandomHasher.new.hash
|
19
|
+
Signature.new("file", id, "/presigned/posts/upload", { token: "xyz123", id: id })
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
Defile.backends["limited_cache"] = FakePresignBackend.new(File.expand_path("default_cache", tmp_path), max_size: 100)
|
24
|
+
|
25
|
+
Defile.direct_upload = ["cache", "limited_cache"]
|
26
|
+
|
27
|
+
class Defile::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 "defile"
|
4
|
+
require "defile/rails"
|
5
|
+
require "jquery/rails"
|
6
|
+
|
7
|
+
module Defile
|
8
|
+
class TestApp < Rails::Application
|
9
|
+
config.secret_token = '6805012ab1750f461ef3c531bdce84c0'
|
10
|
+
config.session_store :cookie_store, :key => '_defile_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 "defile/spec_helper"
|
41
|
+
|
42
|
+
Capybara.configure do |config|
|
43
|
+
config.server_port = 56120
|
44
|
+
end
|
45
|
+
|
46
|
+
Defile.host = "//127.0.0.1:56120"
|
@@ -0,0 +1,40 @@
|
|
1
|
+
//= require jquery
|
2
|
+
//= require defile
|
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,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,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(Defile.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,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,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,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,17 @@
|
|
1
|
+
Defile::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
|
File without changes
|
data/spec/defile_spec.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
require "defile"
|
2
|
+
|
3
|
+
RSpec.describe Defile 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(Defile.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 { Defile.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 { Defile.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 { Defile.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 { Defile.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(Defile.verify_uploadable(Defile::FileDouble.new("hello"), 8)).to be_truthy
|
29
|
+
end
|
30
|
+
|
31
|
+
it "raises Defile::Invalid if size is exceeded" do
|
32
|
+
expect { Defile.verify_uploadable(Defile::FileDouble.new("hello world"), 8) }.to raise_error(Defile::Invalid)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|