paperclip-storage-ftp 1.2.6 → 1.2.7

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: 52dd89bd22677f466873efbf00464db5185f89e7
4
- data.tar.gz: cc7bf20e77a50e009cf45b2feca86c311d3451aa
3
+ metadata.gz: de0a8d8ea7ed9dc2328bf8c6b9d5ff9eaae40f35
4
+ data.tar.gz: 7da4635ee11a19c26c051e4a5c23229efa00521a
5
5
  SHA512:
6
- metadata.gz: 854d6376410b4c7e7dbed7d0ad0ecc35aa349c2974618aeef3340c4795e0bcc1a237c68dd4dae433a559cfd69b85860afdd63618dd8cf459c91355dbe9d57eb5
7
- data.tar.gz: 7463b456f29a1bc116a4ff64ae34626a90c9682e043f6c3046c613ad7e8d8c6053c833eaee6c466330873e64cc593fccc9beda548a225b052505be4fcb48af5a
6
+ metadata.gz: 7947b3eb940d332765d567b71c6f548e5ed1ef8bdba0654801a46496981d7a8f8d89af5ff3803889192cc3ab2d6be29d200a1937c117942cbaf1e24531a49436
7
+ data.tar.gz: 267ed5502e2193a0acc7357d64b6a33c501eac55fc35101046da96405ea0f1fb01b773c04961e494a8c3d2a2ea6225355f4dcad6555285f1b55dcceb3e300b44
data/.gitignore CHANGED
@@ -24,6 +24,8 @@ tmp
24
24
  vendor/apache-ftpserver/res/ftpd.pid*
25
25
  vendor/apache-ftpserver/res/home/*
26
26
  vendor/apache-ftpserver/res/user1/*
27
+ !vendor/apache-ftpserver/res/user1/.gitkeep
27
28
  vendor/apache-ftpserver/res/user2/*
29
+ !vendor/apache-ftpserver/res/user2/.gitkeep
28
30
  vendor/apache-ftpserver/res/log/*
29
31
  .jrubyrc
@@ -8,3 +8,5 @@ gemfile:
8
8
  matrix:
9
9
  allow_failures:
10
10
  - rvm: jruby-19mode
11
+ sudo: false
12
+ cache: bundler
data/README.md CHANGED
@@ -87,6 +87,10 @@ end
87
87
 
88
88
  ## Changelog
89
89
 
90
+ ### 1.2.7
91
+
92
+ * Reduce number of FTP commands for creating directories [#27](https://github.com/xing/paperclip-storage-ftp/pull/27)
93
+
90
94
  ### 1.2.6
91
95
 
92
96
  * New option `:ftp_keep_empty_directories` to disable the removal of empty parent directories when deleting files (introduced in 1.2.2). See usage example above.
@@ -39,11 +39,14 @@ module Paperclip
39
39
  with_ftp_servers do |servers|
40
40
  servers.map do |server|
41
41
  run_thread do
42
+ write_queue = {}
42
43
  @queued_for_write.each do |style_name, file|
43
44
  remote_path = path(style_name)
44
45
  log("saving ftp://#{server.user}@#{server.host}:#{remote_path}")
45
- server.put_file(file.path, remote_path)
46
+ write_queue[file.path] = remote_path
46
47
  end
48
+
49
+ server.put_files(write_queue)
47
50
  end
48
51
  end.each(&:join)
49
52
  end
@@ -60,10 +60,29 @@ module Paperclip
60
60
 
61
61
  def put_file(local_file_path, remote_file_path)
62
62
  pathname = Pathname.new(remote_file_path)
63
- mkdir_p(pathname.dirname.to_s)
64
63
  connection.putbinaryfile(local_file_path, remote_file_path)
65
64
  end
66
65
 
66
+ def put_files(file_paths)
67
+ tree = directory_tree(file_paths.values)
68
+ mktree(tree)
69
+
70
+ file_paths.each do |local_file_path, remote_file_path|
71
+ put_file(local_file_path, remote_file_path)
72
+ end
73
+ end
74
+
75
+ def directory_tree(file_paths)
76
+ directories = file_paths.map do |path|
77
+ Pathname.new(path).dirname.to_s.split("/").reject(&:empty?)
78
+ end
79
+ tree = Hash.new {|h, k| h[k] = Hash.new(&h.default_proc)}
80
+ directories.each do |directory|
81
+ directory.inject(tree){|h,k| h[k]}
82
+ end
83
+ tree
84
+ end
85
+
67
86
  def delete_file(remote_file_path)
68
87
  connection.delete(remote_file_path)
69
88
  rescue Net::FTPPermError
@@ -79,16 +98,20 @@ module Paperclip
79
98
  # Stop trying to remove parent directories
80
99
  end
81
100
 
82
- def mkdir_p(dirname)
83
- pathname = Pathname.new(dirname)
84
- pathname.descend do |p|
101
+ def mktree(tree, base = "/")
102
+ return unless tree.any?
103
+ list = connection.nlst(base)
104
+ tree.reject{|k,_| list.include?(k)}.each do |directory, sub_directories|
85
105
  begin
86
- connection.mkdir(p.to_s)
106
+ connection.mkdir(base + directory)
87
107
  rescue Net::FTPPermError
88
- # This error can be caused by an existing directory.
89
- # Ignore, and keep on trying to create child directories.
108
+ # This error can be caused by an already existing directory,
109
+ # maybe it was created in the meantime.
90
110
  end
91
111
  end
112
+ tree.each do |directory, sub_directories|
113
+ mktree(sub_directories, base + directory + "/")
114
+ end
92
115
  end
93
116
 
94
117
  private
@@ -12,7 +12,7 @@ Gem::Specification.new do |gem|
12
12
  gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
13
13
  gem.name = "paperclip-storage-ftp"
14
14
  gem.require_paths = ["lib"]
15
- gem.version = "1.2.6"
15
+ gem.version = "1.2.7"
16
16
 
17
17
  gem.add_dependency("paperclip")
18
18
 
@@ -137,4 +137,18 @@ describe "paperclip-storage-ftp", :integration => true do
137
137
  end
138
138
  end
139
139
  end
140
+
141
+ context "performance" do
142
+ let(:user) { UserWithOneServerAndDeepPath.new }
143
+ let(:padded_user_id) { user.id.to_s.rjust(3, "0") }
144
+ let(:uploaded_file_deep_path) { FtpServer::USER1_PATH + "/img/user_with_one_server_and_deep_paths/avatars/000/000/#{padded_user_id}/original/avatar.jpg" }
145
+
146
+ it "triggers minimal amount of ftp commands" do
147
+ expect_any_instance_of(Net::FTP).to receive(:nlst).exactly(7).times.and_call_original
148
+ expect_any_instance_of(Net::FTP).to receive(:mkdir).exactly(9).times.and_call_original
149
+ user.avatar = file
150
+ user.save!
151
+ File.exist?(uploaded_file_deep_path).should be true
152
+ end
153
+ end
140
154
  end
@@ -2,6 +2,7 @@ require "spec_helper"
2
2
 
3
3
  describe Paperclip::Storage::Ftp::Server do
4
4
  let(:server) { Paperclip::Storage::Ftp::Server.new }
5
+ let(:connection) { double("connection") }
5
6
 
6
7
  context "initialize" do
7
8
  it "accepts options to initialize attributes" do
@@ -25,7 +26,7 @@ describe Paperclip::Storage::Ftp::Server do
25
26
 
26
27
  context "#file_exists?" do
27
28
  before do
28
- server.stub(:connection).and_return(double("connection"))
29
+ server.stub(:connection).and_return(connection)
29
30
  end
30
31
 
31
32
  it "returns true if the file exists on the server" do
@@ -56,7 +57,7 @@ describe Paperclip::Storage::Ftp::Server do
56
57
 
57
58
  context "#get_file" do
58
59
  before do
59
- server.stub(:connection).and_return(double("connection"))
60
+ server.stub(:connection).and_return(connection)
60
61
  end
61
62
 
62
63
  it "returns the file object" do
@@ -67,19 +68,163 @@ describe Paperclip::Storage::Ftp::Server do
67
68
 
68
69
  context "#put_file" do
69
70
  before do
70
- server.stub(:connection).and_return(double("connection"))
71
+ server.stub(:connection).and_return(connection)
71
72
  end
72
73
 
73
74
  it "stores the file on the server" do
74
- server.should_receive(:mkdir_p).with("/files")
75
75
  server.connection.should_receive(:putbinaryfile).with("/tmp/original.jpg", "/files/original.jpg")
76
76
  server.put_file("/tmp/original.jpg", "/files/original.jpg")
77
77
  end
78
78
  end
79
79
 
80
+ context "#put_files" do
81
+ before do
82
+ server.stub(:connection).and_return(connection)
83
+ end
84
+
85
+ shared_examples "proper handling" do
86
+ it "passes files to #put_file" do
87
+ server.should_receive(:mktree).with(tree)
88
+ server.should_receive(:put_file).with(files.keys.first, files.values.first).ordered
89
+ server.should_receive(:put_file).with(files.keys.last, files.values.last).ordered
90
+ server.put_files files
91
+ end
92
+ end
93
+
94
+ context "common directories" do
95
+ let(:files) do
96
+ {
97
+ "/tmp/foo1.jpg" => "/bar/foo1.jpg",
98
+ "/tmp/foo2.jpg" => "/bar/foo2.jpg"
99
+ }
100
+ end
101
+ let(:tree) { { "bar"=>{} } }
102
+
103
+ include_examples "proper handling"
104
+ end
105
+
106
+ context "no common directories" do
107
+ let(:files) do
108
+ {
109
+ "/tmp/foo1.jpg" => "/bar/foo1.jpg",
110
+ "/tmp/foo2.jpg" => "/baz/foo2.jpg"
111
+ }
112
+ end
113
+ let(:tree) { { "bar"=>{}, "baz"=>{} } }
114
+
115
+ include_examples "proper handling"
116
+ end
117
+
118
+ context "exactly one file" do
119
+ let(:files) do
120
+ { "/tmp/foo1.jpg" => "/bar/foo1.jpg" }
121
+ end
122
+ let(:tree) { { "bar"=>{} } }
123
+
124
+ it "passes file to #put_file" do
125
+ server.should_receive(:mktree).with(tree)
126
+ server.should_receive(:put_file).with(files.keys.first, files.values.first)
127
+ server.put_files files
128
+ end
129
+ end
130
+
131
+ context "no files" do
132
+ let(:files) { {} }
133
+
134
+ it "does not to anything" do
135
+ server.should_not_receive(:put_file)
136
+ connection.should_not_receive(:mkdir)
137
+ server.put_files files
138
+ end
139
+ end
140
+ end
141
+
142
+ context "#directory_tree" do
143
+ let(:files) {
144
+ %w(/foo/bar1.jpg /foo/bar2.jpg /foo/bar/baz.jpg /foo/foo/bar.jpg /foobar/foobar.jpg /root.jpg)
145
+ }
146
+
147
+ it "handles empty file list" do
148
+ expect(server.directory_tree([])).to eq({})
149
+ end
150
+
151
+ it "extracts nested directory structure" do
152
+ expect(server.directory_tree(files)).to eq(
153
+ {
154
+ "foo" => {
155
+ "bar" => {},
156
+ "foo" => {}
157
+ },
158
+ "foobar"=>{}
159
+ }
160
+ )
161
+ end
162
+ end
163
+
164
+ context "#mktree" do
165
+ before do
166
+ server.stub(:connection).and_return(connection)
167
+ end
168
+ let(:tree) do
169
+ {
170
+ "foo"=>{
171
+ "bar"=>{},
172
+ "baz"=>{"qux"=>{}}},
173
+ "foobar"=>{}
174
+ }
175
+ end
176
+
177
+ it "handles empty tree" do
178
+ server.mktree({})
179
+ end
180
+
181
+ context "empty ftp tree" do
182
+ it "creates entire nested tree" do
183
+ connection.should_receive(:nlst).with("/").ordered.and_return([])
184
+ connection.should_receive(:mkdir).with("/foo").ordered
185
+ connection.should_receive(:mkdir).with("/foobar").ordered
186
+ connection.should_receive(:nlst).with("/foo/").ordered.and_return([])
187
+ connection.should_receive(:mkdir).with("/foo/bar").ordered
188
+ connection.should_receive(:mkdir).with("/foo/baz").ordered
189
+ connection.should_receive(:nlst).with("/foo/baz/").ordered.and_return([])
190
+ connection.should_receive(:mkdir).with("/foo/baz/qux").ordered
191
+ server.mktree(tree)
192
+ end
193
+ end
194
+
195
+ context "partially existent ftp tree" do
196
+ it "creates only the missing directories" do
197
+ connection.should_receive(:nlst).with("/").ordered.and_return(["foo"])
198
+ connection.should_receive(:mkdir).with("/foobar").ordered
199
+ connection.should_receive(:nlst).with("/foo/").ordered.and_return(["baz"])
200
+ connection.should_receive(:mkdir).with("/foo/bar").ordered
201
+ connection.should_receive(:nlst).with("/foo/baz/").ordered.and_return(["qux"])
202
+ server.mktree(tree)
203
+ end
204
+ end
205
+
206
+ context "intermittent creation of directories" do
207
+ let(:tree) do
208
+ {
209
+ "foo"=>{},
210
+ "bar"=>{"foobar"=>{}}
211
+ }
212
+ end
213
+
214
+ it "handles Net::FTPPermError" do
215
+ connection.should_receive(:nlst).with("/").ordered.and_return([])
216
+ connection.should_receive(:mkdir).with("/foo").ordered.and_raise(Net::FTPPermError)
217
+ connection.should_receive(:mkdir).with("/bar").ordered.and_raise(Net::FTPPermError)
218
+ connection.should_receive(:nlst).with("/bar/").ordered.and_return([])
219
+ connection.should_receive(:mkdir).with("/bar/foobar").ordered.and_raise(Net::FTPPermError)
220
+ server.mktree(tree)
221
+ end
222
+ end
223
+ end
224
+
80
225
  context "#delete_file" do
81
226
  before do
82
- server.stub(:connection).and_return(double("connection"))
227
+ server.stub(:connection).and_return(connection)
83
228
  end
84
229
 
85
230
  it "deletes the file on the server" do
@@ -96,7 +241,7 @@ describe Paperclip::Storage::Ftp::Server do
96
241
 
97
242
  context "#rmdir_p" do
98
243
  before do
99
- server.stub(:connection).and_return(double("connection"))
244
+ server.stub(:connection).and_return(connection)
100
245
  end
101
246
 
102
247
  it "deletes the directory and all parent directories" do
@@ -119,24 +264,4 @@ describe Paperclip::Storage::Ftp::Server do
119
264
  server.connection.should == ftp
120
265
  end
121
266
  end
122
-
123
- context "mkdir_p" do
124
- before do
125
- server.stub(:connection).and_return(double("connection"))
126
- end
127
-
128
- it "creates the directory and all its parent directories" do
129
- server.connection.should_receive(:mkdir).with("/").ordered
130
- server.connection.should_receive(:mkdir).with("/files").ordered
131
- server.connection.should_receive(:mkdir).with("/files/foo").ordered
132
- server.connection.should_receive(:mkdir).with("/files/foo/bar").ordered
133
- server.mkdir_p("/files/foo/bar")
134
- end
135
-
136
- it "does not stop on Net::FTPPermError" do
137
- server.connection.should_receive(:mkdir).with("/").and_raise(Net::FTPPermError)
138
- server.connection.should_receive(:mkdir).with("/files")
139
- server.mkdir_p("/files")
140
- end
141
- end
142
267
  end
@@ -100,10 +100,13 @@ describe Paperclip::Storage::Ftp do
100
100
  :thumb => thumb_file
101
101
  })
102
102
 
103
- first_server.should_receive(:put_file).with("/tmp/original/foo.jpg", "/files/original/foo.jpg")
104
- first_server.should_receive(:put_file).with("/tmp/thumb/foo.jpg", "/files/thumb/foo.jpg")
105
- second_server.should_receive(:put_file).with("/tmp/original/foo.jpg", "/files/original/foo.jpg")
106
- second_server.should_receive(:put_file).with("/tmp/thumb/foo.jpg", "/files/thumb/foo.jpg")
103
+ write_queue = {
104
+ "/tmp/original/foo.jpg" => "/files/original/foo.jpg",
105
+ "/tmp/thumb/foo.jpg" => "/files/thumb/foo.jpg"
106
+ }
107
+
108
+ first_server.should_receive(:put_files).with(write_queue)
109
+ second_server.should_receive(:put_files).with(write_queue)
107
110
 
108
111
  attachment.should_receive(:with_ftp_servers).and_yield([first_server, second_server])
109
112
 
@@ -88,6 +88,20 @@ class UserWithInvalidPort < UserBase
88
88
  end
89
89
  end
90
90
 
91
+ class UserWithOneServerAndDeepPath < UserBase
92
+ setup_avatar_attachment(avatar_options.merge(
93
+ :path => "/img/:class/:attachment/:id_partition/:style/:filename",
94
+ :ftp_servers => [
95
+ {
96
+ :host => "127.0.0.1",
97
+ :user => "user1",
98
+ :password => "secret1",
99
+ :port => 2121
100
+ }
101
+ ]
102
+ ))
103
+ end
104
+
91
105
  class UserIgnoringFailingConnection < UserWithInvalidPort
92
106
  setup_avatar_attachment(avatar_options.merge(
93
107
  :ftp_ignore_failing_connections => true
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: paperclip-storage-ftp
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.6
4
+ version: 1.2.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sebastian Röbke
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-07-22 00:00:00.000000000 Z
11
+ date: 2015-10-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: paperclip
@@ -135,6 +135,8 @@ files:
135
135
  - vendor/apache-ftpserver/res/conf/users.properties
136
136
  - vendor/apache-ftpserver/res/ftp-db.sql
137
137
  - vendor/apache-ftpserver/res/ftpserver.jks
138
+ - vendor/apache-ftpserver/res/user1/.gitkeep
139
+ - vendor/apache-ftpserver/res/user2/.gitkeep
138
140
  homepage: https://github.com/xing/paperclip-storage-ftp
139
141
  licenses:
140
142
  - MIT
@@ -155,7 +157,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
155
157
  version: '0'
156
158
  requirements: []
157
159
  rubyforge_project:
158
- rubygems_version: 2.4.7
160
+ rubygems_version: 2.4.8
159
161
  signing_key:
160
162
  specification_version: 4
161
163
  summary: Allow Paperclip attachments to be stored on FTP servers