Commit 1c3dc68da731f78b61f2923c4c022137475eab19

Authored by Antonio Terceiro
2 parents 2a045324 3af4b045

Merge branch 'dbm-private-files' into 'master'

Dbm private files

Also rewriting the file visualization to work consistently with every
file type. Here is the overall basic behavior now:

  * If the request is passed with view=true, content is displayed
    as an article content.
    * If the file has an inline visualization (like images) it's already
      displayed.
    * If not, a download link is displayed.
  * If the request is passed with view=false, the file is provided
    straight, without any noosfero layout being loaded.

  * If the file is private:
    * And the user accesses its public filesystem path, apache (this is
      done by noosfero-apache) will redirect the request to rails
      path so that the rails server will provide it considering
      appropriate permissions.
    * And the user accesses its rails path, rails server will provide as
      well.
  * If the file is public:
    * And the user accesses its public filesystem path, apache will
      provide the file.
    * And the user accesses its rails path, rails server will redirect
      to its public filesystem path so that apache provides the file.

The feature and debian package were tested and are still available for testing on: http://private-files.dev.colivre.net

See merge request !797
app/controllers/public/content_viewer_controller.rb
... ... @@ -205,8 +205,6 @@ class ContentViewerController < ApplicationController
205 205  
206 206 def rendered_file_download(view = nil)
207 207 if @page.download? view
208   - headers['Content-Type'] = @page.mime_type
209   - headers.merge! @page.download_headers
210 208 data = @page.data
211 209  
212 210 # TODO test the condition
... ... @@ -214,7 +212,12 @@ class ContentViewerController < ApplicationController
214 212 raise "No data for file"
215 213 end
216 214  
217   - render :text => data, :layout => false
  215 + if @page.published && @page.uploaded_file?
  216 + redirect_to @page.public_filename
  217 + else
  218 + send_data data, @page.download_headers
  219 + end
  220 +
218 221 return true
219 222 end
220 223  
... ...
app/models/article.rb
... ... @@ -383,6 +383,10 @@ class Article < ActiveRecord::Base
383 383 end
384 384 end
385 385  
  386 + def full_path
  387 + profile.hostname.blank? ? "/#{profile.identifier}/#{path}" : "/#{path}"
  388 + end
  389 +
386 390 def url
387 391 @url ||= self.profile.url.merge(:page => path.split('/'))
388 392 end
... ... @@ -408,17 +412,19 @@ class Article < ActiveRecord::Base
408 412 end
409 413  
410 414 def download? view = nil
411   - (self.uploaded_file? and not self.image?) or
412   - (self.image? and view.blank?) or
413   - (not self.uploaded_file? and self.mime_type != 'text/html')
  415 + false
414 416 end
415 417  
416 418 def is_followed_by?(user)
417 419 self.person_followers.include? user
418 420 end
419 421  
  422 + def download_disposition
  423 + 'inline'
  424 + end
  425 +
420 426 def download_headers
421   - {}
  427 + { :filename => filename, :type => mime_type, :disposition => download_disposition}
422 428 end
423 429  
424 430 def alternate_languages
... ...
app/models/rss_feed.rb
... ... @@ -65,6 +65,10 @@ class RssFeed < Article
65 65 'text/xml'
66 66 end
67 67  
  68 + def download?(view = nil)
  69 + true
  70 + end
  71 +
68 72 include Rails.application.routes.url_helpers
69 73 def fetch_articles
70 74 if parent && parent.has_posts?
... ...
app/models/uploaded_file.rb
... ... @@ -2,6 +2,9 @@
2 2 #
3 3 # Limitation: only file metadata are versioned. Only the latest version
4 4 # of the file itself is kept. (FIXME?)
  5 +
  6 +require 'sdbm'
  7 +
5 8 class UploadedFile < Article
6 9  
7 10 attr_accessible :uploaded_data, :title
... ... @@ -10,6 +13,19 @@ class UploadedFile &lt; Article
10 13 _('File')
11 14 end
12 15  
  16 + DBM_PRIVATE_FILE = 'cache/private_files'
  17 + after_save do |uploaded_file|
  18 + if uploaded_file.published_changed?
  19 + dbm = SDBM.open(DBM_PRIVATE_FILE)
  20 + if uploaded_file.published
  21 + dbm.delete(uploaded_file.public_filename)
  22 + else
  23 + dbm.store(uploaded_file.public_filename, uploaded_file.full_path)
  24 + end
  25 + dbm.close
  26 + end
  27 + end
  28 +
13 29 track_actions :upload_image, :after_create, :keep_params => ["view_url", "thumbnail_path", "parent.url", "parent.name"], :if => Proc.new { |a| a.published? && a.image? && !a.parent.nil? && a.parent.gallery? }, :custom_target => :parent
14 30  
15 31 def title
... ... @@ -106,10 +122,13 @@ class UploadedFile &lt; Article
106 122 self.name ||= self.filename
107 123 end
108 124  
109   - def download_headers
110   - {
111   - 'Content-Disposition' => "attachment; filename=\"#{self.filename}\"",
112   - }
  125 + def download_disposition
  126 + case content_type
  127 + when 'application/pdf'
  128 + 'inline'
  129 + else
  130 + 'attachment'
  131 + end
113 132 end
114 133  
115 134 def data
... ...
app/views/file_presenter/_generic.html.erb
... ... @@ -2,4 +2,4 @@
2 2 <%= generic.abstract %>
3 3 </div>
4 4  
5   -<%= button(:download, _('Download'), [Noosfero.root, generic.public_filename].join, class:'download-link', option:'primary', size:'lg') %>
  5 +<%= button(:download, _('Download'), generic.url, class:'download-link', option:'primary', size:'lg', :target => "_blank") %>
... ...
debian/apache2/virtualhost.conf
... ... @@ -8,6 +8,19 @@ DocumentRoot &quot;/usr/share/noosfero/public&quot;
8 8  
9 9 RewriteEngine On
10 10  
  11 +# If your XMPP XMPP/BOSH isn't in localhost, change the config below to correct
  12 +# point to address
  13 +RewriteRule /http-bind http://localhost:5280/http-bind [P,QSA,L]
  14 +<Proxy http://localhost:5280/http-bind>
  15 + Order Allow,Deny
  16 + Allow from All
  17 +</Proxy>
  18 +
  19 +# Pass access to private files to backend
  20 +RewriteMap private_files "dbm=sdbm:/usr/share/noosfero/cache/private_files"
  21 +RewriteCond ${private_files:$1|NOT_FOUND} !NOT_FOUND
  22 +RewriteRule ^(/articles/.*) ${private_files:$1} [P,QSA,L]
  23 +
11 24 # Rewrite index to check for static index.html
12 25 RewriteRule ^/$ /index.html [QSA]
13 26  
... ...
debian/dbinstall
... ... @@ -5,8 +5,6 @@ set -e
5 5 # dbconfig-common uses "pgsql", but we want "postgresql"
6 6 sed -i -e 's/adapter: pgsql/adapter: postgresql/' /etc/noosfero/database.yml
7 7  
8   -/etc/init.d/noosfero setup
9   -
10 8 cd /usr/share/noosfero && su noosfero -c "rake db:schema:load RAILS_ENV=production"
11 9 cd /usr/share/noosfero && su noosfero -c "rake db:migrate RAILS_ENV=production SCHEMA=/dev/null"
12 10 cd /usr/share/noosfero && su noosfero -c "rake db:data:minimal RAILS_ENV=production"
... ...
debian/dbupgrade
... ... @@ -2,7 +2,5 @@
2 2  
3 3 set -e
4 4  
5   -/etc/init.d/noosfero setup
6   -
7 5 cd /usr/share/noosfero && su noosfero -c "rake db:migrate RAILS_ENV=production SCHEMA=/dev/null"
8 6  
... ...
debian/noosfero.links
1 1 var/tmp/noosfero usr/share/noosfero/tmp
2 2 var/log/noosfero usr/share/noosfero/log
  3 +var/cache/noosfero usr/share/noosfero/cache
3 4 etc/noosfero/database.yml usr/share/noosfero/config/database.yml
4 5 etc/noosfero/unicorn.rb usr/share/noosfero/config/unicorn.rb
5 6 etc/noosfero/plugins usr/share/noosfero/config/plugins
... ...
debian/noosfero.postinst
... ... @@ -68,10 +68,17 @@ if [ ! -z &quot;$RET&quot; ]; then
68 68 export NOOSFERO_DOMAIN="$RET"
69 69 fi
70 70  
  71 +/etc/init.d/noosfero setup
  72 +
71 73 # dbconfig-common magic
72 74 . /usr/share/dbconfig-common/dpkg/postinst
73 75 dbc_go noosfero $@
74 76  
  77 +if [ ! -f /usr/share/noosfero/cache/private_files.pag ] && [ $1 = "configure" ] && [ -n $2 ]; then
  78 + echo "Creating private files dbm map..."
  79 + cd /usr/share/noosfero && su noosfero -c "rake cache:private_files RAILS_ENV=production"
  80 +fi
  81 +
75 82 # stop debconf to avoid the problem with infinite hanging, cfe
76 83 # http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=295477
77 84 db_stop
... ...
debian/update-noosfero-apache
... ... @@ -17,13 +17,13 @@ if test -x /usr/share/noosfero/script/apacheconf; then
17 17 if ! test -e "$apache_site"; then
18 18 echo "Generating apache virtual host ..."
19 19 cd /usr/share/noosfero && su noosfero -c "RAILS_ENV=production ./script/apacheconf virtualhosts" > "$apache_site"
20   - else
21   - pattern="Include \/etc\/noosfero\/apache\/virtualhost.conf"
22   - include="Include \/usr\/share\/noosfero\/util\/chat\/apache\/xmpp.conf"
23   - if ! cat $apache_site | grep "^ *$include" > /dev/null ; then
24   - echo "Updating apache virtual host ..."
25   - sed -i "s/.*$pattern.*/ $include\n&/" $apache_site
26   - fi
  20 + fi
  21 +
  22 + # remove old way to include chat config
  23 + pattern="Include \/usr\/share\/noosfero\/util\/chat\/apache\/xmpp.conf"
  24 + if cat $apache_site | grep "^ *$pattern" > /dev/null ; then
  25 + echo "Removing obsolete chat configuration ..."
  26 + sed -i "/.*$pattern.*/d" $apache_site
27 27 fi
28 28  
29 29 echo 'Noosfero Apache configuration updated.'
... ...
etc/init.d/noosfero
... ... @@ -86,6 +86,13 @@ do_setup() {
86 86 chmod 750 /var/tmp/noosfero
87 87 fi
88 88  
  89 + # Noosfero cache directory
  90 + if [ ! -d /var/cache/noosfero ]; then
  91 + mkdir /var/cache/noosfero
  92 + chown $NOOSFERO_USER:root /var/cache/noosfero
  93 + chmod 755 /var/cache/noosfero
  94 + fi
  95 +
89 96 # symlink the directories into Noosfero directory
90 97 if [ ! -e $NOOSFERO_DIR/tmp ]; then
91 98 ln -s /var/tmp/noosfero $NOOSFERO_DIR/tmp
... ... @@ -96,6 +103,9 @@ do_setup() {
96 103 if [ ! -e $NOOSFERO_DIR/log ]; then
97 104 ln -s /var/log/noosfero $NOOSFERO_DIR/log
98 105 fi
  106 + if [ ! -e $NOOSFERO_DIR/cache ]; then
  107 + ln -s /var/cache/noosfero $NOOSFERO_DIR/cache
  108 + fi
99 109 }
100 110  
101 111 do_start() {
... ...
lib/file_presenter.rb
... ... @@ -52,8 +52,8 @@ class FilePresenter
52 52 nil
53 53 end
54 54  
55   - def download?(view=nil)
56   - false
  55 + def download? view = nil
  56 + view.blank?
57 57 end
58 58  
59 59 def short_description
... ...
lib/tasks/cache.rake 0 → 100644
... ... @@ -0,0 +1,14 @@
  1 +namespace :cache do
  2 + task :private_files => :environment do
  3 + require 'sdbm'
  4 +
  5 + hash = {}
  6 + UploadedFile.where(:published => false).find_each do |uploaded_file|
  7 + hash[uploaded_file.public_filename] = uploaded_file.full_path
  8 + end
  9 +
  10 + dbm = SDBM.open(UploadedFile::DBM_PRIVATE_FILE)
  11 + dbm.update(hash)
  12 + dbm.close
  13 + end
  14 +end
... ...
script/apacheconf
... ... @@ -17,7 +17,6 @@ when &#39;virtualhosts&#39;
17 17 puts " #{server_directive} #{domain.name}"
18 18 server_directive = 'ServerAlias'
19 19 end
20   - puts " Include /usr/share/noosfero/util/chat/apache/xmpp.conf"
21 20 puts " Include /etc/noosfero/apache/virtualhost.conf"
22 21 puts "</VirtualHost>"
23 22 end
... ...
test/functional/content_viewer_controller_test.rb
... ... @@ -51,27 +51,26 @@ class ContentViewerControllerTest &lt; ActionController::TestCase
51 51 assert_response :missing
52 52 end
53 53  
54   - should 'produce a download-link when article is a uploaded file' do
  54 + should 'produce a download-link when view page is true' do
55 55 profile = create_user('someone').person
56 56 html = UploadedFile.create! :uploaded_data => fixture_file_upload('/files/500.html', 'text/html'), :profile => profile
57 57 html.save!
58 58  
59   - get :view_page, :profile => 'someone', :page => [ '500.html' ]
  59 + get :view_page, :profile => 'someone', :page => [ '500.html' ], :view => true
60 60  
61 61 assert_response :success
62   - assert_match /#{html.public_filename}/, @response.body
  62 + assert_select "a[href=#{html.full_path}]"
63 63 end
64 64  
65   - should 'download file when article is image' do
  65 + should 'download file when view page is blank' do
66 66 profile = create_user('someone').person
67 67 image = UploadedFile.create! :uploaded_data => fixture_file_upload('/files/rails.png', 'image/png'), :profile => profile
68 68 image.save!
69 69  
70 70 get :view_page, :profile => 'someone', :page => [ 'rails.png' ]
71 71  
72   - assert_response :success
73   - assert_not_nil assigns(:page).data
74   - assert_match /image\/png/, @response.headers['Content-Type']
  72 + assert_response :redirect
  73 + assert_redirected_to image.public_filename
75 74 end
76 75  
77 76 should 'display image on a page when article is image and has a view param' do
... ...
test/unit/article_block_test.rb
... ... @@ -140,6 +140,8 @@ class ArticleBlockTest &lt; ActiveSupport::TestCase
140 140 block.article = file
141 141 block.save!
142 142  
  143 + UploadedFile.any_instance.stubs(:url).returns('myhost.mydomain/path/to/file')
  144 +
143 145 assert_tag_in_string instance_eval(&block.content), :tag => 'a', :content => _('Download')
144 146 end
145 147  
... ...
test/unit/article_test.rb
... ... @@ -2241,4 +2241,15 @@ class ArticleTest &lt; ActiveSupport::TestCase
2241 2241 assert !a.display_preview?
2242 2242 end
2243 2243  
  2244 + should 'return full_path' do
  2245 + p1 = fast_create(Profile)
  2246 + p2 = fast_create(Profile)
  2247 + p2.domains << Domain.create!(:name => 'p2.domain')
  2248 + a1 = fast_create(Article, :profile_id => p1.id)
  2249 + a2 = fast_create(Article, :profile_id => p2.id)
  2250 +
  2251 + assert_equal "/#{p1.identifier}/#{a1.path}", a1.full_path
  2252 + assert_equal "/#{a2.path}", a2.full_path
  2253 + end
  2254 +
2244 2255 end
... ...
test/unit/blog_helper_test.rb
... ... @@ -101,11 +101,9 @@ class BlogHelperTest &lt; ActionView::TestCase
101 101  
102 102 should 'display link to file if post is an uploaded_file' do
103 103 file = create(UploadedFile, :uploaded_data => fixture_file_upload('/files/test.txt', 'text/plain'), :profile => profile, :published => true, :parent => blog)
104   -
105 104 result = display_post(file)
106   - assert_tag_in_string result, :tag => 'a',
107   - :attributes => { :href => file.public_filename },
108   - :content => _('Download')
  105 +
  106 + assert_tag_in_string result, :tag => 'a', :content => _('Download')
109 107 end
110 108  
111 109 should 'display image if post is an image' do
... ...
test/unit/uploaded_file_test.rb
... ... @@ -357,4 +357,25 @@ class UploadedFileTest &lt; ActiveSupport::TestCase
357 357 assert_instance_of Fixnum, UploadedFile.max_size
358 358 end
359 359  
  360 + should 'add file to dbm if it becomes private' do
  361 + require 'sdbm'
  362 + public_file = create(UploadedFile, :uploaded_data => fixture_file_upload('/files/test.txt', 'text/plain'), :profile => profile, :published => true)
  363 + private_file = create(UploadedFile, :uploaded_data => fixture_file_upload('/files/rails.png', 'image/png'), :profile => profile, :published => false)
  364 +
  365 + dbm = SDBM.open(UploadedFile::DBM_PRIVATE_FILE)
  366 + assert !dbm.has_key?(public_file.public_filename)
  367 + assert dbm.has_key?(private_file.public_filename)
  368 + dbm.close
  369 +
  370 + public_file.published = false
  371 + public_file.save!
  372 + private_file.published = true
  373 + private_file.save!
  374 +
  375 + dbm = SDBM.open(UploadedFile::DBM_PRIVATE_FILE)
  376 + assert dbm.has_key?(public_file.public_filename)
  377 + assert !dbm.has_key?(private_file.public_filename)
  378 + dbm.close
  379 + end
  380 +
360 381 end
... ...
util/chat/apache/xmpp.conf
... ... @@ -1,11 +0,0 @@
1   -# If your XMPP XMPP/BOSH isn't in localhost, change the config below to correct
2   -# point to address
3   -
4   - RewriteEngine On
5   - RewriteRule /http-bind http://localhost:5280/http-bind [P,QSA,L]
6   - <Proxy http://localhost:5280/http-bind>
7   - Order Allow,Deny
8   - Allow from All
9   - </Proxy>
10   -
11   -# vim: ft=apache