From 9603550aabbaf6e72e73169b096bba33249367ec Mon Sep 17 00:00:00 2001 From: Christoph Haas Date: Fri, 24 Jun 2016 16:18:36 +0200 Subject: [PATCH 01/24] /thumbnail-with-version/package/version implemented --- app/controllers/packages_controller.rb | 26 ++++++++++++++++++++++++++ config/routes.rb | 1 + 2 files changed, 27 insertions(+) diff --git a/app/controllers/packages_controller.rb b/app/controllers/packages_controller.rb index 626d230..b7b7043 100644 --- a/app/controllers/packages_controller.rb +++ b/app/controllers/packages_controller.rb @@ -94,6 +94,32 @@ class PackagesController < ApplicationController send_file @screenshot.image.path(:thumb), type: "image/png", disposition: 'inline' end + # Similar to 'def thumbnail'. But tries to find a screenshot that matches the given version best. + # This algorithm collects all image + # versions of a package and determines the (second) newest version. + # E.g. if there are version 1.0 and 2.0 and the user is looking for + # a screenshot of version 1.5 then the 1.0 version is returned. + # This way the user does not see a screenshot of version 2.0 because + # 2.0 might contain features that were not there in version 1.5. + def thumbnail_with_version + @package = Package.find_by(name: params[:name]) + unless @package + thumbnail404 + return + end + @screenshot = @package.best_screenshot_for_version(params[:version]) + + # Return a 404 if the package has no screenshots or the image was not found + unless @screenshot and @screenshot.image.path + thumbnail404 + return + end + + # Send the thumbnail + # TODO: Make sure it uses X-Sendfile correctly in production + send_file @screenshot.image.path(:thumb), type: "image/png", disposition: 'inline' + end + # Receives a form with a simple text field 'description' so that users can update # the description of their screenshot. def update_screenshot_description diff --git a/config/routes.rb b/config/routes.rb index e1f1d01..ad07a4b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -27,6 +27,7 @@ Debshots::Application.routes.draw do get 'approve_screenshot/:id' => 'packages#approve_screenshot', as: :approve_screenshot get 'about' => 'welcome#about' get 'thumbnail/:name' => 'packages#thumbnail', as: :thumbnail_image, name: /[^\/]+/ + get 'thumbnail-with-version/:name/:version' => 'packages#thumbnail_with_version', name: /[^\/]+/, version: /\S+/ # The priority is based upon order of creation: first created -> highest priority. # See how all your routes lay out with "rake routes". From bac90cbd2f3c48888c679645680a5dac028cb8ed Mon Sep 17 00:00:00 2001 From: Christoph Haas Date: Fri, 24 Jun 2016 16:19:05 +0200 Subject: [PATCH 02/24] Do not crash when sorting screenshots by version if the version is empty --- app/models/package.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/models/package.rb b/app/models/package.rb index 7e532df..04b51ea 100644 --- a/app/models/package.rb +++ b/app/models/package.rb @@ -65,6 +65,8 @@ class Package < ActiveRecord::Base private def version_compare(x,y) + x ||= '0' + y ||= '0' version_x = DebImporter::Version.new(x) version_y = DebImporter::Version.new(y) if version_x Date: Fri, 24 Jun 2016 16:27:04 +0200 Subject: [PATCH 03/24] Added legacy URLs /without_screenshots and /with_screenshots --- config/routes.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/config/routes.rb b/config/routes.rb index ad07a4b..c2e9db3 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -29,6 +29,10 @@ Debshots::Application.routes.draw do get 'thumbnail/:name' => 'packages#thumbnail', as: :thumbnail_image, name: /[^\/]+/ get 'thumbnail-with-version/:name/:version' => 'packages#thumbnail_with_version', name: /[^\/]+/, version: /\S+/ + # Legacy URLs + get 'with_screenshots', to: redirect('/packages?show=with') + get 'without_screenshots', to: redirect('/packages?show=without') + # The priority is based upon order of creation: first created -> highest priority. # See how all your routes lay out with "rake routes". From 69499addafd130a833df5831aca0b49f7982e2eb Mon Sep 17 00:00:00 2001 From: Christoph Haas Date: Fri, 24 Jun 2016 16:27:14 +0200 Subject: [PATCH 04/24] Working nginx vhost configuration added to docs --- README.rdoc | 126 +++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 114 insertions(+), 12 deletions(-) diff --git a/README.rdoc b/README.rdoc index 63a0261..47523be 100644 --- a/README.rdoc +++ b/README.rdoc @@ -114,23 +114,125 @@ https://www.phusionpassenger.com/library/walkthroughs/deploy/ruby/ownserver/ngin Example nginx vhost config: +## +# You should look at the following URL's in order to grasp a solid understanding +# of Nginx configuration files in order to fully unleash the power of Nginx. +# http://wiki.nginx.org/Pitfalls +# http://wiki.nginx.org/QuickStart +# http://wiki.nginx.org/Configuration +# +# Generally, you will want to move this file somewhere, and start with a clean +# file but keep this around for reference. Or just disable in sites-enabled. +# +# Please see /usr/share/doc/nginx-doc/examples/ for more detailed examples. +## + +#proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=assets:768m use_temp_path=off; + +#add_header X-Cache-Status $upstream_cache_status; +#add_header X-Runtime 42; +more_clear_headers 'X-Runtime'; +more_clear_headers 'X-Powered-By'; +more_clear_headers 'Server'; +server_tokens off; + +#passenger_show_version_in_header off; + +#log_format proxy '[$time_local] Cache: $upstream_cache_status $upstream_addr $upstream_response_time $status $bytes_sent $proxy_add_x_forwarded_for $request_uri "$http_referer" "$http_user_agent"'; +#access_log /var/log/nginx/debshots-access.log proxy; + +# Default server configuration +# +#passenger_log_level 5; + server { - listen 85.25.83.22:80 default_server; + listen 85.25.83.22:80 default_server; + #listen 80 default_server; + #listen [::]:80 default_server; + + # SSL configuration + # + # listen 443 ssl default_server; + # listen [::]:443 ssl default_server; + # + # Self signed certs generated by the ssl-cert package + # Don't use them in a production server! + # + # include snippets/snakeoil.conf; + passenger_enabled on; passenger_ruby /home/debshots/.rbenv/versions/2.3.1/bin/ruby; - root /home/debshots/debshots/public; - server_name _; + #passenger_pass_header X-Accel-Redirect; + + #proxy_cache_methods GET HEAD; + + root /home/debshots/debshots/public; + + # Add index.php to the list if you are using PHP + #index index.html index.htm index.nginx-debian.html; + + server_name _; + #proxy_cache assets; + #add_header X-Cache-Status $upstream_cache_status; + #proxy_ignore_headers Cache-Control; + + #add_header X-Debian rocks; + + location /assets/ { + alias /home/debshots/debshots/public/assets/; + expires 1h; + #include debshots_params; + add_header X-Coffee assets; + add_header Cache-Control public; + } + location /logo/ { + alias /home/debshots/debshots/public/logo/; + expires 1h; + add_header X-Coffee logo; + add_header Cache-Control public; + } + location /screenshots/ { + alias /home/debshots/debshots/public/screenshots/; + expires max; + add_header X-Coffee screenshots; + add_header Cache-Control public; + } + location /images/ { + alias /home/debshots/debshots/public/images/; + expires 1h; + add_header X-Coffee images; + add_header Cache-Control public; + } + + # Send thumbnails using X-Sendfile / X-Accel-Redirect + # The correct thumbnail is computed by the Rails application so it cannot be served directly. + location /thumbnail/ { + expires 1d; + add_header X-Coffee thumbnail; + add_header Cache-Control public; + + passenger_set_header X-Sendfile-Type "X-Accel-Redirect"; + passenger_env_var HTTP_X_ACCEL_MAPPING /home/debshots/debshots/public/=/__send_file_accel/; + passenger_pass_header X-Accel-Redirect; + } + + # Send public assets using X-Sendfile / X-Accel-Redirect (e.g. public/images/dummy/...) + location /public/ { + expires 1h; + add_header X-Coffee public; + add_header Cache-Control public; + + passenger_set_header X-Sendfile-Type "X-Accel-Redirect"; + passenger_env_var HTTP_X_ACCEL_MAPPING /home/debshots/debshots/public/=/__send_file_accel/; + passenger_pass_header X-Accel-Redirect; + } + + location /__send_file_accel/ { + internal; + alias /home/debshots/debshots/public/; + } } -=== Enable nginx caching - -mkdir /var/cache/nginx - -chown www-data.www-data /var/cache/nginx - -nginx config… - -proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=my_cache:10m max_size=10g inactive=60m use_temp_path=off; == Supported URL paths (aka routes) From bed96ed633feb18e98acb1c4eb38a1bb91e907ce Mon Sep 17 00:00:00 2001 From: Christoph Haas Date: Fri, 24 Jun 2016 16:41:02 +0200 Subject: [PATCH 05/24] Renamed templates for list and grid view to avoid manual call of render() --- app/controllers/packages_controller.rb | 5 +-- app/views/packages/index-grid.slim | 28 ---------------- app/views/packages/index-list.slim | 46 -------------------------- 3 files changed, 3 insertions(+), 76 deletions(-) delete mode 100644 app/views/packages/index-grid.slim delete mode 100644 app/views/packages/index-list.slim diff --git a/app/controllers/packages_controller.rb b/app/controllers/packages_controller.rb index b7b7043..7452ff9 100644 --- a/app/controllers/packages_controller.rb +++ b/app/controllers/packages_controller.rb @@ -3,13 +3,14 @@ class PackagesController < ApplicationController def list @packages = query_packages @packages = @packages.paginate(page: params[:page], per_page: 6) - render 'packages/index-list.slim' + # render 'packages/index-list.slim' end def grid @packages = query_packages @packages = @packages.paginate(page: params[:page], per_page: 24) - render 'packages/index-grid.slim' + # render 'packages/index-grid.slim' + respond_to :html, :json end def details diff --git a/app/views/packages/index-grid.slim b/app/views/packages/index-grid.slim deleted file mode 100644 index 61ae37c..0000000 --- a/app/views/packages/index-grid.slim +++ /dev/null @@ -1,28 +0,0 @@ -// Paginator and search bar -.row - .small-6.medium-8.large-9.columns - // Paginator - = render 'packages/paginator' - - .small-6.medium-4.large-3.columns - // Search form - = render 'packages/searchfield' - -// Grid view -.row - .small-9.large-10.columns - - if @packages.length>0 - .row.small-up-1.medium-up-3.large-up-4.grid-thumbnails - - @packages.all.each do |pkg| - .column - = render partial: 'packages/grid_thumbnail', locals: { pkg: pkg } - - - else - = render 'packages/noresults' - - // Paginator - = render 'packages/paginator' - - // Icon sidebar for options and views - .small-3.large-2.columns - = render 'packages/sidebar' diff --git a/app/views/packages/index-list.slim b/app/views/packages/index-list.slim deleted file mode 100644 index 99ae8f1..0000000 --- a/app/views/packages/index-list.slim +++ /dev/null @@ -1,46 +0,0 @@ -// Paginator and search bar -.row - .small-6.medium-8.large-9.columns - // Paginator - = render 'packages/paginator' - - .small-6.medium-4.large-3.columns - // Search form - = render 'packages/searchfield' - -// Package list -.row.packagepage - .small-9.medium-10.columns - - if @packages.length>0 - - @packages.to_a.each do |pkg| - .row.listview - .small-12.medium-4.columns - a.black href=package_path(name: pkg.name) - .pkgname - =pkg.name - ' > - - if pkg.screenshots.any? - // TODO: smarter search for the best screenshot instead of taking the first one - - screenshot = pkg.screenshots.first - a.black title=screenshot.caption href=package_path(name: pkg.name) - = image_tag(screenshot.image.url(:large, timestamp: false), alt: screenshot.caption) - - if pkg.screenshots.length > 1 - a.black href=package_path(name: pkg.name) - .text-center and #{pluralize(pkg.screenshots.length-1, 'screenshot')} more… - - else - a href=package_path(name: pkg.name) - img.screenshot src="/images/dummy/no-screenshots-upload-one.svg" - .small-12.medium-8.columns - .row.packagedetails - .subtitle = pkg.description - = pkg.long_description or 'Sorry - no description available.' - - // Second paginator at the bottom so the user does not have to scroll up again - = render 'packages/paginator' - - - else - = render 'packages/noresults' - - // Icon sidebar for options and views - .small-3.medium-2.columns - = render 'packages/sidebar' From 21c40f92d3bfffa5de051fced2c9082bf34cb443 Mon Sep 17 00:00:00 2001 From: Christoph Haas Date: Mon, 27 Jun 2016 15:59:15 +0200 Subject: [PATCH 06/24] Typo fixed --- README.rdoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rdoc b/README.rdoc index 47523be..e10f8e9 100644 --- a/README.rdoc +++ b/README.rdoc @@ -317,7 +317,7 @@ server { /screenshot/:package /screenshot-404/:package (deprecated) -* Return a small (320x240 pixel) PNG screenshot file of a package +* Return a large (320x240 pixel) PNG screenshot file of a package * If the package does not have any screenshots then return a 404 code /screenshot-with-version/:package/:version From 04c5009b5de11d3a658dd857c58559382253afac Mon Sep 17 00:00:00 2001 From: Christoph Haas Date: Mon, 27 Jun 2016 15:59:42 +0200 Subject: [PATCH 07/24] Simplification refactoring --- app/controllers/packages_controller.rb | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/app/controllers/packages_controller.rb b/app/controllers/packages_controller.rb index 7452ff9..dcd1a2d 100644 --- a/app/controllers/packages_controller.rb +++ b/app/controllers/packages_controller.rb @@ -1,16 +1,11 @@ class PackagesController < ApplicationController def list - @packages = query_packages - @packages = @packages.paginate(page: params[:page], per_page: 6) - # render 'packages/index-list.slim' + @packages = query_packages.paginate(page: params[:page], per_page: 6) end def grid - @packages = query_packages - @packages = @packages.paginate(page: params[:page], per_page: 24) - # render 'packages/index-grid.slim' - respond_to :html, :json + @packages = query_packages.paginate(page: params[:page], per_page: 24) end def details From e864415a03a54cfd90dfe523275c162a870c1254 Mon Sep 17 00:00:00 2001 From: Christoph Haas Date: Mon, 27 Jun 2016 15:59:51 +0200 Subject: [PATCH 08/24] Docs added --- app/models/package.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/models/package.rb b/app/models/package.rb index 04b51ea..2af10ea 100644 --- a/app/models/package.rb +++ b/app/models/package.rb @@ -53,6 +53,12 @@ class Package < ActiveRecord::Base end # Return the newest screenshot that is not newer than the given version. + # This algorithm collects all image + # versions of a package and determines the (second) newest version. + # E.g. if there are version 1.0 and 2.0 and the user is looking for + # a screenshot of version 1.5 then the 1.0 version is returned. + # This way the user does not see a screenshot of version 2.0 because + # 2.0 might contain features that were not there in version 1.5. def best_screenshot_for_version(version) sorted_screenshots = self.screenshots_sorted_by_version sorted_screenshots.each do |ss| From 0c4a0d9a3a0d96c32b4ea2757ed13028bf41dcc9 Mon Sep 17 00:00:00 2001 From: Christoph Haas Date: Mon, 27 Jun 2016 16:00:28 +0200 Subject: [PATCH 09/24] Joined thumbnail_with_version with thumbnail dev to remove redundant code --- app/controllers/packages_controller.rb | 31 +++++--------------------- config/routes.rb | 2 +- 2 files changed, 7 insertions(+), 26 deletions(-) diff --git a/app/controllers/packages_controller.rb b/app/controllers/packages_controller.rb index dcd1a2d..1275edd 100644 --- a/app/controllers/packages_controller.rb +++ b/app/controllers/packages_controller.rb @@ -77,34 +77,15 @@ class PackagesController < ApplicationController thumbnail404 return end - @screenshot = @package.screenshots.first - # Return a 404 if the package has no screenshots or the image was not found - unless @screenshot and @screenshot.image.path - thumbnail404 - return + # Called as /thumbnail-with-version/:name/:version + if params[:version] + @screenshot = @package.best_screenshot_for_version(params[:version]) + # Called as /thumbnail/:name + else + @screenshot = @package.screenshots.first end - # Send the thumbnail - # TODO: Make sure it uses X-Sendfile correctly in production - send_file @screenshot.image.path(:thumb), type: "image/png", disposition: 'inline' - end - - # Similar to 'def thumbnail'. But tries to find a screenshot that matches the given version best. - # This algorithm collects all image - # versions of a package and determines the (second) newest version. - # E.g. if there are version 1.0 and 2.0 and the user is looking for - # a screenshot of version 1.5 then the 1.0 version is returned. - # This way the user does not see a screenshot of version 2.0 because - # 2.0 might contain features that were not there in version 1.5. - def thumbnail_with_version - @package = Package.find_by(name: params[:name]) - unless @package - thumbnail404 - return - end - @screenshot = @package.best_screenshot_for_version(params[:version]) - # Return a 404 if the package has no screenshots or the image was not found unless @screenshot and @screenshot.image.path thumbnail404 diff --git a/config/routes.rb b/config/routes.rb index c2e9db3..f591dc0 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -27,7 +27,7 @@ Debshots::Application.routes.draw do get 'approve_screenshot/:id' => 'packages#approve_screenshot', as: :approve_screenshot get 'about' => 'welcome#about' get 'thumbnail/:name' => 'packages#thumbnail', as: :thumbnail_image, name: /[^\/]+/ - get 'thumbnail-with-version/:name/:version' => 'packages#thumbnail_with_version', name: /[^\/]+/, version: /\S+/ + get 'thumbnail-with-version/:name/:version' => 'packages#thumbnail', name: /[^\/]+/, version: /\S+/ # Legacy URLs get 'with_screenshots', to: redirect('/packages?show=with') From d6bfba2e8379b13ee164bfd86dd761b351f9e12d Mon Sep 17 00:00:00 2001 From: Christoph Haas Date: Mon, 27 Jun 2016 16:00:57 +0200 Subject: [PATCH 10/24] Added package views that got lost during renaming them --- app/views/packages/grid.slim | 28 ++++++++++++++++++++++ app/views/packages/list.slim | 46 ++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 app/views/packages/grid.slim create mode 100644 app/views/packages/list.slim diff --git a/app/views/packages/grid.slim b/app/views/packages/grid.slim new file mode 100644 index 0000000..61ae37c --- /dev/null +++ b/app/views/packages/grid.slim @@ -0,0 +1,28 @@ +// Paginator and search bar +.row + .small-6.medium-8.large-9.columns + // Paginator + = render 'packages/paginator' + + .small-6.medium-4.large-3.columns + // Search form + = render 'packages/searchfield' + +// Grid view +.row + .small-9.large-10.columns + - if @packages.length>0 + .row.small-up-1.medium-up-3.large-up-4.grid-thumbnails + - @packages.all.each do |pkg| + .column + = render partial: 'packages/grid_thumbnail', locals: { pkg: pkg } + + - else + = render 'packages/noresults' + + // Paginator + = render 'packages/paginator' + + // Icon sidebar for options and views + .small-3.large-2.columns + = render 'packages/sidebar' diff --git a/app/views/packages/list.slim b/app/views/packages/list.slim new file mode 100644 index 0000000..99ae8f1 --- /dev/null +++ b/app/views/packages/list.slim @@ -0,0 +1,46 @@ +// Paginator and search bar +.row + .small-6.medium-8.large-9.columns + // Paginator + = render 'packages/paginator' + + .small-6.medium-4.large-3.columns + // Search form + = render 'packages/searchfield' + +// Package list +.row.packagepage + .small-9.medium-10.columns + - if @packages.length>0 + - @packages.to_a.each do |pkg| + .row.listview + .small-12.medium-4.columns + a.black href=package_path(name: pkg.name) + .pkgname + =pkg.name + ' > + - if pkg.screenshots.any? + // TODO: smarter search for the best screenshot instead of taking the first one + - screenshot = pkg.screenshots.first + a.black title=screenshot.caption href=package_path(name: pkg.name) + = image_tag(screenshot.image.url(:large, timestamp: false), alt: screenshot.caption) + - if pkg.screenshots.length > 1 + a.black href=package_path(name: pkg.name) + .text-center and #{pluralize(pkg.screenshots.length-1, 'screenshot')} more… + - else + a href=package_path(name: pkg.name) + img.screenshot src="/images/dummy/no-screenshots-upload-one.svg" + .small-12.medium-8.columns + .row.packagedetails + .subtitle = pkg.description + = pkg.long_description or 'Sorry - no description available.' + + // Second paginator at the bottom so the user does not have to scroll up again + = render 'packages/paginator' + + - else + = render 'packages/noresults' + + // Icon sidebar for options and views + .small-3.medium-2.columns + = render 'packages/sidebar' From 205c62477878c88b9350eaaf006987159cf9ee06 Mon Sep 17 00:00:00 2001 From: Christoph Haas Date: Mon, 27 Jun 2016 17:38:24 +0200 Subject: [PATCH 11/24] Delivering proper PNG 404 images for thumbnails and large images --- app/controllers/packages_controller.rb | 42 ++++++++++++++++-- config/routes.rb | 2 + public/images/dummy/screenshot404.png | Bin 0 -> 30191 bytes ...enshots-available.png => thumbnail404.png} | Bin 4 files changed, 40 insertions(+), 4 deletions(-) create mode 100644 public/images/dummy/screenshot404.png rename public/images/dummy/{no-screenshots-available.png => thumbnail404.png} (100%) diff --git a/app/controllers/packages_controller.rb b/app/controllers/packages_controller.rb index 1275edd..b7cafbb 100644 --- a/app/controllers/packages_controller.rb +++ b/app/controllers/packages_controller.rb @@ -92,11 +92,39 @@ class PackagesController < ApplicationController return end - # Send the thumbnail - # TODO: Make sure it uses X-Sendfile correctly in production + # Send the thumbnail (uses X-Sendfile or similar if possible) send_file @screenshot.image.path(:thumb), type: "image/png", disposition: 'inline' end + # Returns a large screenshot image if posssible. + # If the package is not found it returns a dummy image along with status 404. + # If the package is found but has no screenshots then it also returns a + # dummy image along with status 404. + def screenshot + @package = Package.find_by(name: params[:name]) + unless @package + screenshot404 + return + end + + # Called as /thumbnail-with-version/:name/:version + if params[:version] + @screenshot = @package.best_screenshot_for_version(params[:version]) + # Called as /thumbnail/:name + else + @screenshot = @package.screenshots.first + end + + # Return a 404 if the package has no screenshots or the image was not found + unless @screenshot and @screenshot.image.path + screenshot404 + return + end + + # Send the thumbnail (uses X-Sendfile or similar if possible) + send_file @screenshot.image.path(:large), type: "image/png", disposition: 'inline' + end + # Receives a form with a simple text field 'description' so that users can update # the description of their screenshot. def update_screenshot_description @@ -120,9 +148,15 @@ class PackagesController < ApplicationController private # Send a dummy thumbnail reading "No screenshot available. Sorry." - # TODO: Make sure it uses X-Sendfile correctly in production def thumbnail404 - send_file Rails.root.join('public/images/dummy/no-screenshots-available.png'), + send_file Rails.root.join('public/images/dummy/thumbnail404.png'), + type: "image/png", + disposition: 'inline', + status: 404 + end + + def screenshot404 + send_file Rails.root.join('public/images/dummy/screenshot404.png'), type: "image/png", disposition: 'inline', status: 404 diff --git a/config/routes.rb b/config/routes.rb index f591dc0..b997f73 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -28,6 +28,8 @@ Debshots::Application.routes.draw do get 'about' => 'welcome#about' get 'thumbnail/:name' => 'packages#thumbnail', as: :thumbnail_image, name: /[^\/]+/ get 'thumbnail-with-version/:name/:version' => 'packages#thumbnail', name: /[^\/]+/, version: /\S+/ + get 'screenshot/:name' => 'packages#screenshot', as: :screenshot_image, name: /[^\/]+/ + get 'screenshot-with-version/:name/:version' => 'packages#screenshot', name: /[^\/]+/, version: /\S+/ # Legacy URLs get 'with_screenshots', to: redirect('/packages?show=with') diff --git a/public/images/dummy/screenshot404.png b/public/images/dummy/screenshot404.png new file mode 100644 index 0000000000000000000000000000000000000000..18b455d9b680ef165bfca4e8f583412b597c35af GIT binary patch literal 30191 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 M{fOFqRomeI1By;#jQ{`u literal 0 HcmV?d00001 diff --git a/public/images/dummy/no-screenshots-available.png b/public/images/dummy/thumbnail404.png similarity index 100% rename from public/images/dummy/no-screenshots-available.png rename to public/images/dummy/thumbnail404.png From 54ee5d80a7c780c1901d13b3a724d7333d3bca63 Mon Sep 17 00:00:00 2001 From: Christoph Haas Date: Mon, 27 Jun 2016 18:05:56 +0200 Subject: [PATCH 12/24] Cruft removed --- app/assets/stylesheets/_settings.scss | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/assets/stylesheets/_settings.scss b/app/assets/stylesheets/_settings.scss index df8abd4..b5fdc5e 100644 --- a/app/assets/stylesheets/_settings.scss +++ b/app/assets/stylesheets/_settings.scss @@ -952,6 +952,3 @@ a.black margin-left: 2em; margin-bottom: 0.5em; } - - - From c69a4270e97be9ec56e1f20d8f5492345ddcdca6 Mon Sep 17 00:00:00 2001 From: Christoph Haas Date: Mon, 27 Jun 2016 18:06:11 +0200 Subject: [PATCH 13/24] Fixed dropdown to allow users to delete their own screenshots --- app/views/packages/_user_dropdown.slim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/packages/_user_dropdown.slim b/app/views/packages/_user_dropdown.slim index deecbaf..0c33a2e 100644 --- a/app/views/packages/_user_dropdown.slim +++ b/app/views/packages/_user_dropdown.slim @@ -4,7 +4,7 @@ 'Manage your screenshot .dropdown-pane data-dropdown=true id="admin-info-#{screenshot.id}" a.button.small.alert[ - href=delete_screenshot_path(@package.name, screenshot.id) + href=delete_screenshot_path(screenshot.id) onclick="return confirm('Really delete the screenshot?');" ] Delete screenshot p Write a description for this screenshot: From 0c439359b9a472f801eea4166c2ae77720976739 Mon Sep 17 00:00:00 2001 From: Christoph Haas Date: Mon, 27 Jun 2016 18:06:29 +0200 Subject: [PATCH 14/24] Adding DIV to make sure that the image caption is always below the image --- app/views/packages/details.slim | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/app/views/packages/details.slim b/app/views/packages/details.slim index ccccede..ee3725f 100644 --- a/app/views/packages/details.slim +++ b/app/views/packages/details.slim @@ -13,13 +13,14 @@ .text-center a.black.fancybox href=screenshot.image.url(:large, timestamp: false) rel='fancybox-thumb' title=screenshot.caption = image_tag(screenshot.image.url(:large, timestamp: false), alt: screenshot.caption) - - if screenshot.description - .imgcaption =screenshot.description - - if screenshot.version.present? - em.imgcaption - '(from version #{screenshot.version}) - - if user_signed_in? - em.imgcaption =screenshot.adminstatus + div + - if screenshot.description + .imgcaption =screenshot.description + - if screenshot.version.present? + em.imgcaption + '(from version #{screenshot.version}) + - if user_signed_in? + em.imgcaption Admin status: #{screenshot.adminstatus} // Is the user an admin? - if user_signed_in? From b3d0b2dc06bfa54bace638d5f5bf5eafe89c04f2 Mon Sep 17 00:00:00 2001 From: Christoph Haas Date: Tue, 28 Jun 2016 11:39:31 +0200 Subject: [PATCH 15/24] JSON controller added. /json/package/:name implemented --- app/controllers/json_controller.rb | 14 ++++++++++++++ app/views/json/package.json.jbuilder | 7 +++++++ config/routes.rb | 1 + test/controllers/json_controller_test.rb | 24 ++++++++++++++++++++++++ 4 files changed, 46 insertions(+) create mode 100644 app/controllers/json_controller.rb create mode 100644 app/views/json/package.json.jbuilder create mode 100644 test/controllers/json_controller_test.rb diff --git a/app/controllers/json_controller.rb b/app/controllers/json_controller.rb new file mode 100644 index 0000000..4a376e6 --- /dev/null +++ b/app/controllers/json_controller.rb @@ -0,0 +1,14 @@ +class JsonController < ApplicationController + def package + @p = Package.find_by_name! params[:name] + end + + def packages + end + + def screenshots + end + + def packages_without_screenshots + end +end diff --git a/app/views/json/package.json.jbuilder b/app/views/json/package.json.jbuilder new file mode 100644 index 0000000..f651db6 --- /dev/null +++ b/app/views/json/package.json.jbuilder @@ -0,0 +1,7 @@ +json.package @p.name + +json.screenshots @p.screenshots do |s| + json.small_image_url "#{request.protocol}#{request.host_with_port}#{s.image.url(:thumb, timestamp: false)}" + json.large_image_url "#{request.protocol}#{request.host_with_port}#{s.image.url(:large, timestamp: false)}" + json.version s.version +end diff --git a/config/routes.rb b/config/routes.rb index b997f73..5307dda 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -35,6 +35,7 @@ Debshots::Application.routes.draw do get 'with_screenshots', to: redirect('/packages?show=with') get 'without_screenshots', to: redirect('/packages?show=without') + get 'json/package/:name' => 'json#package', as: :json_package, defaults: { format: :json } # The priority is based upon order of creation: first created -> highest priority. # See how all your routes lay out with "rake routes". diff --git a/test/controllers/json_controller_test.rb b/test/controllers/json_controller_test.rb new file mode 100644 index 0000000..0b10766 --- /dev/null +++ b/test/controllers/json_controller_test.rb @@ -0,0 +1,24 @@ +require "test_helper" + +class JsonControllerTest < ActionController::TestCase + def test_package + get :package + assert_response :success + end + + def test_packages + get :packages + assert_response :success + end + + def test_screenshots + get :screenshots + assert_response :success + end + + def test_packages-without-screenshots + get :packages-without-screenshots + assert_response :success + end + +end From a90bb51b38201292817796b50ee73074bd42c62d Mon Sep 17 00:00:00 2001 From: Christoph Haas Date: Tue, 28 Jun 2016 17:03:40 +0200 Subject: [PATCH 16/24] /json/packages and /json/screenshots implemented --- app/controllers/json_controller.rb | 6 ++++++ app/views/json/packages.json.jbuilder | 6 ++++++ app/views/json/screenshots.json.jbuilder | 10 ++++++++++ config/routes.rb | 3 +++ 4 files changed, 25 insertions(+) create mode 100644 app/views/json/packages.json.jbuilder create mode 100644 app/views/json/screenshots.json.jbuilder diff --git a/app/controllers/json_controller.rb b/app/controllers/json_controller.rb index 4a376e6..66251b3 100644 --- a/app/controllers/json_controller.rb +++ b/app/controllers/json_controller.rb @@ -1,14 +1,20 @@ class JsonController < ApplicationController + # JSON information on a single package def package @p = Package.find_by_name! params[:name] end + # JSON information on all packages def packages + @p = Package.all end + # JSON information on all screenshots def screenshots + @s = Screenshot.includes(:package) end + # JSON list of packages that do not have screenshots def packages_without_screenshots end end diff --git a/app/views/json/packages.json.jbuilder b/app/views/json/packages.json.jbuilder new file mode 100644 index 0000000..e3ea32d --- /dev/null +++ b/app/views/json/packages.json.jbuilder @@ -0,0 +1,6 @@ +json.cache! ['json_packages'], expires_in: 1.hour do + json.packages @p do |p| + json.extract! p, :maintainer, :name, :section, :maintainer_email, :homepage, :description + json.url package_url(p.name) + end +end diff --git a/app/views/json/screenshots.json.jbuilder b/app/views/json/screenshots.json.jbuilder new file mode 100644 index 0000000..e226259 --- /dev/null +++ b/app/views/json/screenshots.json.jbuilder @@ -0,0 +1,10 @@ +json.cache! ['json_screenshots'], expires_in: 1.hour do + json.screenshots @s do |s| + p = s.package + json.extract! s, :version + json.extract! p, :maintainer, :name, :section, :maintainer_email, :homepage, :description + json.url package_url(p.name) + json.small_image_url "#{request.protocol}#{request.host_with_port}#{s.image.url(:thumb, timestamp: false)}" + json.large_image_url "#{request.protocol}#{request.host_with_port}#{s.image.url(:large, timestamp: false)}" + end +end diff --git a/config/routes.rb b/config/routes.rb index 5307dda..9c73bd8 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -36,6 +36,9 @@ Debshots::Application.routes.draw do get 'without_screenshots', to: redirect('/packages?show=without') get 'json/package/:name' => 'json#package', as: :json_package, defaults: { format: :json } + get 'json/packages' => 'json#packages', as: :json_packages, defaults: { format: :json } + get 'json/screenshots' => 'json#screenshots', as: :json_screenshots, defaults: { format: :json } + # The priority is based upon order of creation: first created -> highest priority. # See how all your routes lay out with "rake routes". From c0109eb6e7af8d8a1572715f9f8c82a2dceebf5e Mon Sep 17 00:00:00 2001 From: Christoph Haas Date: Tue, 28 Jun 2016 17:09:17 +0200 Subject: [PATCH 17/24] json/packages-without-screenshots implemented --- app/controllers/json_controller.rb | 1 + config/routes.rb | 1 + 2 files changed, 2 insertions(+) diff --git a/app/controllers/json_controller.rb b/app/controllers/json_controller.rb index 66251b3..9db5d5e 100644 --- a/app/controllers/json_controller.rb +++ b/app/controllers/json_controller.rb @@ -16,5 +16,6 @@ class JsonController < ApplicationController # JSON list of packages that do not have screenshots def packages_without_screenshots + @p = Package.without_screenshots.all end end diff --git a/config/routes.rb b/config/routes.rb index 9c73bd8..37b9499 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -38,6 +38,7 @@ Debshots::Application.routes.draw do get 'json/package/:name' => 'json#package', as: :json_package, defaults: { format: :json } get 'json/packages' => 'json#packages', as: :json_packages, defaults: { format: :json } get 'json/screenshots' => 'json#screenshots', as: :json_screenshots, defaults: { format: :json } + get 'json/packages-without-screenshots' => 'json#packages_without_screenshots', defaults: { format: :json } # The priority is based upon order of creation: first created -> highest priority. # See how all your routes lay out with "rake routes". From 1928f8d2a8d326509792b08aa31d92ac73649ff9 Mon Sep 17 00:00:00 2001 From: Christoph Haas Date: Tue, 28 Jun 2016 17:12:32 +0200 Subject: [PATCH 18/24] Caching configured using file_store --- config/environments/development.rb | 6 +++++- config/environments/production.rb | 5 ++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/config/environments/development.rb b/config/environments/development.rb index 6e3a125..b750e0b 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -9,9 +9,13 @@ Debshots::Application.configure do # Do not eager load code on boot. config.eager_load = false - # Show full error reports and disable caching. + # Show full error reports config.consider_all_requests_local = true + + # Disable caching (except for working on caching) config.action_controller.perform_caching = false + # config.action_controller.perform_caching = true + # config.cache_store = :file_store, "#{Rails.root.to_s}/tmp/cache/filestore" # Don't care if the mailer can't send. config.action_mailer.raise_delivery_errors = false diff --git a/config/environments/production.rb b/config/environments/production.rb index a6df31c..65d43d9 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -13,9 +13,12 @@ Debshots::Application.configure do # ActiveRecord errors propagate normally (forward deprecation to Rails 5) config.active_record.raise_in_transactional_callbacks = true - # Full error reports are disabled and caching is turned on. + # Full error reports are disabled config.consider_all_requests_local = false + + # Caching turned on config.action_controller.perform_caching = true + config.cache_store = :file_store, "#{Rails.root.to_s}/tmp/cache/filestore" # Enable Rack::Cache to put a simple HTTP cache in front of your application # Add `rack-cache` to your Gemfile before enabling this. From b3625f0a890c0d2471c5bc4df522524a78c879c2 Mon Sep 17 00:00:00 2001 From: Christoph Haas Date: Tue, 28 Jun 2016 17:12:56 +0200 Subject: [PATCH 19/24] Missing jbuilder file added for /json/packages-without-screenshots --- app/views/json/packages_without_screenshots.json.jbuilder | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 app/views/json/packages_without_screenshots.json.jbuilder diff --git a/app/views/json/packages_without_screenshots.json.jbuilder b/app/views/json/packages_without_screenshots.json.jbuilder new file mode 100644 index 0000000..5f77f38 --- /dev/null +++ b/app/views/json/packages_without_screenshots.json.jbuilder @@ -0,0 +1,5 @@ +json.cache! ['json_packages'], expires_in: 1.hour do + json.packages @p do |p| + json.extract! p, :name + end +end From 4d65c071f9a57eeedb696fdcebbb1055e27e4399 Mon Sep 17 00:00:00 2001 From: Christoph Haas Date: Wed, 29 Jun 2016 10:42:22 +0200 Subject: [PATCH 20/24] Cruft removed --- lib/deb_importer.rb | 31 ------------------------------- 1 file changed, 31 deletions(-) diff --git a/lib/deb_importer.rb b/lib/deb_importer.rb index 9ab3855..f948222 100644 --- a/lib/deb_importer.rb +++ b/lib/deb_importer.rb @@ -188,36 +188,5 @@ module DebImporter def dpkg_compare_version(v1, v2, op) system("dpkg --compare-versions #{v1} #{op} #{v2}") end - - # def ___version_compare(x,y) - # # Compare a version string (like the upstream_version or - # # debian_revision string) against another version string. - # # The algorithm works like this: - # # - # # The strings are compared from left to right. - # # First the initial part of each string consisting entirely of non-digit - # # characters is determined. These two parts (one of which may be empty) - # # are compared lexically. If a difference is found it is returned. - # # The lexical comparison is a comparison of ASCII values modified so - # # that all the letters sort earlier than all the non-letters and so that - # # a tilde sorts before anything, even the end of a part. - # # For example, the following parts are in sorted order from - # # earliest to latest: ~~, ~~a, ~, the empty part, a.[37] - # # - # # Then the initial part of the remainder of each string which - # # consists entirely of digit characters is determined. The - # # numerical values of these two parts are compared, and any - # # difference found is returned as the result of the comparison. - # # For these purposes an empty string (which can only occur at - # # the end of one or both version strings being compared) counts as zero. - # # - # # These two steps (comparing and removing initial non-digit strings - # # and initial digit strings) are repeated until a difference is - # # found or both strings are exhausted. - - # (x.chars).zip(y.chars) do |xchar,ychar| - # puts xchar, ychar - # end - # end end # /class end # module From 670df2f2773502679f6548a8298296f672d068a3 Mon Sep 17 00:00:00 2001 From: Christoph Haas Date: Wed, 29 Jun 2016 10:43:01 +0200 Subject: [PATCH 21/24] Fixed bug that parsed description fields incorrectly Example: Description-en: 389 Directory Server suite - server Based on the Lightweight Directory Access Protocol (LDAP), the 389 Directory Server is designed to manage large directories of users and resources robustly and scalably. . Its key features include: * four-way multi-master replication; * great scalability; * extensive documentation; * Active Directory user and group synchronization; * secure authentication and transport; * support for LDAPv3; * graphical management console; * on-line, zero downtime update of schema, configuration, and in-tree Access Control Information. The "Its key features include:" was mistaken as a key. --- lib/deb_importer.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/deb_importer.rb b/lib/deb_importer.rb index f948222..81be7e2 100644 --- a/lib/deb_importer.rb +++ b/lib/deb_importer.rb @@ -101,10 +101,10 @@ module DebImporter name=value='' data.each_line do |line| case line - when /^(.+?): (.+)/ # "Key: Value" + when /^(\S+?): (.+)/ # "Key: Value" fields[name.to_sym]=value unless value.empty? name,value=$1,$2 - when /^(.+?):$/ # "Key:" (start of multi-line entry without value in line) + when /^(\S+?):$/ # "Key:" (start of multi-line entry without value in line) fields[name.to_sym]=value unless value.empty? name=$1 value='' From 73c236147130332ddba8b09a3f0607e653b21be6 Mon Sep 17 00:00:00 2001 From: Christoph Haas Date: Wed, 29 Jun 2016 21:48:25 +0200 Subject: [PATCH 22/24] Cruft removed --- app/assets/stylesheets/moderate.scss | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 app/assets/stylesheets/moderate.scss diff --git a/app/assets/stylesheets/moderate.scss b/app/assets/stylesheets/moderate.scss deleted file mode 100644 index 5571dd0..0000000 --- a/app/assets/stylesheets/moderate.scss +++ /dev/null @@ -1,3 +0,0 @@ -// Place all the styles related to the Moderate controller here. -// They will automatically be included in application.css. -// You can use Sass (SCSS) here: http://sass-lang.com/ From 8dc2514852341bcc216138b4fe6f3f96aca21eee Mon Sep 17 00:00:00 2001 From: Christoph Haas Date: Wed, 29 Jun 2016 21:49:32 +0200 Subject: [PATCH 23/24] Added partial to display long description properly --- app/assets/stylesheets/_settings.scss | 6 ++++++ app/views/packages/_long_description.slim | 19 +++++++++++++++++++ app/views/packages/details.slim | 3 ++- 3 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 app/views/packages/_long_description.slim diff --git a/app/assets/stylesheets/_settings.scss b/app/assets/stylesheets/_settings.scss index b5fdc5e..8dbd208 100644 --- a/app/assets/stylesheets/_settings.scss +++ b/app/assets/stylesheets/_settings.scss @@ -952,3 +952,9 @@ a.black margin-left: 2em; margin-bottom: 0.5em; } + +.description-verbatim-lines +{ + line-height: 120%; + // white-space: pre-line; +} diff --git a/app/views/packages/_long_description.slim b/app/views/packages/_long_description.slim new file mode 100644 index 0000000..c957fb9 --- /dev/null +++ b/app/views/packages/_long_description.slim @@ -0,0 +1,19 @@ +/ Converts long descriptions from Debian control files into +/ proper HTML. +/ See: https://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-Description + +- text.lines.each do |line| + / pre +++#{line}--- + - case line.rstrip + - when '.' + / Empty line + / TODO: Technically wrong because this creates an empty paragraph: + p + - when /^(\s+)(.+)/ + / Verbatim text + div.description-verbatim-lines + = (' ' * $1.to_s.length).html_safe + = $2 + - else + / Normal text + = line diff --git a/app/views/packages/details.slim b/app/views/packages/details.slim index ee3725f..0a0bd89 100644 --- a/app/views/packages/details.slim +++ b/app/views/packages/details.slim @@ -44,7 +44,8 @@ p.subtitle Description // TODO: split long_description by

tags - if @package.long_description.present? - p =@package.long_description + = render(partial: 'long_description', locals: {text: @package.long_description}) + / p =@package.long_description - else p Sorry - no more information available. - if @package.homepage.present? From 88728c0bba0ed85382a1df0812c0da3805f48031 Mon Sep 17 00:00:00 2001 From: Christoph Haas Date: Wed, 29 Jun 2016 21:49:53 +0200 Subject: [PATCH 24/24] Keep leading spacesd when parsing the long description --- lib/deb_importer.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/deb_importer.rb b/lib/deb_importer.rb index 81be7e2..6d2b96d 100644 --- a/lib/deb_importer.rb +++ b/lib/deb_importer.rb @@ -108,7 +108,7 @@ module DebImporter fields[name.to_sym]=value unless value.empty? name=$1 value='' - when /^\s+(.+)/ # " Indented multi-line value" + when /^\s(.+)/ # " Indented multi-line value" # Add a newline for multi-line entries ("Key: Value\n Foo\n Bar") unless value.empty? value << "\n"