adrift 0.0.1
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.
- data/.gitignore +6 -0
- data/.rspec +1 -0
- data/Gemfile +3 -0
- data/LICENSE +20 -0
- data/README.rdoc +10 -0
- data/Rakefile +22 -0
- data/adrift.gemspec +38 -0
- data/autotest/discover.rb +1 -0
- data/features/active_record_integration.feature +42 -0
- data/features/data_mapper_integration.feature +42 -0
- data/features/step_definitions/model_steps.rb +54 -0
- data/features/support/env.rb +55 -0
- data/features/support/world.rb +58 -0
- data/lib/adrift.rb +10 -0
- data/lib/adrift/attachment.rb +246 -0
- data/lib/adrift/file_to_attach.rb +121 -0
- data/lib/adrift/integration.rb +39 -0
- data/lib/adrift/integration/active_record.rb +29 -0
- data/lib/adrift/integration/base.rb +87 -0
- data/lib/adrift/integration/data_mapper.rb +29 -0
- data/lib/adrift/pattern.rb +219 -0
- data/lib/adrift/processor.rb +100 -0
- data/lib/adrift/railtie.rb +12 -0
- data/lib/adrift/storage.rb +82 -0
- data/lib/adrift/version.rb +3 -0
- data/spec/adrift/attachment_spec.rb +488 -0
- data/spec/adrift/file_to_attach_spec.rb +78 -0
- data/spec/adrift/integration/active_record_spec.rb +21 -0
- data/spec/adrift/integration/base_spec.rb +7 -0
- data/spec/adrift/integration/data_mapper_spec.rb +21 -0
- data/spec/adrift/pattern_spec.rb +98 -0
- data/spec/adrift/processor_spec.rb +61 -0
- data/spec/adrift/storage_spec.rb +181 -0
- data/spec/fixtures/me.png +0 -0
- data/spec/fixtures/me_no_colors.png +0 -0
- data/spec/shared_examples/integration/base.rb +128 -0
- data/spec/spec_helper.rb +47 -0
- metadata +277 -0
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Adrift
|
4
|
+
describe FileToAttach do
|
5
|
+
context "when initialized with an unknown file representation" do
|
6
|
+
it "raises an error" do
|
7
|
+
expect {
|
8
|
+
FileToAttach.new(Object.new)
|
9
|
+
}.to raise_error(UnknownFileRepresentationError)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
context "within a rails application" do
|
14
|
+
let(:file_to_attach) { FileToAttach.new(file_representation) }
|
15
|
+
let(:file_representation) do
|
16
|
+
double('uploaded file', {
|
17
|
+
:original_filename => 'me.png',
|
18
|
+
:tempfile => double('tempfile', :path => '/tmp/123')
|
19
|
+
})
|
20
|
+
end
|
21
|
+
|
22
|
+
describe "#original_filename" do
|
23
|
+
it "returns the original filename of the uploaded file" do
|
24
|
+
file_to_attach.original_filename.should ==
|
25
|
+
file_representation.original_filename
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "#path" do
|
30
|
+
it "returns the uploaded tempfile's path" do
|
31
|
+
file_to_attach.path.should == file_representation.tempfile.path
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
context "within a rack (non rails) application" do
|
37
|
+
let(:file_to_attach) { FileToAttach.new(file_representation) }
|
38
|
+
let(:file_representation) do
|
39
|
+
{
|
40
|
+
:filename => 'me.png',
|
41
|
+
:tempfile => double('tempfile', :path => '/tmp/123')
|
42
|
+
}
|
43
|
+
end
|
44
|
+
|
45
|
+
describe "#original_filename" do
|
46
|
+
it "returns the original filename of the uploaded file" do
|
47
|
+
file_to_attach.original_filename.should ==
|
48
|
+
file_representation[:filename]
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe "#path" do
|
53
|
+
it "returns the uploaded tempfile's path" do
|
54
|
+
file_to_attach.path.should == file_representation[:tempfile].path
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
context "when initialized with a local file" do
|
60
|
+
let(:file_to_attach) { FileToAttach.new(file_representation) }
|
61
|
+
let(:file_representation) do
|
62
|
+
double('file', :to_path => '/avatars/me.png')
|
63
|
+
end
|
64
|
+
|
65
|
+
describe "#original_filename" do
|
66
|
+
it "returns the local file name" do
|
67
|
+
file_to_attach.original_filename.should == 'me.png'
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
describe "#path" do
|
72
|
+
it "returns the local file's path" do
|
73
|
+
file_to_attach.path.should == file_representation.to_path
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Adrift::Integration
|
4
|
+
describe ActiveRecord do
|
5
|
+
it_behaves_like Adrift::Integration::Base do
|
6
|
+
before { klass.stub(after_save: nil, before_destroy: nil) }
|
7
|
+
|
8
|
+
describe ".attachment" do
|
9
|
+
it "registers an after save callback to save the attachments" do
|
10
|
+
klass.should_receive(:after_save).with(:save_attachments)
|
11
|
+
klass.attachment :avatar
|
12
|
+
end
|
13
|
+
|
14
|
+
it "registers a before destroy callback to destroy the attachments" do
|
15
|
+
klass.should_receive(:before_destroy).with(:destroy_attachments)
|
16
|
+
klass.attachment :avatar
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Adrift::Integration
|
4
|
+
describe DataMapper do
|
5
|
+
it_behaves_like Adrift::Integration::Base do
|
6
|
+
before { klass.stub(after: nil, before: nil) }
|
7
|
+
|
8
|
+
describe ".attachment" do
|
9
|
+
it "registers an after save callback to save the attachments" do
|
10
|
+
klass.should_receive(:after).with(:save, :save_attachments)
|
11
|
+
klass.attachment :avatar
|
12
|
+
end
|
13
|
+
|
14
|
+
it "registers a before destroy callback to destroy the attachments" do
|
15
|
+
klass.should_receive(:before).with(:destroy, :destroy_attachments)
|
16
|
+
klass.attachment :avatar
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Adrift
|
4
|
+
describe Pattern do
|
5
|
+
describe "#specialize" do
|
6
|
+
let(:attachment) { double('attachment') }
|
7
|
+
|
8
|
+
it "replaces :attachment with the pluralized attachment's name" do
|
9
|
+
attachment.stub(name: 'avatar')
|
10
|
+
Pattern.new(':attachment').specialize(attachment: attachment).should == 'avatars'
|
11
|
+
end
|
12
|
+
|
13
|
+
it "replaces :style with the given style" do
|
14
|
+
Pattern.new(':style').specialize(style: :normal).should == 'normal'
|
15
|
+
end
|
16
|
+
|
17
|
+
it "replaces :url with the attachment's url for the given style" do
|
18
|
+
attachment.stub(:url) do |style|
|
19
|
+
case style
|
20
|
+
when :original then '/attachment/url'
|
21
|
+
when :small then '/attachment/small/url'
|
22
|
+
end
|
23
|
+
end
|
24
|
+
pattern = Pattern.new(':url')
|
25
|
+
pattern.specialize(attachment: attachment, style: :original).should == '/attachment/url'
|
26
|
+
pattern.specialize(attachment: attachment, style: :small).should == '/attachment/small/url'
|
27
|
+
end
|
28
|
+
|
29
|
+
context "when the model's class is a top-level one" do
|
30
|
+
it "replaces :class with the pluralized model's class name" do
|
31
|
+
attachment.stub_chain(:model, :class, :name).and_return('User')
|
32
|
+
Pattern.new(':class').specialize(attachment: attachment).should == 'users'
|
33
|
+
end
|
34
|
+
|
35
|
+
it "replaces :class_name with the pluralized model's class name" do
|
36
|
+
attachment.stub_chain(:model, :class, :name).and_return('User')
|
37
|
+
Pattern.new(':class_name').specialize(attachment: attachment).should == 'users'
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
context "when the model's class is not a top-level one" do
|
42
|
+
it "replaces :class with the pluralized model's class name namespaced" do
|
43
|
+
attachment.stub_chain(:model, :class, :name).and_return('App::Models::User')
|
44
|
+
Pattern.new(':class').specialize(attachment: attachment).should == 'app/models/users'
|
45
|
+
end
|
46
|
+
|
47
|
+
it "replaces :class_name with the pluralized model's class name" do
|
48
|
+
attachment.stub_chain(:model, :class, :name).and_return('App::Models::User')
|
49
|
+
Pattern.new(':class_name').specialize(attachment: attachment).should == 'users'
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
it "replaces :id with the model's id" do
|
54
|
+
attachment.stub_chain(:model, :id).and_return(1)
|
55
|
+
Pattern.new(':id').specialize(attachment: attachment).should == '1'
|
56
|
+
end
|
57
|
+
|
58
|
+
context "when Pattern::Tags::Root.path has been assigned" do
|
59
|
+
after { Pattern::Tags::Root.path = nil }
|
60
|
+
|
61
|
+
it "returns Root.path" do
|
62
|
+
Pattern::Tags::Root.path = '/root/path'
|
63
|
+
Pattern.new(':root').specialize.should == '/root/path'
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
context "when Root.path hasn't been assigned" do
|
68
|
+
it "returns '.' by default" do
|
69
|
+
Pattern.new(':root').specialize.should == '.'
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
it "replaces :filename with the attachment's file name" do
|
74
|
+
attachment.stub(filename: 'me.png')
|
75
|
+
Pattern.new(':filename').specialize(attachment: attachment).should == 'me.png'
|
76
|
+
end
|
77
|
+
|
78
|
+
it "replaces :basename with the attachment's file basename" do
|
79
|
+
attachment.stub(filename: 'me.png')
|
80
|
+
Pattern.new(':basename').specialize(attachment: attachment).should == 'me'
|
81
|
+
end
|
82
|
+
|
83
|
+
it "replaces :extension with the attachment's file extension" do
|
84
|
+
attachment.stub(filename: 'me.png')
|
85
|
+
Pattern.new(':extension').specialize(attachment: attachment).should == 'png'
|
86
|
+
end
|
87
|
+
|
88
|
+
it "replaces a tag every time it appears" do
|
89
|
+
attachment.stub(name: 'avatar')
|
90
|
+
Pattern.new(':attachment/:attachment/:attachment').specialize(attachment: attachment).should == 'avatars/avatars/avatars'
|
91
|
+
end
|
92
|
+
|
93
|
+
it "doesn't replace unknown tags" do
|
94
|
+
Pattern.new(':unknown_tag').specialize.should == ':unknown_tag'
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Adrift
|
4
|
+
module Processor
|
5
|
+
describe Thumbnail::Cli do
|
6
|
+
describe "#run" do
|
7
|
+
it "calls convert with the proper syntax" do
|
8
|
+
cli = Thumbnail::Cli.new
|
9
|
+
cli.should_receive('`').with('convert /tmp/123 -resize "50x50^" -gravity "center" -extent "50x50" /tmp/small-123')
|
10
|
+
cli.run('/tmp/123', '/tmp/small-123', :resize => '50x50^', :gravity => 'center', :extent => '50x50')
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
describe Thumbnail do
|
16
|
+
let(:cli) { double('cli').as_null_object }
|
17
|
+
let(:processor) { Thumbnail.new(cli) }
|
18
|
+
|
19
|
+
describe "#processed_files" do
|
20
|
+
context "for a newly instantiated processor" do
|
21
|
+
it "returns an empty hash" do
|
22
|
+
processor.processed_files.should == {}
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
context "immediately after a proccess" do
|
27
|
+
it "returns the generated files in the last process" do
|
28
|
+
processor.process('/tmp/123', :small => '50x50', :normal => '100x100')
|
29
|
+
processor.processed_files.should == { :small => '/tmp/small-123', :normal => '/tmp/normal-123' }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
context "after two proccess" do
|
34
|
+
it "returns the generated files in the last process" do
|
35
|
+
processor.process('/tmp/123', :small => '50x50')
|
36
|
+
processor.process('/tmp/456', :normal => '100x100')
|
37
|
+
processor.processed_files.should == { :normal => '/tmp/normal-456' }
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe "#process" do
|
43
|
+
it "doesn't do anything if no styles are given" do
|
44
|
+
processor.process('/tmp/123', {})
|
45
|
+
processor.processed_files.should be_empty
|
46
|
+
end
|
47
|
+
|
48
|
+
it "creates the thumbnails for the given styles" do
|
49
|
+
cli.should_receive(:run).with('/tmp/123', '/tmp/small-123', :resize => '50x50!')
|
50
|
+
cli.should_receive(:run).with('/tmp/123', '/tmp/normal-123', :resize => '100x100')
|
51
|
+
processor.process('/tmp/123', :small => '50x50!', :normal => '100x100')
|
52
|
+
end
|
53
|
+
|
54
|
+
it "crops the image when the style definition ends with '#'" do
|
55
|
+
cli.should_receive(:run).with('/tmp/123', '/tmp/small-123', :resize => '50x50^', :gravity => 'center', :background => 'None', :extent => '50x50')
|
56
|
+
processor.process('/tmp/123', :small => '50x50#')
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,181 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Adrift::Storage
|
4
|
+
describe Filesystem do
|
5
|
+
let(:storage) { Filesystem.new }
|
6
|
+
|
7
|
+
describe "#dirty?" do
|
8
|
+
context "for a newly instantiated storage" do
|
9
|
+
it "returns false" do
|
10
|
+
storage.should_not be_dirty
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
context "when it hasn't been flushed" do
|
15
|
+
context "and there's a file that needs to be stored" do
|
16
|
+
it "reutrns false" do
|
17
|
+
storage.store('/tmp/1', '/path/to/storage/1/file')
|
18
|
+
storage.should be_dirty
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
context "and there's a file that needs to be removed" do
|
23
|
+
it "returns false" do
|
24
|
+
storage.remove('/path/to/storage/1/file')
|
25
|
+
storage.should be_dirty
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
context "immediately after a flush" do
|
31
|
+
it "returns true" do
|
32
|
+
storage.store('/tmp/1', '/path/to/storage/1/file')
|
33
|
+
storage.remove('/path/to/storage/2/file')
|
34
|
+
storage.flush
|
35
|
+
storage.should_not be_dirty
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe "#removed" do
|
41
|
+
context "for a newly instantiated storage" do
|
42
|
+
it "returns an empty array" do
|
43
|
+
storage.removed.should == []
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
context "immediately after a flush" do
|
48
|
+
before do
|
49
|
+
storage.remove('/path/to/storage/1/file')
|
50
|
+
storage.remove('/path/to/storage/2/file')
|
51
|
+
storage.flush
|
52
|
+
end
|
53
|
+
|
54
|
+
it "returns the files removed in the last flush" do
|
55
|
+
storage.removed.should include('/path/to/storage/1/file')
|
56
|
+
storage.removed.should include('/path/to/storage/2/file')
|
57
|
+
storage.removed.size.should == 2
|
58
|
+
end
|
59
|
+
|
60
|
+
context "when a file has been enqueued for removal" do
|
61
|
+
before { storage.remove('/path/to/storage/3/file') }
|
62
|
+
|
63
|
+
it "doesn't include it before a flush" do
|
64
|
+
storage.removed.should_not include('/path/to/storage/3/file')
|
65
|
+
end
|
66
|
+
|
67
|
+
it "only contains it after a flush" do
|
68
|
+
storage.flush
|
69
|
+
storage.removed.should == ['/path/to/storage/3/file']
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
describe "#stored" do
|
76
|
+
context "for a newly instantiated storage" do
|
77
|
+
it "returns an empty array" do
|
78
|
+
storage.stored.should == []
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
context "immediately after a flush" do
|
83
|
+
before do
|
84
|
+
storage.store('/tmp/1', '/path/to/storage/1/file')
|
85
|
+
storage.store('/tmp/2', '/path/to/storage/2/file')
|
86
|
+
storage.flush
|
87
|
+
end
|
88
|
+
|
89
|
+
it "returns the files stored in the last flush" do
|
90
|
+
storage.stored.should include(['/tmp/1', '/path/to/storage/1/file'])
|
91
|
+
storage.stored.should include(['/tmp/2', '/path/to/storage/2/file'])
|
92
|
+
storage.stored.size.should == 2
|
93
|
+
end
|
94
|
+
|
95
|
+
context "when a file has been enqueued for storage" do
|
96
|
+
before { storage.store('/tmp/3', '/path/to/storage/3/file') }
|
97
|
+
|
98
|
+
it "doesn't include it before a flush" do
|
99
|
+
storage.stored.should_not include(['/tmp/3', '/path/to/storage/3/file'])
|
100
|
+
end
|
101
|
+
|
102
|
+
it "only contains it after a flush" do
|
103
|
+
storage.flush
|
104
|
+
storage.stored.should == [['/tmp/3', '/path/to/storage/3/file']]
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
describe "#remove!" do
|
111
|
+
before { File.stub(:exist? => true) }
|
112
|
+
|
113
|
+
context "when there're files that need to be removed" do
|
114
|
+
before do
|
115
|
+
storage.remove('/path/to/storage/1/file')
|
116
|
+
storage.remove('/path/to/storage/2/file')
|
117
|
+
end
|
118
|
+
|
119
|
+
it "removes the specified files" do
|
120
|
+
FileUtils.should_receive(:rm).with('/path/to/storage/1/file')
|
121
|
+
FileUtils.should_receive(:rm).with('/path/to/storage/2/file')
|
122
|
+
storage.remove!
|
123
|
+
end
|
124
|
+
|
125
|
+
it "doesn't try to remove inexistent files" do
|
126
|
+
File.stub(:exist?) { |path| path != '/path/to/storage/1/file' }
|
127
|
+
FileUtils.should_not_receive(:rm).with('/path/to/storage/1/file')
|
128
|
+
FileUtils.should_receive(:rm).with('/path/to/storage/2/file')
|
129
|
+
storage.remove!
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
describe "#store!" do
|
135
|
+
before { File.stub(:exist? => true) }
|
136
|
+
|
137
|
+
context "when there're files that need to be stored" do
|
138
|
+
before do
|
139
|
+
storage.store('/tmp/1', '/path/to/storage/1/file')
|
140
|
+
storage.store('/tmp/2', '/path/to/storage/2/file')
|
141
|
+
end
|
142
|
+
|
143
|
+
it "tries to create the directories for the files to store" do
|
144
|
+
FileUtils.should_receive(:mkdir_p).with('/path/to/storage/1')
|
145
|
+
FileUtils.should_receive(:mkdir_p).with('/path/to/storage/2')
|
146
|
+
storage.store!
|
147
|
+
end
|
148
|
+
|
149
|
+
it "moves the specified files to their destination path" do
|
150
|
+
FileUtils.should_receive(:cp).with('/tmp/1', '/path/to/storage/1/file')
|
151
|
+
FileUtils.should_receive(:cp).with('/tmp/2', '/path/to/storage/2/file')
|
152
|
+
storage.store!
|
153
|
+
end
|
154
|
+
|
155
|
+
it "changes the permissions of the stored files" do
|
156
|
+
FileUtils.should_receive(:chmod).with(0644, '/path/to/storage/1/file')
|
157
|
+
FileUtils.should_receive(:chmod).with(0644, '/path/to/storage/2/file')
|
158
|
+
storage.store!
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
describe "for each file" do
|
163
|
+
it "creates its directory, then moves it there and then sets its permissions" do
|
164
|
+
storage.store('/tmp/1', '/path/to/storage/1/file')
|
165
|
+
FileUtils.should_receive(:mkdir_p).ordered
|
166
|
+
FileUtils.should_receive(:cp).ordered
|
167
|
+
FileUtils.should_receive(:chmod).ordered
|
168
|
+
storage.store!
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
describe "#flush" do
|
174
|
+
it "removes the enqueued files for removal and then stores the enqueued for storage" do
|
175
|
+
storage.should_receive(:remove!).ordered
|
176
|
+
storage.should_receive(:store!).ordered
|
177
|
+
storage.flush
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|