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| %>
-
-
- <%= flavor %>
-
-
- <% end %>
-
- <% else %>
-
<%= _("No appliances found in project %s") % @project %>.
- <% end %>
-
-
-
-
- <% @flavors.each do |flavor| %>
-
- <% 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| %>
-
-
- <%= flavor %>
-
- <%end%>
-
-
- <% @flavors.each do |flavor| %>
-
-
-
-
- <% @data.select {|k,v| v.has_key?(:ymp) && v[:flavor] == flavor}.sort.reverse.each do |k,v|%>
-
- <% end %>
-
-
-
- <%= _("Add repository and install manually") %>
-
-
- <% @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? %>
-
- <%= _("Grab binary packages directly") %>
-
-
-
- <% @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| %>
-
- <%= icon "download" %>
- <%= k %>
-
- <% 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? -%>
-
-
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? %>
-
-
-
-
-
-
-
-
-
- <% active = true %>
- <% @apps.sample(5).each do |package| %>
-
-
-
-
- <% active = false %>
- <%end%>
-
-
-
- Previous
-
-
-
- Next
-
-
- <%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? %>
-
-
-<% else %>
-
-
-
-
-
-
-
<%= @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? %>
-
- <%= _("Show %s for other distributions") % @pkgname %>
-
- <% 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? %>
- type="button" data-toggle="collapse" data-target="#<%= slug %>-experimental-packages"><%= _("Show experimental packages") %>
- <% home_disabled = 'disabled' if home.empty? %>
- type="button" data-toggle="collapse" data-target="#<%= slug %>-community-packages"><%= _("Show community packages") %>
-
-
-
- <%= render :partial => 'package/download_rows', :locals => {:packages => devel, :distro => distro, :oneclick => true, :type => 'experimental' } %>
-
-
-
- <% end -%>
- <% end -%>
-
- <% unless @extra_dists.blank? %>
-
-
<%= _("Unsupported distributions") %>
-
-
- <%= _("The following distributions are not officially supported. Use these packages at your own risk.") %>
- <%= _("Show") %>
-
-
-
- <% @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 @@
-
-
-<% unless @related_categories.blank? %>
-
- Related categories
-
- <% @related_categories[0..7].each do |cat| %>
-
- <%= link_to (cat[:name].split /(?=[A-Z][a-z])/).join(" "), { :controller => :package, :action => :category, :category => cat[:name]}, :class => "nav-link" %>
-
- <% end -%>
-
-
-<% 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 @@
-
-
- <%= form_tag( {:controller => 'search', :action => :index}, :method => :get ) do %>
-
-
-
-
- <%= icon "search" %>
- <%= _("Search") %>
-
-
- <%= icon "settings" %>
-
-
-
- <% end -%>
-
-
-<%= 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? %>
-
-
- <%= _("Sub-Packages") %>
-
-
-
- <% 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