From ca42eff64d129a2548ffa9b1d926c3b8f1c31378 Mon Sep 17 00:00:00 2001 From: Henne Vogelsang Date: Wed, 8 Nov 2023 13:28:55 +0100 Subject: [PATCH] Introduces repomd search With this we get a Distribution with a bunch of Repository in the database that you can sync. Which will create, per Distribution/Repository, a gazillion Package objects with data from primary, appdata and updateinfo. The user workflow will be as following: - As an admin I setup Distribution and it's repositories - As an admin I sync a Distribution's repositories to the database - As a user I search for Package of a Distributions in it's repositories - As a user I extend the search to packages build on OBS for this Distribution (not implemented here) Separating the Distribution "repository" search from searching through OBS will hopefully make more clear for newbies that enabling extra repos is kind of dangerous. --- app/controllers/application_controller.rb | 266 +----------------- app/controllers/main_controller.rb | 9 + app/controllers/package_controller.rb | 135 --------- app/controllers/packages_controller.rb | 12 + app/jobs/application_job.rb | 3 + app/jobs/cache_screenshot_job.rb | 45 +++ app/models/appdata.rb | 66 ----- app/models/category.rb | 4 + app/models/distribution.rb | 18 ++ app/models/package.rb | 47 ++++ app/models/repository.rb | 213 ++++++++++++++ app/models/screenshot.rb | 103 ------- app/views/download/_download.css.erb | 40 --- app/views/download/appliance.erb | 52 ---- app/views/download/package.erb | 130 --------- app/views/error.html.erb | 8 - app/views/layouts/_search_form.html.haml | 14 + app/views/layouts/application.html.haml | 2 +- app/views/main/index.html.haml | 5 + app/views/main/search.html.haml | 23 ++ app/views/package/_download_rows.html.erb | 64 ----- app/views/package/explore.html.erb | 46 --- app/views/package/show.html.erb | 137 --------- .../packages/_screenshot_carousel.html.haml | 16 ++ app/views/packages/show.html.haml | 33 +++ app/views/search/_category_header.html.erb | 16 -- app/views/search/_default_searches.html.erb | 7 - app/views/search/_find_form.html.erb | 29 -- app/views/search/_find_results.html.erb | 90 ------ app/views/search/_settings.html.erb | 43 --- app/views/search/find.html.erb | 8 - config/routes.rb | 24 +- 32 files changed, 453 insertions(+), 1255 deletions(-) create mode 100644 app/controllers/main_controller.rb delete mode 100644 app/controllers/package_controller.rb create mode 100644 app/controllers/packages_controller.rb create mode 100644 app/jobs/application_job.rb create mode 100644 app/jobs/cache_screenshot_job.rb delete mode 100644 app/models/appdata.rb create mode 100644 app/models/category.rb create mode 100644 app/models/distribution.rb create mode 100644 app/models/package.rb create mode 100644 app/models/repository.rb delete mode 100644 app/models/screenshot.rb delete mode 100644 app/views/download/_download.css.erb delete mode 100644 app/views/download/appliance.erb delete mode 100644 app/views/download/package.erb delete mode 100644 app/views/error.html.erb create mode 100644 app/views/layouts/_search_form.html.haml create mode 100644 app/views/main/index.html.haml create mode 100644 app/views/main/search.html.haml delete mode 100644 app/views/package/_download_rows.html.erb delete mode 100644 app/views/package/explore.html.erb delete mode 100644 app/views/package/show.html.erb create mode 100644 app/views/packages/_screenshot_carousel.html.haml create mode 100644 app/views/packages/show.html.haml delete mode 100644 app/views/search/_category_header.html.erb delete mode 100644 app/views/search/_default_searches.html.erb delete mode 100644 app/views/search/_find_form.html.erb delete mode 100644 app/views/search/_find_results.html.erb delete mode 100644 app/views/search/_settings.html.erb delete mode 100644 app/views/search/find.html.erb diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index fe77892ed..cb654586d 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,267 +1,5 @@ -# frozen_string_literal: true - -require 'api_connect' -require 'net/https' -require 'json' - -class OBSError < StandardError; end - class ApplicationController < ActionController::Base - before_action :validate_configuration - before_action :set_language - before_action :set_distributions - before_action :set_releases - before_action :set_baseproject - - helper :all # include all helpers, all the time - require 'rexml/document' - - class MissingParameterError < RuntimeError; end - - EXCEPTIONS_TO_IGNORE = [OBS::InvalidSearchTerm, - ApiConnect::Error, - ApplicationController::MissingParameterError, - Timeout::Error].freeze - - rescue_from Exception do |exception| - logger.error "Exception: #{exception.class}: #{exception.message}" - @message = exception.message - layout = request.xhr? ? false : 'application' - logger.error exception.backtrace.join("\n") unless EXCEPTIONS_TO_IGNORE.include? exception - render template: 'error', formats: [:html], layout: layout, status: 400 - end - - def prepare_appdata - @appdata = case @baseproject - when 'ALL' - tw = tumbleweed_appdata - stable = leap_appdata(@stable_version) - testing = @testing_version ? leap_appdata(@testing_version) : {} - legacy = @legacy_release ? leap_appdata(@legacy_release) : {} - # Overwriting entries is okay, appdata is not used for - # installation when @baseproject == 'ALL' - tw.merge(stable).merge(testing).merge(legacy) - when 'openSUSE:Factory' - tumbleweed_appdata - when "openSUSE:Leap:#{@stable_version}" - leap_appdata(@stable_version) - when "openSUSE:Leap:#{@testing_version}" - leap_appdata(@testing_version) - when "openSUSE:Leap:#{@legacy_release}" - leap_appdata(@legacy_release) - else - { apps: [], categories: Set.new } - end - end - - # TODO: Used to alter what OBS.search_published_binary returns, extract this into OBS. - def fix_package_projects - # Due to Leap 15.3's release model, there is no 1:1 distribution <> - # baseproject relation anymore. - # official packages: - # DISTRIBUTION_PROJECTS_OVERRIDE contains valid baseprojects - # -> treat as if it was from the distribution's main project, but save the - # link to the real project for the generation of the download page - # other packages: - # package.repo uses the distribution's default reponame - # -> only "fix" the baseproject for grouping purposes - @packages.each do |package| - dist = @distributions.find do |d| - projects = DISTRIBUTION_PROJECTS_OVERRIDE.fetch(d[:dist_id], nil) - projects&.include?(package.project) - end - - if dist - logger.debug("Match in override hash, changing #{package.baseproject} to #{dist[:project]}") - package.realproject = package.project - package.baseproject = dist[:project] - package.project = dist[:project] - else - repo = @distributions.find { |d| d[:reponame] == package.repository } - if repo - package.realproject = package.project - package.baseproject = repo[:project] - elsif package.repository == 'openSUSE_Leap_15.4' - leap154 = @distributions.find { |d| d[:dist_id] == '23178' } - next unless leap154 - - package.baseproject = leap154[:project] - elsif package.repository == 'openSUSE_Leap_15.5' - leap155 = @distributions.find { |d| d[:dist_id] == '23175' } - next unless leap155 - - package.baseproject = leap155[:project] - end - end - end - end - - # TODO: Used to alter what OBS.search_published_binary returns, extract this into OBS. - def filter_packages - # remove maintenance projects, they are not meant for end users - @packages.reject! { |p| p.project.include? 'openSUSE:Maintenance:' } - @packages.reject! { |p| p.project == 'openSUSE:Factory:Rebuild' } - @packages.reject! { |p| p.project.start_with?('openSUSE:Factory:Staging') } - - # only show packages - @packages.reject! { |p| p.type == 'ymp' } - - @packages.reject! { |p| /-devel/i.match?(p.name) } unless @search_devel - - unless @search_lang - @packages.reject! { |p| p.name.end_with?('-lang') || p.name.include?('-translations-') || p.name.include?('-l10n-') } - end - - @packages.reject! { |p| p.name.end_with?('-buildsymbols', '-debuginfo', '-debugsource') } unless @search_debug - - # filter out ports for different arch - if @baseproject.end_with?('ARM') - @packages.select! { |p| p.project.include?('ARM') || p.repository.include?('ARM') } - elsif @baseproject.end_with?('PowerPC') - @packages.select! { |p| p.project.include?('PowerPC') || p.repository.include?('PowerPC') } - else # x86 - @packages.reject! do |p| - p.repository.end_with?('_ARM', '_PowerPC', '_zSystems') || p.repository == 'ports' || - p.project.include?('ARM') || p.project.include?('PowerPC') || p.project.include?('zSystems') - end - end - end - - private - - def validate_configuration - config = Rails.configuration.x - layout = request.xhr? ? false : 'application' - - if config.api_username.blank? && config.opensuse_cookie.blank? - @message = _('The authentication to the OBS API has not been configured correctly.') - render template: 'error', formats: [:html], layout: layout, status: 503 - end - end - - def set_language - set_gettext_locale - requested_locale = FastGettext.locale - # if we don't have translations for the requested locale, try - # the short form without underscore - unless LANGUAGES.include? requested_locale - requested_locale = requested_locale.split('_').first - params[:locale] = LANGUAGES.include?(requested_locale) ? requested_locale : 'en' - set_gettext_locale - end - @lang = FastGettext.locale - end - - def load_releases - release_file_url = 'https://get.opensuse.org/api/v0/distributions.json' - Rails.cache.fetch('software-o-o/releases', expires_in: 10.minutes) do - JSON.parse(URI.parse(release_file_url).open.read)['Leap'].sort_by { |r| -r['upgrade-weight'] } - rescue StandardError => e - Rails.logger.error "Error while parsing releases entry in #{release_file_url}: #{e}" - next - end - rescue StandardError => e - Rails.logger.error "Error while parsing releases file #{release_file_url}: #{e}" - raise e - end - - def set_releases - @stable_version = nil - @testing_version = nil - @testing_state = nil - @legacy_release = nil - - # look for most current release - versions = load_releases - unless versions.blank? - if versions[0]['state'] == 'Stable' - @stable_version = versions[0]['version'].to_s - @legacy_release = versions[1]['version'].to_s - else - @testing_version = versions[0]['version'].to_s - @testing_state = versions[0]['state'].to_s - @stable_version = versions[1]['version'].to_s - @legacy_release = versions[2]['version'].to_s - end - end - end - - def tumbleweed_appdata - Rails.cache.fetch('appdata/tumbleweed', expires_in: 12.hours) do - Appdata.new('tumbleweed').data - end - end - - def leap_appdata(version) - Rails.cache.fetch("appdata/leap#{version}", expires_in: 12.hours) do - Appdata.new("leap/#{version}").data - end - end - - def set_distributions - @distributions = Rails.cache.fetch('distributions', expires_in: 120.minutes) do - load_distributions - end - rescue OBSError - @distributions = nil - @hide_search_box = true - flash[:error] = 'Connection to OBS is unavailable. Functionality of this site is limited.' - end - - def load_distributions - logger.debug 'Loading distributions' - loaded_distros = [] - begin - response = ApiConnect.get('public/distributions') - doc = REXML::Document.new response.body - doc.elements.each('distributions/distribution') do |element| - loaded_distros << parse_distribution(element) - end - loaded_distros.unshift({ name: 'ALL Distributions', project: 'ALL' }) - rescue Exception => e - logger.error "Error while loading distributions: #{e}" - raise OBSError.new, _('OBS Backend not available') - end - loaded_distros - end - - def parse_distribution(element) - dist = { - name: element.elements['name'].text, - project: element.elements['project'].text, - reponame: element.elements['reponame'].text, - repository: element.elements['repository'].text, - dist_id: element.attributes['id'] - } - logger.debug "Added Distribution: #{dist}" - dist - end - - def set_search_options - @search_term = params[:q] || '' - @search_devel = (cookies[:search_devel] == 'true') - @search_lang = (cookies[:search_lang] == 'true') - @search_debug = (cookies[:search_debug] == 'true') - end - - def set_baseproject - default_baseproject = "openSUSE:Leap:#{@stable_version}" - - if params[:baseproject].present? && valid_baseproject?(params[:baseproject]) - @baseproject = params[:baseproject] - update_baseproject_cookie(params[:baseproject]) - elsif cookies[:baseproject].present? && valid_baseproject?(cookies[:baseproject]) - @baseproject = cookies[:baseproject] - end - @baseproject = default_baseproject unless @baseproject.present? - end - - def valid_baseproject?(project) - @distributions.present? && @distributions.select { |d| d[:project] == project }.present? - end - - def update_baseproject_cookie(project) - cookies.delete :baseproject - cookies.permanent[:baseproject] = project + def set_distribution + @distribution = Distribution.find_by!(name: params[:distribution_name] || params[:distribution]) end end diff --git a/app/controllers/main_controller.rb b/app/controllers/main_controller.rb new file mode 100644 index 000000000..eb3f67b48 --- /dev/null +++ b/app/controllers/main_controller.rb @@ -0,0 +1,9 @@ +class MainController < ApplicationController + before_action :set_distribution, only: :search + + def index; end + + def search + @packages = @distribution.packages.where('name ILIKE ?', "%#{params[:q]}%").order("LENGTH(name) ASC").order(:weight) + end +end diff --git a/app/controllers/package_controller.rb b/app/controllers/package_controller.rb deleted file mode 100644 index 9c0698b4a..000000000 --- a/app/controllers/package_controller.rb +++ /dev/null @@ -1,135 +0,0 @@ -# frozen_string_literal: true - -class PackageController < ApplicationController - before_action :set_search_options, only: %i[show categories] - before_action :prepare_appdata, :set_categories - - skip_before_action :set_language, only: %i[thumbnail screenshot] - - def show - @pkgname = params[:package] - raise MissingParameterError, 'Invalid parameter package' unless valid_package_name? @pkgname - - begin - raise OBSError if @distributions.nil? - - @search_term = params[:search_term] - - @packages = OBS.search_published_binary("\"#{@pkgname}\"") - # only show rpms - @packages.select! { |p| p.type != 'ymp' && p.quality != 'Private' } - fix_package_projects - @default_project_name = @distributions.find { |d| d[:project] == @baseproject }[:name] - default_update_projects = ["#{@baseproject}:Update", "#{@baseproject}:NonFree:Update"] - default_release_projects = [@baseproject, "#{@baseproject}:NonFree"] - @default_package = @packages.find { |p| default_update_projects.include?(p.project) } || - @packages.find { |p| default_release_projects.include?(p.project) } - - pkg_appdata = @appdata[:apps].find { |app| app[:pkgname].casecmp(@pkgname).zero? } - if pkg_appdata - @name = pkg_appdata[:name] - @appcategories = pkg_appdata[:categories] - @homepage = pkg_appdata[:homepage] - @appid = pkg_appdata[:id] - end - - @screenshot = url_for controller: :package, action: :screenshot, package: @pkgname, only_path: true - @thumbnail = url_for controller: :package, action: :thumbnail, package: @pkgname, only_path: true - - filter_packages - - @official_projects = @distributions.map { |d| d[:project] } - # get extra distributions that are not in the default distribution list - @extra_packages = @packages.reject { |p| @distributions.map { |d| d[:project] }.include? p.baseproject } - @extra_dists = @extra_packages.filter_map(&:baseproject).uniq.map { |d| { project: d } } - rescue OBSError - @hide_search_box = true - flash.now[:error] = _('Connection to OBS is unavailable. Functionality of this site is limited.') - end - end - - def explore - # Workaround to know in advance non-cached screenshots - # Ideally the apps structure should include Screenshot objects from the beginning - @apps = @appdata[:apps].reject do |a| - a[:screenshots].blank? || !Screenshot.new(a[:pkgname], a[:screenshots][0]).thumbnail_generated? - end - end - - def category - @category = params[:category] - raise MissingParameterError, 'Invalid parameter category' unless valid_package_name? @category - - mapping = @main_sections.select { |s| s[:id].casecmp(@category).zero? } - categories = (mapping.blank? ? [@category] : mapping.first[:categories]) - - app_pkgs = @appdata[:apps].reject { |app| (app[:categories].map(&:downcase) & categories.map(&:downcase)).blank? } - package_names_unsorted = app_pkgs.map { |p| p[:pkgname] }.uniq - @packagenames = package_names_unsorted.sort_by { |x| @appdata[:apps].find { |a| a[:pkgname] == x }[:name] } - - app_categories = app_pkgs.map { |p| p[:categories] }.flatten - reject_categories = %w[GNOME KDE Qt GTK] - @related_categories = app_categories.uniq.map { |c| { name: c, weight: app_categories.count { |v| v == c } } } - @related_categories = @related_categories.sort_by { |c| c[:weight] }.reverse.reject { |c| categories.include? c[:name] } - @related_categories = @related_categories.reject { |c| reject_categories.include? c[:name] } - - render 'search/find' - end - - def screenshot - image params[:package], :screenshot - end - - def thumbnail - image params[:package], :thumbnail - end - - private - - def valid_package_name?(name) - name =~ /^[[:alnum:]][-+~\w.:@]*$/ - end - - def image(pkgname, type) - @appdata[:apps].each do |app| - next unless app[:pkgname] == pkgname - next if app[:screenshots].blank? - - app[:screenshots].each do |image_url| - return redirect_to(image_url, allow_other_host: true) if type == :screenshot && image_url - next if image_url.blank? - - path = begin - screenshot = Screenshot.new(pkgname, image_url) - screenshot.thumbnail_path(fetch: true) - rescue OpenURI::HTTPError => e - Rails.logger.error "Error retrieving #{image_url}: #{e}" - next - end - return redirect_to "/#{path}" - end - end - - return head(404, 'content_type' => 'text/plain') if type == :screenshot - - # a screenshot object with nil url returns default thumbnails - screenshot = Screenshot.new(pkgname, nil) - path = screenshot.thumbnail_path(fetch: true) - url = ActionController::Base.helpers.asset_url(path) - redirect_to url - end - - # See https://specifications.freedesktop.org/menu-spec/menu-spec-latest.html - def set_categories - @main_sections = [ - { name: _('Games'), id: 'Games', icon: 'games', categories: ['Game'] }, - { name: _('Development'), id: 'Development', icon: 'code', categories: ['Development'] }, - { name: _('Education & Science'), id: 'Education', icon: 'education', categories: %w[Education Science] }, - { name: _('Multimedia'), id: 'Multimedia', icon: 'multimedia', categories: %w[AudioVideo Audio Video] }, - { name: _('Graphics'), id: 'Graphics', icon: 'graphics', categories: ['Graphics'] }, - { name: _('Office & Productivity'), id: 'Office', icon: 'office', categories: ['Office'] }, - { name: _('Network'), id: 'Network', icon: 'network', categories: ['Network'] }, - { name: _('System & Utility'), id: 'Tools', icon: 'utils', categories: %w[Settings System Utility] } - ] - end -end diff --git a/app/controllers/packages_controller.rb b/app/controllers/packages_controller.rb new file mode 100644 index 000000000..5be0a7bdc --- /dev/null +++ b/app/controllers/packages_controller.rb @@ -0,0 +1,12 @@ +class PackagesController < ApplicationController + before_action :set_distribution + before_action :set_package, only: %i[show update] + + def show; end + + private + + def set_package + @package = @distribution.packages.find_by!(name: params[:name]) + end +end diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb new file mode 100644 index 000000000..bacae8464 --- /dev/null +++ b/app/jobs/application_job.rb @@ -0,0 +1,3 @@ +class ApplicationJob < ActiveJob::Base +end + diff --git a/app/jobs/cache_screenshot_job.rb b/app/jobs/cache_screenshot_job.rb new file mode 100644 index 000000000..89c1e9040 --- /dev/null +++ b/app/jobs/cache_screenshot_job.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +# Fetches one or more screenshots from an array of urls and attaches it to a Package +class CacheScreenshotJob < ApplicationJob + def perform(package_id, screenshot_urls = []) + package = Package.find_by(id: package_id) + return unless package + + package.screenshots.purge + + screenshot_urls.each do |url| + begin + response = http_connection(url: url).get + rescue Faraday::ConnectionFailed, Faraday::SSLError + logger.debug "Could not fetch #{url}..." + next + end + + unless response.success? + logger.debug "Could not fetch #{url}..." + next + end + + filename = File.basename(URI.parse(url).path) + filepath = Rails.root.join('tmp', 'file-cache', filename) + File.binwrite(filepath, response.body) + package.screenshots.attach(io: File.open(filepath), filename: filename, content_type: response.headers['content-type']) + File.delete(filepath) + end + package.increment('weight', package.screenshots_attachments.count) + package.save + end + + private + + def http_connection(url:) + Faraday.new(url) do |conn| + store = ActiveSupport::Cache.lookup_store(:file_store, Rails.root.join('tmp', 'http-cache')) + + conn.headers['User-Agent'] = 'software.opensuse.org' + conn.use Faraday::FollowRedirects::Middleware + conn.use :http_cache, store: store, serializer: Marshal + end + end +end diff --git a/app/models/appdata.rb b/app/models/appdata.rb deleted file mode 100644 index 1658f0691..000000000 --- a/app/models/appdata.rb +++ /dev/null @@ -1,66 +0,0 @@ -# frozen_string_literal: true - -require 'open-uri' -require 'open_uri_redirections' -require 'zlib' - -class Appdata - attr_reader :data - - def initialize(distribution) - @distribution = distribution - @data = load_appdata - end - - private - - def load_appdata - data = { apps: [], categories: Set.new } - %w[oss non-oss].each do |flavour| - xml = load_xml(flavour) - data[:apps].concat(parse_appdata_apps(xml)) - data[:categories].merge(parse_appdata_categories(xml)) - end - data - end - - def load_xml(flavour) - baseurl = if @distribution == 'tumbleweed' - "https://download.opensuse.org/tumbleweed/repo/#{flavour}/" - else - "https://download.opensuse.org/distribution/#{@distribution}/repo/#{flavour}/" - end - - index_url = "#{baseurl}/repodata/repomd.xml" - repomd = Nokogiri::XML(URI.open(index_url)).remove_namespaces! - href = repomd.xpath('/repomd/data[@type="appdata"]/location').attr('href').text - appdata_url = baseurl + href - Nokogiri::XML(Zlib::GzipReader.new(URI.open(appdata_url, allow_redirections: :all))) - # Broad except, could be network connection, missing 'href' attribute - rescue StandardError => e - Rails.logger.error("Error: #{e} -- appdata_url=#{appdata_url}") - Nokogiri::XML('') - end - - def parse_appdata_apps(xml) - apps = [] - xml.xpath('/components/component').each do |app| - appdata = {} - # Filter translated versions of name and summary out - appdata[:name] = app.xpath('name[not(@xml:lang)]').text - appdata[:summary] = app.xpath('summary[not(@xml:lang)]').text - appdata[:pkgname] = app.xpath('pkgname').text - appdata[:categories] = app.xpath('categories/category').map(&:text).reject { |c| c.match(/^X-/) }.uniq - appdata[:id] = app.xpath('id').text - appdata[:homepage] = app.xpath('url').text - appdata[:screenshots] = app.xpath('screenshots/screenshot/image').map(&:text) - apps << appdata - end - apps - end - - def parse_appdata_categories(xml) - xml.xpath('/components/component/categories/category') - .map(&:text).reject { |c| c.match(/^X-/) }.uniq - end -end diff --git a/app/models/category.rb b/app/models/category.rb new file mode 100644 index 000000000..c3ae6af59 --- /dev/null +++ b/app/models/category.rb @@ -0,0 +1,4 @@ +class Category < ApplicationRecord + has_and_belongs_to_many :packages + validates :name, presence: true +end diff --git a/app/models/distribution.rb b/app/models/distribution.rb new file mode 100644 index 000000000..305f1be20 --- /dev/null +++ b/app/models/distribution.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +# A Linux Distribution +class Distribution < ApplicationRecord + has_many :repositories + has_many :packages, through: :repositories + + validates :vendor, :name, :version, :obs_repo_names, presence: true + + def sync + repositories.where(updateinfo: false).each(&:sync) + repositories.where(updateinfo: true).each(&:sync) + end + + def full_name + "#{vendor} #{name}" + end +end diff --git a/app/models/package.rb b/app/models/package.rb new file mode 100644 index 000000000..25c5190a4 --- /dev/null +++ b/app/models/package.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +# A binary package of a Linux distribution repository +class Package < ApplicationRecord + belongs_to :repository + has_and_belongs_to_many :categories, -> { distinct } + has_many_attached :screenshots + has_many_attached :icons + + validates :name, :release, :architectures, :license, presence: true + validates :name, uniqueness: { scope: :repository } + serialize :architectures, type: Array + has_paper_trail on: [:update], only: [:release] + + SUPPORT_PACKAGE_NAME_PARTS = ['-branding-', '-extension-', '-plugin-', '-plugins-', '-trans-', '-translations-', '-traineddata-'].freeze + SUPPORT_PACKAGE_NAME_ENDINGS = ['-32bit', '-data', '-devel', '-devel-static', '-doc', '-docs', '-docs-html', '-debug', + '-compat', '-api', '-addons', + '-font', '-fonts', '-extra', '-extras', 'extension', '-extensions', '-example', '-examples', + '-headers', '-imports', '-icons', '-javadoc', + '-lang', '-lib', '-libs', '-locale', '-module', '-modules', + '-plugin', '-plugins', '-schema', '-schemas', '-source', '-src', '-support', '-systemd', + '-test', '-testresults', '-tests', '-testsuite', '-theme', '-themes', '-translations', + '-wallpaper', '-wallpapers'].freeze + SUPPORT_PACKAGE_NAME_BEGINNINGS = ['system-', 'php8-', 'php7-', 'aws-sdk-', 'ocaml-', 'aspell-', + 'patterns-', 'cross-', 'myspell-', 'libqt5-', 'maven-', + 'typelib-', 'qt6-', 'ruby2.5-', 'ghc-', 'perl-', 'python3-', 'texlive-'].freeze + + def title + name unless read_attribute(:title) + end + + def main_package? + SUPPORT_PACKAGE_NAME_PARTS.each do |part| + return false if name.include?(part) + end + + SUPPORT_PACKAGE_NAME_BEGINNINGS.each do |beginning| + return false if name.starts_with?(beginning) + end + + SUPPORT_PACKAGE_NAME_ENDINGS.each do |ending| + return false if name.ends_with?(ending) + end + + true + end +end diff --git a/app/models/repository.rb b/app/models/repository.rb new file mode 100644 index 000000000..307d89313 --- /dev/null +++ b/app/models/repository.rb @@ -0,0 +1,213 @@ +# frozen_string_literal: true + +# A distribution repository (repomd+appdata) with many packages +# https://github.com/openSUSE/libzypp/tree/master/zypp/parser/yum/schema +# https://www.freedesktop.org/software/appstream/docs/chap-Metadata.html +# +class Repository < ApplicationRecord + belongs_to :distribution + has_many :packages + has_many_attached :repodata_files + + validates :url, presence: true + + def sync + if updateinfo? + sync_updateinfo + else + sync_primary + sync_appdata + set_weight + end + end + + def resync + update(revision: nil) + sync + end + + # private + + def sync_updateinfo + logger.debug('Syncing updateinfo...') + + updateinfo_xml.elements('update').each do |update_hash| + update_hash.deep_symbolize_keys! + update_hash[:pkglist][:collection][:package].each do |package_hash| + package = packages.find_by(name: package_hash[:name]) + + next unless package + + package.update!(release: package_hash[:version]) + end + end + end + + def sync_primary + logger.debug('Syncing primary...') + + primary_xml.elements('package').each do |package_hash| + package_hash.deep_symbolize_keys! + logger.debug("Trying to sync #{package_hash[:name]}...") + package = packages.find_or_create_by(name: package_hash[:name]) + + package.assign_attributes(package_hash.slice(:name, :url, :summary, :description)) + package.architectures << package_hash[:arch] + package.architectures = package.architectures.uniq.compact + package.license = package_hash[:format][:'rpm:license'] + package.release = package_hash[:version][:ver] + package.increment('weight') if package.main_package? + package.save! + end + cleanup_packages + end + + def sync_appdata + logger.debug('Trying to sync appdata...') + appdata_xml.elements('component').each do |component_hash| + component_hash.deep_symbolize_keys! + logger.debug("Trying to sync appdata for #{component_hash[:pkgname]}...") + package = packages.find_by(name: component_hash[:pkgname]) + next unless package + + # FIXME: Handle the translations of appdata.summary + summary = component_hash[:summary] + summary = summary.first if summary.is_a?(Array) + + # FIXME: Handle the translations of appdata.name + title = component_hash[:name] + title = title.first if title.is_a?(Array) + + # FIXME: Handle HTML components like

and translations.... + description = component_hash[:description] + + if component_hash[:screenshots] + screenshots = component_hash[:screenshots].elements('screenshot').map { |screenshot| screenshot['image']['_content'] } + screenshots.compact! + end + # FIXME: icons, how does that work with appdata-icons.tar.gz? + # XML is 128x128/4Pane.png + + if component_hash[:categories] + if component_hash[:categories][:category].is_a?(Array) + component_hash[:categories][:category].map { |category| package.categories << Category.find_or_create_by(name: category) } + else + package.categories << Category.find_or_create_by(name: component_hash[:categories][:category]) + end + end + + package.assign_attributes(title: title, summary: summary, description: description, appstream: true) + package.increment('weight', 10) + package.save! + CacheScreenshotJob.perform_later(package.id, screenshots) if screenshots&.any? + end + + true + end + + def cleanup_packages + packages_in_primary = primary_xml.elements('package').map { |package| package['name'] } + packages_in_db = packages.pluck(:name) + to_delete = packages_in_db.uniq - packages_in_primary.uniq + + to_delete.each do |package_name| + packages.find_by(name: package_name).delete + logger.debug("Deleted package #{package_name} from distribution #{distribution.name}") + end + end + + def repomd_xml + filename = fetch_by_type(type: 'repomd') + repomd_blob = repodata_files_blobs.find_by(filename: filename) + Xmlhash.parse(repomd_blob.download) + end + + def primary_xml + xml_by_type(type: 'primary') + end + + def appdata_xml + xml_by_type(type: 'appdata') + end + + def updateinfo_xml + xml_by_type(type: 'updateinfo') + end + + # FIXME: This method and everything below is a service... + def xml_by_type(type:) + empty_xml = Xmlhash.parse('') + + filename = fetch_by_type(type: type) + return empty_xml unless filename + + blob = repodata_files_blobs.find_by(filename: filename) + return empty_xml unless blob + + Xmlhash.parse(blob.download) + end + + def fetch_by_type(type:) + full_url = repodata_url + return fetch_repomd if type == 'repomd' + + filename = repomd_xml.elements('data').find { |element| element['type'] == type }['location']['href'].split('/').last + full_url << filename + return filename if repodata_files_blobs.find_by(filename: filename) + + response = http_connection(url: full_url.join('/')).get + raise "Could not fetch #{full_url.join('/')}" unless response.success? + + logger.debug("Successfully fetched #{full_url.join('/')}") + + filepath = Rails.root.join('tmp', 'file-cache', "#{id}-#{filename}") + File.binwrite(filepath, uncompress_gzip(response.body)) + repodata_files.attach(io: File.open(filepath), filename: filename, content_type: response.headers['content-type']) + File.delete(filepath) + + filename + end + + def repodata_url + [url, 'repodata'] + end + + def fetch_repomd + full_url = repodata_url + full_url << 'repomd.xml' + response = http_connection(url: full_url.join('/')).get + raise "Could not fetch #{full_url.join('/')}" unless response.success? + + logger.debug("Successfully fetched #{full_url.join('/')}") + + download_revision = Xmlhash.parse(response.body)['revision'] + + filename = "#{download_revision}-repomd.xml" + return filename if repodata_files_blobs.find_by(filename: filename) + + filepath = Rails.root.join('tmp', 'file-cache', "#{id}-#{filename}") + File.binwrite(filepath, response.body) + repodata_files.attach(io: File.open(filepath), filename: filename, content_type: response.headers['content-type']) + File.delete(filepath) + + update(revision: download_revision) + + filename + end + + def http_connection(url:) + Faraday.new(url) do |conn| + conn.headers['User-Agent'] = 'software.opensuse.org' + conn.use Faraday::FollowRedirects::Middleware + end + end + + # NOTE: Can't use the gzip Faraday middleware, it only considers the Content-Encoding header. + # But mirrors serve the gzip files, not the content of the repomd files encoded in gzip. + # So they only set the header "content-type"=>"application/x-gzip"... + def uncompress_gzip(body) + io = StringIO.new(body) + gzip_reader = Zlib::GzipReader.new(io, encoding: 'ASCII-8BIT') + gzip_reader.read + end +end diff --git a/app/models/screenshot.rb b/app/models/screenshot.rb deleted file mode 100644 index 6c8636dda..000000000 --- a/app/models/screenshot.rb +++ /dev/null @@ -1,103 +0,0 @@ -# frozen_string_literal: true - -require 'open-uri' -require 'mini_magick' - -# Class to cache and resize the screenshot of a given package -class Screenshot - THUMBNAIL_WIDTH = '600' - - # @return [String] name of the package - attr_reader :pkg_name - # @return [String] original (remote) location of the screenshot - attr_reader :source_url - - def initialize(pkg_name, source_url = nil) - @pkg_name = pkg_name - @source_url = source_url - end - - # Relative path of the thumbnail, ready to be passed to #image_tag - # - # If the thumbnail is already available locally or is one of the default - # images (i.e. there is no remote screenshot), it will return the correct - # path right away. - # - # If the screenshot is available remotely but the thumbnail is still not - # generated, it will generate the thumbnail before returning the url if - # :fetch is true or will return nil if :fetch is false. - # - # @return [String] - def thumbnail_path(fetch: true) - begin - generate_thumbnail unless cached? || !fetch || !source_url - # This is sensitive enough (depending on an external system) to - # justify an agressive rescue. #open can produce the following - # rescue Errno::ETIMEDOUT, Net::ReadTimeout, OpenURI::HTTPError => e - # And also there is a chance of exception generating the thumbnail - rescue Exception - raise unless Rails.env.production? - - Rails.logger.error("No screenshot fetched for: #{pkg_name}") - end - - if cached? - generated_thumbnail_path - else - default_thumbnail_path - end - end - - # @return [Boolean] true if a proper thumbnail is available (not a default thumbnail) - # - # This is useful for carousel screenshots, where we don't want to show default thumbnails. - def thumbnail_generated? - cached? - end - - protected - - def cached? - File.exist?(File.join(Rails.root, 'public', generated_thumbnail_path)) - end - - def generate_thumbnail - Rails.logger.debug("Fetching screenshot from #{source_url}") - img = MiniMagick::Image.open(source_url) - img.resize THUMBNAIL_WIDTH - file_path = File.join(Rails.root, 'public', generated_thumbnail_path) - img.write file_path - end - - # Path to the generated thumbnail image - # This is served from /public, and not from the asset pipeline. - def generated_thumbnail_path - "images/thumbnails/#{pkg_name}.png" - end - - # Default thumbnail path, based on the package name. - # This is served from the asset pipeline. - def default_thumbnail_path - file = case pkg_name - when /-devel$/, /-devel-/, /-debug$/, /-debuginfo/, /-debugsource/, /-kmp-/ - 'devel-package.png' - when /-lang$/, /-l10n-/, /-i18n-/, /-translations/ - 'lang-package.png' - when /-doc$/, /-help-/, /-javadoc$/ - 'doc-package.png' - when /^rubygem-/ - 'ruby-package.png' - when /^perl-/ - 'perl-package.png' - when /^python-/, /^python2-/, /^python3-/ - 'python-package.png' - when /^kernel-/ - 'kernel-package.png' - when /^openstack-/i - 'openstack-package.png' - else - 'package.png' - end - "default-screenshots/#{file}" - end -end diff --git a/app/views/download/_download.css.erb b/app/views/download/_download.css.erb deleted file mode 100644 index dc9d30f41..000000000 --- a/app/views/download/_download.css.erb +++ /dev/null @@ -1,40 +0,0 @@ - diff --git a/app/views/download/appliance.erb b/app/views/download/appliance.erb deleted file mode 100644 index c20de33b5..000000000 --- a/app/views/download/appliance.erb +++ /dev/null @@ -1,52 +0,0 @@ -<%= render(:partial => "download", :formats => [:css]) %> -

-
-<% if @data.blank? %> -

<%= _("No appliance data for %{project}") % { project: @project } %>

-<% else %> - <% unless @flavors.blank? %> -

<%= _("%s project") % [@project] %>

-

<%= _("Select the image type") %>

-
- <% @flavors.each do |flavor| %> - - - <% end %> -
- <% else %> -

<%= _("No appliances found in project %s") % @project %>.

- <% end %> - - -
- - <% @flavors.each do |flavor| %> -
-
-
-
- <% @data.select {|k,v| v[:flavor] == flavor }.each do |k,v| %> -
- -
<%= v[:flavor] + " " + _("Image:") %>
- - - -
- <% end %> -
-
-
-
- <% end %> -
- -<% end %> -
-
diff --git a/app/views/download/package.erb b/app/views/download/package.erb deleted file mode 100644 index 330642355..000000000 --- a/app/views/download/package.erb +++ /dev/null @@ -1,130 +0,0 @@ -<%= render(:partial => "download", :formats => [:css]) %> - -
-
- <% if @data.blank? %> -

<%= _("No data for %s / %s") % [ @project, @package] %>

- <% else %> - <% unless @flavors.blank? %> -

- <%= _("#{@package} from #{project_url(@project)} project") %>

-

<%= _("Select Your Operating System") %>

-
- <% @flavors.each do |flavor| %> - - <%end%> -
-
- <% @flavors.each do |flavor| %> -
-
-
-
- <% @data.select {|k,v| v.has_key?(:ymp) && v[:flavor] == flavor}.sort.reverse.each do |k,v|%> - - <% end %> -
-
- -
- <% @data.select {|k,v| v.has_key?(:repo) && !k.nil? && v[:flavor] == flavor}.sort.reverse.each do |k,v| %> - <% secure_apt_url = ['Debian', 'Raspbian'].include?(v[:flavor]) ? "https://wiki.debian.org/SecureApt" : "https://help.ubuntu.com/community/SecureApt" %> - <% if v[:flavor] == "Arch" %> - <% repo_name = @project.gsub(":", "_") + "_" + k %> -
<%= _("For Arch Linux, edit /etc/pacman.conf and add the following (note that the order of repositories in pacman.conf is important, since pacman always downloads the first found package):") %>
-
<%= "[#{repo_name}]" %>
-Server = <%= v[:repo].gsub(/(\w):(\w)/, '\1:/\2') %>$arch
-
-
<%= _("Then run the following as root") %>
-
-key=$(curl -fsSL <%= "#{v[:repo]}$(uname -m)/#{repo_name}.key" %>)
-fingerprint=$(gpg --quiet --with-colons --import-options show-only --import --fingerprint <<< "${key}" | awk -F: '$1 == "fpr" { print $10 }')
-
-pacman-key --init
-pacman-key --add - <<< "${key}"
-pacman-key --lsign-key "${fingerprint}"
-
-pacman -Sy <%= repo_name %>/<%= @package %>
- <% elsif ['Debian', 'Raspbian', 'Ubuntu'].include?(v[:flavor]) %> -
<%= (_("For %s run the following:") % k.gsub('_', ' ').html_safe).html_safe %>
-
<%= (_("Keep in mind that the owner of the key may distribute updates, packages and repositories that your system will trust (more information).") % secure_apt_url).html_safe %>
-
<%=
-                     # don't use apt-add-repository wrapper for Ubuntu for now, because it adds source repo which we don't provide
-                     #        "apt-add-repository deb #{v[:repo]} /\napt-get update\napt-get install #{@package}"
-                     "echo 'deb #{v[:repo].gsub(/(\w):(\w)/, '\1:/\2').gsub(/^https/, 'http')} /' | sudo tee /etc/apt/sources.list.d/#{@project}.list\ncurl -fsSL #{v[:repo]}Release.key | gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/#{@project.gsub(':', '_')}.gpg > /dev/null\nsudo apt update\nsudo apt install #{@package}"
-                     %>
- <% else %> -
<%= (_("For %s run the following as root:") % k.gsub('_', ' ').html_safe).html_safe %>
-
<%=
-                     case v[:flavor]
-                     when 'openSUSE', 'SLE'
-                         "zypper addrepo #{v[:repo]}#{@project}.repo\nzypper refresh\nzypper install #{@package}"
-                     when 'Fedora'
-                       version = k.split("_").last
-                       if version == "Rawhide" or Integer(version) >= 22
-                         "dnf config-manager --add-repo #{v[:repo]}#{@project}.repo\ndnf install #{@package}"
-                       else
-                         "cd /etc/yum.repos.d/\nwget #{v[:repo]}#{@project}.repo\nyum install #{@package}"
-                       end
-                     when 'CentOS', 'RHEL', 'SL'
-                       "cd /etc/yum.repos.d/\nwget #{v[:repo]}#{@project}.repo\nyum install #{@package}"
-                     when 'Univention'
-                       "echo 'deb #{v[:repo].gsub(/(\w):(\w)/, '\1:/\2').gsub(/^https/, 'http')} /' > /etc/apt/sources.list.d/#{@project}.list\nwget -nv #{v[:repo]}Release.key -O Release.key\napt-key add - < Release.key\napt-get update\napt-get install #{@package}"
-                     when 'Mageia', 'Mandriva'
-                       version = k.split("_").last
-                       if version == "Cauldron" or Integer(version) >= 6
-                         "dnf config-manager --add-repo #{v[:repo]}#{@project}.repo\ndnf install #{@package}"
-                       else
-                         "urpmi.addmedia #{@project} #{v[:repo]}\nurpmi.update -a\nurpmi #{@package}"
-                       end
-                     else
-                       '?'
-                     end
-                     %>
- <% end %> - <% end %> -
-
- <% if not @package.nil? %> - -
-
- <% @data.select {|k,v| v.has_key?(:package) && !k.nil? && v[:flavor] == flavor}.sort.reverse.each do |k,v| %> -
-

<%= (_("Packages for %s:") % [("" + k.gsub('_', ' ') + "").html_safe]).html_safe %>

- <% v[:package].sort.each do |k,v| %> - - <% end %> -
- - <% end %> -
-
- <% end %> -
-
-
- <% end %> -
-
-<% else %> -

<%= _("No downloads found for %s in project %s") % [ @package, @project] %>.

-<% end %> -<% end %> -
- diff --git a/app/views/error.html.erb b/app/views/error.html.erb deleted file mode 100644 index c2db4b96a..000000000 --- a/app/views/error.html.erb +++ /dev/null @@ -1,8 +0,0 @@ -<% @page_title = "Error" if @page_title.blank? -%> - -
- -
-

<%=h @message %>

-
-
diff --git a/app/views/layouts/_search_form.html.haml b/app/views/layouts/_search_form.html.haml new file mode 100644 index 000000000..6d8d1ba9a --- /dev/null +++ b/app/views/layouts/_search_form.html.haml @@ -0,0 +1,14 @@ +.search-box.py-5 + .container + = form_tag(search_path, method: :get) do + .row + .col-md + .row.m-0.input-group + .col-md-auto.p-0.input-group-prepend + = select_tag 'distribution', options_for_select(Distribution.all.map { |distribution| [distribution.full_name, distribution.name] }, @distribution), :class => 'btn custom-select' + .col.py-3.p-0.p-md-0.input-group-append + = text_field_tag 'q', @query, :id => "search-form", :class => 'form-control', :placeholder => _('Search packages...'), :autocomplete => "off", :autofocus => true, :size => "60" + .col-md-auto.btn-group.d-flex + %button#search-button.btn.btn-primary.ml-auto.d-flex.align-items-center{type: "submit"} + = icon "search" + %span.ml-1= _("Search") \ No newline at end of file diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 400975064..24086c7ad 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -27,7 +27,7 @@ %main.page-content.flex-fill#content - unless @hide_search_box - = render partial: 'search/find_form', + = render partial: 'layouts/search_form', locals: { baseproject: @baseproject } #search-result-container diff --git a/app/views/main/index.html.haml b/app/views/main/index.html.haml new file mode 100644 index 000000000..98287b6f5 --- /dev/null +++ b/app/views/main/index.html.haml @@ -0,0 +1,5 @@ +.container + .row + .col-md-12 + %p + TODO: This is the place to show the appstore interface... diff --git a/app/views/main/search.html.haml b/app/views/main/search.html.haml new file mode 100644 index 000000000..417d0d211 --- /dev/null +++ b/app/views/main/search.html.haml @@ -0,0 +1,23 @@ +.container + .row + .col-md-12 + #search-result.py-3 + - if @packages.blank? + #search-result-error + #msg.alert.alert-warning + = _("No packages found matching your search. ") + - unless @search_devel + %br/ + = _("You could try to extend your search to development packages or search for another base distribution (currently #{@distribution}).") + - else + #search-result-list.row + - @packages.each_with_index do |package, idx| + .col-sm-6.col-md-4 + .package-card.card.mb-4 + %a.card-img-top{href: distribution_package_path(@distribution.name, package.name), style: "background-image: url(https://software.opensuse.org/assets/default-screenshots/package-2df257d0952fd1c381027c9d7b97b6f0cb7acac30729d1fa7d3afc4bb59d621f.png)"} + .card-body + %h4.card-title + = link_to(distribution_package_path(distribution_name: @distribution.name, name: package.name)) do + = highlight(package.name, params[:q]) + %p.card-text + = highlight(package.summary, params[:q]) diff --git a/app/views/package/_download_rows.html.erb b/app/views/package/_download_rows.html.erb deleted file mode 100644 index 5d9b328c9..000000000 --- a/app/views/package/_download_rows.html.erb +++ /dev/null @@ -1,64 +0,0 @@ -<% packages.flatten.sort_by(&:project).group_by(&:project).each do |result| %> - <% if result.first == distro[:project] || result.first == "#{distro[:project]}:NonFree" - name = _("official release") - elsif result.first == "#{distro[:project]}:Update" || result.first == "#{distro[:project]}:NonFree:Update" - name = _("official update") - else - name = shorten result.first, 40 - end - - web_host = Rails.configuration.x.web_host - if result.last.first.package.nil? - pkg_link = "#{web_host}/project/show/#{CGI.escape result.first}" - else - pkg_link = "#{web_host}/package/show/#{CGI.escape result.first}/#{CGI.escape result.last.first.package}" - end -%> - -
-
- -
- <%= link_to name, pkg_link %> - <% if type.eql? 'official' %> - <%= _('Official') %> - <% end %> - <% if type.eql? 'experimental' %> - <%= _('Experimental') %> - <% end %> - <% if type.eql? 'community' %> - <%= _('Community') %> - <% end %> -
- -
- <%# only use the latest version, obs bug: some outdated versions still listed. %> - <% version = result.last.map{|r| r.version }.max %> - <%= shorten version, 23 %> -
- -
- <% release = result.last.select{|r| r.version == version }.map{|v| "#{v.release}".sub(".", "").to_i}.max %> - <% items = result.last.select{|r| r.version == version && "#{r.release}".sub(".", "").to_i == release } %> - <% archs = items.map{|item| human_arch( item.arch ) }.uniq.sort %> - <% archs << _("Source") if archs.delete(_("Source")) %> - - - <% unless type.eql? 'official' %> - <% if oneclick && distro[:project].match(/SUSE/) && !(archs.uniq == [_("Source")]) %> - <% url = url_for :controller => 'download', :action => 'ymp_without_arch_and_version', - :project => result.first, :repository => result.last.first.repository, :package => @pkgname, - :base => result.last.first.baseproject, :query => @pkgname%> - <%= icon "oci", "1.15em" %> <%= _("1 Click Install") %> - <% end %> - <% end %> - - <% project = result.last.first.fetch("realproject", result.first) %> - <%= link_to download_package_path(project: project, package: @pkgname), class: 'btn btn-sm btn-secondary' do %> - <%= icon "download", "1.15em" %> - <%= _('Expert Download') %> - <% end %> - -
-
-
-<% end -%> diff --git a/app/views/package/explore.html.erb b/app/views/package/explore.html.erb deleted file mode 100644 index 5308403f4..000000000 --- a/app/views/package/explore.html.erb +++ /dev/null @@ -1,46 +0,0 @@ -
- - <% if not @apps.blank? %> - - <%end%> - -
- <% @main_sections.each do |section| %> -
- <%= icon section[:icon], "1.5em" %> - <%= link_to section[:name], {:controller => :package, :action => :category, :category => section[:id]}, :class => 'text-body ml-2' %> -
- <% end %> -
- -
diff --git a/app/views/package/show.html.erb b/app/views/package/show.html.erb deleted file mode 100644 index a2117f667..000000000 --- a/app/views/package/show.html.erb +++ /dev/null @@ -1,137 +0,0 @@ -<%= render :partial => 'search/default_searches' if @search_term %> - -<% if @packages.blank? %> -
-
-

<%= _("Package %s not found...") % [@pkgname] %>

-

<%= _("Back to home page") %>

-
-
- -<% else %> - -
-
-
-
- - <%= @pkgname %> - -
-
-

<%= @name || @pkgname %>

- <% desc_package = search_for_description( @pkgname, @packages ) %> - <% unless desc_package.blank? %> - <% unless @appcategories.blank? %> -

- <%= @appcategories.size > 1 ? _("Categories") : _("Category") %>: - <% @appcategories.each do |cat| -%> - <%= link_to cat, {:controller => :package, :action => :category, :category => cat}, :class => 'badge badge-info' %> - <% end -%> -

- <% end -%> -

<%= desc_package.summary %>

-

<%= prepare_desc desc_package.description -%>

- - <%else %> -

<%= _("No description.") %>

- <% end -%> - - <% if @default_package.blank? %> - <%= _("There is no official package available for %s") % @default_project_name %> - <% else %> - <% - url = url_for :controller => 'download', :action => 'ymp_without_arch_and_version', :query => @pkgname, - :project => @default_package.project, :repository => @default_package.repository, :package => @pkgname, :base => @default_package.baseproject, :protocol => 'https' - %> -
    -
  • <%= _("Version") %> <%= shorten @default_package.version, 13 %>
  • -
  • <%= _("Size") %> <%= number_to_human_size desc_package[:size] if desc_package %>
  • -
  • <%= @default_project_name %>
  • -
- <% if @appid && @baseproject != "ALL" %> - - <% end %> - - <%= link_to download_package_path(project: @default_package.project, package: @pkgname), class: 'btn btn-lg btn-secondary' do %> - <%= icon "download" %> - <%= _('Expert Download') %> - <% end %> - <% end -%> -
-
-
-
- - <% unless @default_package.blank? %> -
- -
- <% end %> -
-

<%= _("Distributions") %>

- - <% @distributions.each do |distro| -%> - <% if (pkgs = @packages.select{|s| s.baseproject == distro[:project]}).size > 0 %> - -

<%= distro[:name] %>

- - - <% - official, pkgs = pkgs.partition{|s| s.project == distro[:project] || s.project == "#{distro[:project]}:NonFree" } - update, pkgs = pkgs.partition{|s| s.project == "#{distro[:project]}:Update" || s.project == "#{distro[:project]}:NonFree:Update"} - stable, pkgs = pkgs.partition{|s| s.quality == "Stable"} %> - - <%= render( :partial => 'package/download_rows', :locals => {:packages => official, :distro => distro, :oneclick => true, :type => 'official'} ) if update.blank? %> - <%= render :partial => 'package/download_rows', :locals => {:packages => update, :distro => distro, :oneclick => true, :type => 'official'} %> - <%= render :partial => 'package/download_rows', :locals => {:packages => stable, :distro => distro, :oneclick => true, :type => 'official'} %> - - <% - slug = distro[:project].downcase.strip.gsub('_', '-').gsub('/', '-').gsub(':', '-').gsub(/[^\w-]/, '') - home, pkgs = pkgs.partition{|s| s.project.match( /^home\:/ )} - devel = pkgs.reject{|s| @official_projects.include?( s.project ) || s.project.match( /^home\:/ ) || s.project.match( /#{distro[:project]}\:Update/ ) || - s.project.match( /#{distro[:project]}\:NonFree/ ) || s.project.match( /openSUSE\:Maintenance\:/ ) } %> - -

- <% devel_disabled = 'disabled' if devel.empty? %> - - <% home_disabled = 'disabled' if home.empty? %> - -

- -
- <%= render :partial => 'package/download_rows', :locals => {:packages => devel, :distro => distro, :oneclick => true, :type => 'experimental' } %> -
- -
- <%= render :partial => 'package/download_rows', :locals => {:packages => home, :distro => distro, :oneclick => true, :type => 'community'} %> -
- <% end -%> - <% end -%> - - <% unless @extra_dists.blank? %> - -

<%= _("Unsupported distributions") %>

- -
- <%= _("The following distributions are not officially supported. Use these packages at your own risk.") %> - -
- -
- <% @extra_dists.each do |distro| -%> -

<%= distro[:project] %>

- <% pks = @extra_packages.select{|p| p.baseproject == distro[:project] } %> - <%= render :partial => 'download_rows', :locals => {:packages => pks, :distro => distro, :oneclick => false, :type => 'unsupported'} %> - <% end %> -
- <% end %> - -
- -<% end -%> diff --git a/app/views/packages/_screenshot_carousel.html.haml b/app/views/packages/_screenshot_carousel.html.haml new file mode 100644 index 000000000..c37d341af --- /dev/null +++ b/app/views/packages/_screenshot_carousel.html.haml @@ -0,0 +1,16 @@ +- if @package.screenshots.any? + #carouselExampleIndicators.carousel.slide{"data-ride" => "carousel"} + %ol.carousel-indicators + %li.active{"data-slide-to" => "0", "data-target" => "#carouselExampleIndicators"} + %li{"data-slide-to" => "1", "data-target" => "#carouselExampleIndicators"} + %li{"data-slide-to" => "2", "data-target" => "#carouselExampleIndicators"} + .carousel-inner + - @package.screenshots.each_with_index do |screenshot, index| + - if index == 0 + .carousel-item.active + %img.img-fluid{alt: "...", src: "#{url_for(screenshot)}"}/ + - else + .carousel-item + %img.img-fluid{alt: "...", src: "#{url_for(screenshot)}"}/ +- else + %img.img-fluid{alt: "...", src: "https://software.opensuse.org/assets/default-screenshots/package-2df257d0952fd1c381027c9d7b97b6f0cb7acac30729d1fa7d3afc4bb59d621f.png"}/ \ No newline at end of file diff --git a/app/views/packages/show.html.haml b/app/views/packages/show.html.haml new file mode 100644 index 000000000..d3d52d095 --- /dev/null +++ b/app/views/packages/show.html.haml @@ -0,0 +1,33 @@ +%body + %header.my-5 + .container + .row + .col-md-6 + = render partial: 'screenshot_carousel' + .col-md-6 + %h1 + = @package.title + - categories = @package.categories.pluck(:name).join(',') + - if categories + %p + = @package.categories.pluck(:name).join(',') + %p + %strong + = @package.summary + %p + = @package.description + %ul.list-inline + %li.list-inline-item + = _("Version") + = @package.release + - if @package.appstream? + .appstream-install + %a#appstream-button.btn.btn-lg.btn-primary{href: "appstream://#{@package.name}"} + = icon "oci" + = _("Appstream Install") + - else + .zypper-install + %code + = "zypper in #{@package.name}" + + diff --git a/app/views/search/_category_header.html.erb b/app/views/search/_category_header.html.erb deleted file mode 100644 index cbc4fce2f..000000000 --- a/app/views/search/_category_header.html.erb +++ /dev/null @@ -1,16 +0,0 @@ -
-

<%= @category %>

-
- -<% unless @related_categories.blank? %> - -<% end -%> diff --git a/app/views/search/_default_searches.html.erb b/app/views/search/_default_searches.html.erb deleted file mode 100644 index cb764a40a..000000000 --- a/app/views/search/_default_searches.html.erb +++ /dev/null @@ -1,7 +0,0 @@ -<% if DEFAULT_SEARCHES[@search_term] %> -
-
- <%=DEFAULT_SEARCHES[@search_term].html_safe %> -
-
-<% end %> diff --git a/app/views/search/_find_form.html.erb b/app/views/search/_find_form.html.erb deleted file mode 100644 index d66608df3..000000000 --- a/app/views/search/_find_form.html.erb +++ /dev/null @@ -1,29 +0,0 @@ - -<%= render :partial => 'search/settings' %> diff --git a/app/views/search/_find_results.html.erb b/app/views/search/_find_results.html.erb deleted file mode 100644 index ba923ef72..000000000 --- a/app/views/search/_find_results.html.erb +++ /dev/null @@ -1,90 +0,0 @@ -
-
- - <%= render :partial => 'search/default_searches' if @search_term %> - - <%= render :partial => 'search/category_header' if @category %> - - <% if @packagenames.blank? %> - -
-
- <%= _("No packages found matching your search. ") %> - <% unless @search_devel %> -
- <%= _("You could try to extend your search to development packages or search for another base distribution (currently #{@baseproject}).") %> - <% end %> -
-
- - <% else %> - -
- - <% - @packagenames.each_with_index do |package, idx| - appdata_pkg = @appdata[:apps].select{|a| a[:pkgname] == package} - package_name = package - package_name = appdata_pkg.first[:name] unless ( appdata_pkg.blank? || appdata_pkg.first[:name].blank? ) - package_img = nil - package_img = appdata_pkg.first[:screenshots].first unless ( appdata_pkg.blank? || appdata_pkg.first[:screenshots].blank? ) - thumb_url = screenshot_thumb_url(package) - %> - -
-
- - - -
-

<%= link_to highlight(package_name, @search_term), :controller => :package, :action => :show, :package => package %>

-

- <% - if( appdata_pkg.blank? || appdata_pkg.first[:summary].blank? ) - desc_package = search_for_description(package, @packages) - unless desc_package.blank? - summary = desc_package.summary - end - else - summary = appdata_pkg.first[:summary] - end - - unless summary.blank? %> - <%= highlight( summary, @search_term) %> - <% end -%> -

- - <%= _("View") %> - - - <% - devel_pack_match = [package + "-devel", package + "-lang", package + "-debuginfo", package + "-debugsource", package + "-debuginfo-32bit", - package + "-debuginfo-x86", ] - devel_packages = @packagenames.select{|r| devel_pack_match.include? r } - sub_packages = @packagenames.sort.select{|name| ( !(devel_pack_match.include? name) && - name.start_with?("#{package}-") && @appdata[:apps].select{|a| a[:pkgname] == name}.blank? ) } -%> - - <% unless devel_packages.blank? && sub_packages.blank? %> - - <% end -%> -
-
-
- <% end -%> -
- <% end %> -
-
diff --git a/app/views/search/_settings.html.erb b/app/views/search/_settings.html.erb deleted file mode 100644 index b9f06cea1..000000000 --- a/app/views/search/_settings.html.erb +++ /dev/null @@ -1,43 +0,0 @@ - diff --git a/app/views/search/find.html.erb b/app/views/search/find.html.erb deleted file mode 100644 index d49ace483..000000000 --- a/app/views/search/find.html.erb +++ /dev/null @@ -1,8 +0,0 @@ -<% @page_title = _("Search") -%> - -<% unless @packagenames.nil? %> - <%= render :partial => 'search/find_results' %> - -<% else %> - -<% end %> \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index 932ae5b9e..7b5202a75 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,20 +1,15 @@ Rails.application.routes.draw do + CONS = { + name: %r{[^/]*}, + distribution_name: %r{[^/]*} + }.freeze - root to: 'package#explore' - resources :search, only: [:index] do - end - - controller :package do - get 'package/:package', action: :show, constraints: { package: /[-+~\w\.:\@]+/ } - get 'package/thumbnail/:package.png', action: :thumbnail, constraints: { package: /[-+~\w\.:\@]+/ } - get 'package/screenshot/:package.png', action: :screenshot, constraints: { package: /[-+~\w\.:\@]+/ } + root to: 'main#index' + get '/search' => 'main#search', as: :search - get 'explore', action: :explore - get 'packages', action: :explore - get 'appstore', action: :explore - get 'packages/:category', action: :category, constraints: { category: /[\w\-\.: ]+/ } - get 'appstore/:category', action: :category, constraints: { category: /[\w\-\.: ]+/ } + resources :distributions, only: [], param: :name do + resources :packages, only: :show, param: :name, constraints: CONS end namespace 'download' do @@ -69,7 +64,4 @@ get 'developer', to: redirect('/distributions/testing') get 'developer/:locale', to: redirect('/distributions/testing?locale=%{locale}') get '/promodvd', to: redirect('/distributions') - - # catch all other params as locales... - get '/:locale', to: 'package#explore' end