diff --git a/apps/api/app/admin/downloads.rb b/apps/api/app/admin/downloads.rb index 3f3a3ba..9fcec6c 100644 --- a/apps/api/app/admin/downloads.rb +++ b/apps/api/app/admin/downloads.rb @@ -1,4 +1,4 @@ -ActiveAdmin.register Download do +ActiveAdmin.register WarpEngine::Download, as: "Download" do actions :index, :show menu priority: 7, label: "📊 Download Stats" @@ -15,21 +15,21 @@ ActiveAdmin.register Download do sidebar "Summary", only: :index do dl style: "display:grid;grid-template-columns:1fr auto;gap:6px 12px;margin:0;" do dt { strong "Total" } - dd { b Download.count.to_s } + dd { b WarpEngine::Download.count.to_s } dt { strong "Today" } - dd { b Download.where("created_at >= ?", Date.current.beginning_of_day).count.to_s } + dd { b WarpEngine::Download.where("created_at >= ?", Date.current.beginning_of_day).count.to_s } dt { strong "This week" } - dd { b Download.where("created_at >= ?", 1.week.ago).count.to_s } + dd { b WarpEngine::Download.where("created_at >= ?", 1.week.ago).count.to_s } dt { strong "This month" } - dd { b Download.where("created_at >= ?", 1.month.ago).count.to_s } + dd { b WarpEngine::Download.where("created_at >= ?", 1.month.ago).count.to_s } end end sidebar "Top 10 Files", only: :index do - table_for Download.group(:file_path) + table_for WarpEngine::Download.group(:file_path) .select("file_path, COUNT(*) as dl_count, MAX(created_at) as last_dl") .order("dl_count DESC") .limit(10) do @@ -39,7 +39,7 @@ ActiveAdmin.register Download do end sidebar "Top 5 Software", only: :index do - rows = Download.joins(release: :software) + rows = WarpEngine::Download.joins(release: :software) .group("softwares.title") .order("count_all DESC") .limit(5) diff --git a/apps/api/app/admin/external_links.rb b/apps/api/app/admin/external_links.rb index 20623fd..f535ff0 100644 --- a/apps/api/app/admin/external_links.rb +++ b/apps/api/app/admin/external_links.rb @@ -1,4 +1,4 @@ -ActiveAdmin.register ExternalLink do +ActiveAdmin.register WarpEngine::ExternalLink, as: "External Link" do permit_params :software_id, :label, :url menu false diff --git a/apps/api/app/admin/files.rb b/apps/api/app/admin/files.rb index afbb2c2..75a4814 100644 --- a/apps/api/app/admin/files.rb +++ b/apps/api/app/admin/files.rb @@ -61,7 +61,7 @@ ActiveAdmin.register_page "Files" do # Pre-fetch download counts for files in current directory file_entries = entries.select { |e| e[:type] != :directory } dl_counts = if file_entries.any? - Download.where(file_path: file_entries.map { |e| e[:path] }) + WarpEngine::Download.where(file_path: file_entries.map { |e| e[:path] }) .group(:file_path) .count else diff --git a/apps/api/app/admin/images.rb b/apps/api/app/admin/images.rb index e17e893..6dfb38c 100644 --- a/apps/api/app/admin/images.rb +++ b/apps/api/app/admin/images.rb @@ -1,9 +1,9 @@ -ActiveAdmin.register Image do +ActiveAdmin.register WarpEngine::Image, as: "Image" do permit_params :file_upload menu priority: 5, label: "🖼️ Images" - used_ids = -> { SoftwareImage.distinct.pluck(:image_id) + ImageUsage.used_image_ids } + used_ids = -> { WarpEngine::SoftwareImage.distinct.pluck(:image_id) + ImageUsage.used_image_ids } scope :all, default: true scope("In use") { |scope| scope.where(id: used_ids.call) } @@ -11,7 +11,7 @@ ActiveAdmin.register Image do batch_action :delete_orphans, confirm: "Delete all selected orphan images and their files?" do |ids| orphan_ids = ids.map(&:to_i) - used_ids.call - Image.where(id: orphan_ids).find_each do |img| + WarpEngine::Image.where(id: orphan_ids).find_each do |img| File.delete(img.file_path) if File.exist?(img.file_path) img.destroy end diff --git a/apps/api/app/admin/members.rb b/apps/api/app/admin/members.rb index dcd40f6..2e3ebeb 100644 --- a/apps/api/app/admin/members.rb +++ b/apps/api/app/admin/members.rb @@ -41,7 +41,7 @@ ActiveAdmin.register Member do f.template.image_tag("/api/image/#{resource.image_id}", style: "max-height:80px;max-width:160px;object-fit:contain;margin-top:6px;").html_safe end f.input :image_id, as: :select, label: "Image", - collection: Image.order(:original_filename).map { |img| [ img.original_filename, img.id ] }, + collection: WarpEngine::Image.order(:original_filename).map { |img| [ img.original_filename, img.id ] }, include_blank: "— no image (use avatar_filename) —", hint: img_hint end diff --git a/apps/api/app/admin/platform_links.rb b/apps/api/app/admin/platform_links.rb index b10d521..b644699 100644 --- a/apps/api/app/admin/platform_links.rb +++ b/apps/api/app/admin/platform_links.rb @@ -1,4 +1,4 @@ -ActiveAdmin.register PlatformLink do +ActiveAdmin.register WarpEngine::PlatformLink, as: "Platform Link" do permit_params :name, :url, :platform, :position menu priority: 5, label: "🔗 Platform Links" diff --git a/apps/api/app/admin/releases.rb b/apps/api/app/admin/releases.rb index e86ff0a..eb1ccbe 100644 --- a/apps/api/app/admin/releases.rb +++ b/apps/api/app/admin/releases.rb @@ -1,4 +1,4 @@ -ActiveAdmin.register Release do +ActiveAdmin.register WarpEngine::Release, as: "Release" do actions :index, :show menu false @@ -38,7 +38,7 @@ ActiveAdmin.register Release do end panel "Downloads by File" do - file_stats = Download.where(release_id: resource.id) + file_stats = WarpEngine::Download.where(release_id: resource.id) .group(:file_path) .select("file_path, COUNT(*) as dl_count, MAX(created_at) as last_dl") .order("dl_count DESC") diff --git a/apps/api/app/admin/softwares.rb b/apps/api/app/admin/softwares.rb index 94ec875..f66b677 100644 --- a/apps/api/app/admin/softwares.rb +++ b/apps/api/app/admin/softwares.rb @@ -1,4 +1,4 @@ -ActiveAdmin.register Software do +ActiveAdmin.register WarpEngine::Software, as: "Software" do permit_params :name, :title, :author, :desc, :story, :license, :platform, :status, :highlighted, software_images_attributes: [ :id, :image_id, :is_default, :position, :file_upload, :_destroy ], @@ -56,7 +56,7 @@ ActiveAdmin.register Software do sidebar "Download Statistics", only: :show do sw = resource - total = Download.joins(:release).where(releases: { software_id: sw.id }).count + total = WarpEngine::Download.joins(:release).where(releases: { software_id: sw.id }).count div style: "margin-bottom:12px;" do strong "Total downloads: " @@ -64,7 +64,7 @@ ActiveAdmin.register Software do end sw.releases.each do |rel| - file_stats = Download.where(release_id: rel.id) + file_stats = WarpEngine::Download.where(release_id: rel.id) .group(:file_path) .select("file_path, COUNT(*) as dl_count, MAX(created_at) as last_dl") .order("dl_count DESC") @@ -108,7 +108,7 @@ ActiveAdmin.register Software do si.template.image_tag("/api/image/#{si.object.image_id}", style: "max-height:80px;max-width:160px;object-fit:contain;margin-top:6px;").html_safe end si.input :image_id, as: :select, label: "Image", - collection: Image.order(:original_filename).map { |img| [ img.original_filename, img.id ] }, + collection: WarpEngine::Image.order(:original_filename).map { |img| [ img.original_filename, img.id ] }, include_blank: "— select —", hint: img_hint si.input :file_upload, as: :file, label: "Upload new image" @@ -128,7 +128,7 @@ ActiveAdmin.register Software do f.has_many :releases, allow_destroy: true, new_record: true do |r| r.input :version r.has_many :release_assets, allow_destroy: true, new_record: true do |ra| - ra.input :kind, as: :select, collection: ReleaseAsset::KINDS, include_blank: false + ra.input :kind, as: :select, collection: WarpEngine::ReleaseAsset::KINDS, include_blank: false ra.input :path end end diff --git a/apps/api/app/models/download.rb b/apps/api/app/models/download.rb deleted file mode 100644 index e8e7659..0000000 --- a/apps/api/app/models/download.rb +++ /dev/null @@ -1,15 +0,0 @@ -class Download < ApplicationRecord - belongs_to :release, optional: true - - validates :file_path, presence: true - - default_scope { where(deleted_at: nil) } - - def self.ransackable_attributes(auth_object = nil) - %w[created_at deleted_at file_path id ip_address referer release_id updated_at user_agent] - end - - def self.ransackable_associations(auth_object = nil) - %w[release] - end -end diff --git a/apps/api/app/models/external_link.rb b/apps/api/app/models/external_link.rb deleted file mode 100644 index fbdc178..0000000 --- a/apps/api/app/models/external_link.rb +++ /dev/null @@ -1,18 +0,0 @@ -class ExternalLink < ApplicationRecord - self.table_name = "external_links" - - belongs_to :software - - validates :label, presence: true - validates :url, presence: true - - default_scope { where(deleted_at: nil) } - - def self.ransackable_attributes(auth_object = nil) - %w[created_at deleted_at id label software_id updated_at url] - end - - def self.ransackable_associations(auth_object = nil) - %w[software] - end -end diff --git a/apps/api/app/models/image.rb b/apps/api/app/models/image.rb deleted file mode 100644 index 7fc0804..0000000 --- a/apps/api/app/models/image.rb +++ /dev/null @@ -1,33 +0,0 @@ -class Image < ApplicationRecord - # Híváskor kiértékelve, hogy a path ne class-load-kor rögzüljön. - def self.upload_path - ENV.fetch("IMAGE_CONTAINER_PATH", "/images") - end - - has_many :software_images, dependent: :restrict_with_error - - default_scope { where(deleted_at: nil) } - - attr_accessor :file_upload - - before_save :process_upload, if: -> { file_upload.present? } - - def self.ransackable_attributes(auth_object = nil) - %w[content_type created_at deleted_at filename id original_filename updated_at] - end - - def file_path - File.join(self.class.upload_path, filename.to_s) - end - - private - - def process_upload - FileUtils.mkdir_p(self.class.upload_path) - self.original_filename = file_upload.original_filename - self.content_type = file_upload.content_type.presence || "application/octet-stream" - ext = File.extname(file_upload.original_filename) - self.filename = "#{SecureRandom.uuid}#{ext}" - IO.copy_stream(file_upload.to_io, file_path) - end -end diff --git a/apps/api/app/models/member.rb b/apps/api/app/models/member.rb index 11026e3..af1be40 100644 --- a/apps/api/app/models/member.rb +++ b/apps/api/app/models/member.rb @@ -1,5 +1,5 @@ class Member < ApplicationRecord - belongs_to :image, optional: true + belongs_to :image, class_name: "WarpEngine::Image", optional: true has_one :admin_user validates :nick, presence: true, uniqueness: true diff --git a/apps/api/app/models/platform_link.rb b/apps/api/app/models/platform_link.rb deleted file mode 100644 index df2c2ff..0000000 --- a/apps/api/app/models/platform_link.rb +++ /dev/null @@ -1,23 +0,0 @@ -class PlatformLink < ApplicationRecord - self.table_name = "platform_links" - - SUPPORTED_PLATFORMS = %w[tic80 ebitengine love c64 godot bevy phaser].freeze - - validates :name, presence: true - validates :url, presence: true - validates :platform, presence: true, inclusion: { in: SUPPORTED_PLATFORMS } - - default_scope { where(deleted_at: nil).order(:position) } - - def self.ransackable_attributes(auth_object = nil) - %w[created_at deleted_at id name platform position updated_at url] - end - - def self.ransackable_associations(auth_object = nil) - [] - end - - def self.for_platform(platform) - where(platform: platform).to_a - end -end diff --git a/apps/api/app/models/release.rb b/apps/api/app/models/release.rb deleted file mode 100644 index 5ec553f..0000000 --- a/apps/api/app/models/release.rb +++ /dev/null @@ -1,33 +0,0 @@ -class Release < ApplicationRecord - self.table_name = "releases" - - belongs_to :software - has_many :downloads - has_many :release_assets - - accepts_nested_attributes_for :release_assets, allow_destroy: true - - validates :version, presence: true - validates :version, uniqueness: { scope: :software_id } - - default_scope { where(deleted_at: nil) } - - validate :c64_cannot_be_web_playable - - def c64_cannot_be_web_playable - return unless software&.platform == "c64" - - has_html = release_assets.reject(&:marked_for_destruction?).any? { |a| a.kind == "html" } - if has_html - errors.add(:base, "C64 releases cannot be web playable (download only)") - end - end - - def self.ransackable_attributes(auth_object = nil) - %w[created_at deleted_at id software_id updated_at version] - end - - def self.ransackable_associations(auth_object = nil) - %w[downloads release_assets software] - end -end diff --git a/apps/api/app/models/release_asset.rb b/apps/api/app/models/release_asset.rb deleted file mode 100644 index ab9e39e..0000000 --- a/apps/api/app/models/release_asset.rb +++ /dev/null @@ -1,22 +0,0 @@ -class ReleaseAsset < ApplicationRecord - KINDS = %w[cartridge source html docs - win_x86 win_x64 linux_x86 linux_x64 - mac_x64 mac_arm64 mac_universal].freeze - - belongs_to :release - - enum :kind, KINDS.index_by(&:itself) - - validates :path, presence: true - validates :kind, uniqueness: { scope: :release_id } - - default_scope { where(deleted_at: nil) } - - def self.ransackable_attributes(auth_object = nil) - %w[created_at deleted_at id kind path release_id updated_at] - end - - def self.ransackable_associations(auth_object = nil) - %w[release] - end -end diff --git a/apps/api/app/models/software.rb b/apps/api/app/models/software.rb deleted file mode 100644 index 326f02b..0000000 --- a/apps/api/app/models/software.rb +++ /dev/null @@ -1,27 +0,0 @@ -class Software < ApplicationRecord - self.table_name = "softwares" - - has_many :software_images, foreign_key: :software_id, dependent: :destroy - has_many :images, through: :software_images - has_many :releases, foreign_key: :software_id - has_many :downloads, through: :releases - has_many :external_links, foreign_key: :software_id - - accepts_nested_attributes_for :software_images, allow_destroy: true - accepts_nested_attributes_for :external_links, allow_destroy: true - accepts_nested_attributes_for :releases, allow_destroy: true - - validates :name, presence: true, uniqueness: true - validates :title, presence: true - validates :platform, presence: true - - default_scope { where(deleted_at: nil) } - - def self.ransackable_attributes(auth_object = nil) - %w[author created_at desc highlighted id license name platform site status story title updated_at] - end - - def self.ransackable_associations(auth_object = nil) - %w[releases external_links software_images images] - end -end diff --git a/apps/api/app/models/software_image.rb b/apps/api/app/models/software_image.rb deleted file mode 100644 index 9f07a07..0000000 --- a/apps/api/app/models/software_image.rb +++ /dev/null @@ -1,41 +0,0 @@ -class SoftwareImage < ApplicationRecord - belongs_to :software - belongs_to :image, optional: true - - attr_accessor :file_upload - - before_validation :create_image_from_upload, if: -> { file_upload.present? } - - validates :image_id, presence: true - validates :image_id, uniqueness: { scope: :software_id } - validates :position, numericality: { only_integer: true, greater_than_or_equal_to: 0 } - - before_save :unset_other_defaults, if: -> { is_default? && is_default_changed? } - - default_scope { order(:position) } - - def self.ransackable_attributes(auth_object = nil) - %w[created_at id image_id is_default position software_id updated_at] - end - - def self.ransackable_associations(auth_object = nil) - %w[image software] - end - - private - - def create_image_from_upload - img = Image.new(file_upload: file_upload) - if img.save - self.image = img - else - errors.add(:file_upload, img.errors.full_messages.join(", ")) - end - end - - def unset_other_defaults - SoftwareImage.where(software_id: software_id, is_default: true) - .where.not(id: id) - .update_all(is_default: false) - end -end diff --git a/apps/api/app/serializers/software_serializer.rb b/apps/api/app/serializers/software_serializer.rb index dad76ef..a145f7d 100644 --- a/apps/api/app/serializers/software_serializer.rb +++ b/apps/api/app/serializers/software_serializer.rb @@ -15,7 +15,7 @@ class SoftwareSerializer < Blueprinter::Base field :status field(:highlighted) { |sw| sw.highlighted ? true : false } field(:externalLinks) { |sw| ExternalLinkSerializer.render_as_hash(sw.external_links) } - field(:platformLinks) { |sw| PlatformLinkSerializer.render_as_hash(PlatformLink.for_platform(sw.platform)) } + field(:platformLinks) { |sw| PlatformLinkSerializer.render_as_hash(WarpEngine::PlatformLink.for_platform(sw.platform)) } field(:imageUrl) { |sw| si = sw.software_images.detect(&:is_default?) || sw.software_images.first si ? "/api/image/#{si.image_id}" : nil diff --git a/apps/api/app/services/builds_service.rb b/apps/api/app/services/builds_service.rb index f16d548..2e0e3ee 100644 --- a/apps/api/app/services/builds_service.rb +++ b/apps/api/app/services/builds_service.rb @@ -1,6 +1,6 @@ class BuildsService def index - platforms = PlatformLink::SUPPORTED_PLATFORMS.each_with_object({}) do |platform, hash| + platforms = WarpEngine::PlatformLink::SUPPORTED_PLATFORMS.each_with_object({}) do |platform, hash| service_class = "SoftwareUpdater::#{platform.camelize}Service".constantize hash[platform] = { label: service_class.label, @@ -8,13 +8,13 @@ class BuildsService } end - all_kinds = ReleaseAsset::KINDS + all_kinds = WarpEngine::ReleaseAsset::KINDS { platforms: platforms, allKinds: all_kinds } end def show(name) - software = Software.find_by!(name: name) + software = WarpEngine::Software.find_by!(name: name) service_class = "SoftwareUpdater::#{software.platform.camelize}Service".constantize expected = service_class.expected_kinds diff --git a/apps/api/app/services/concerns/software_updater/software_persistence.rb b/apps/api/app/services/concerns/software_updater/software_persistence.rb index b22ffb2..c36c997 100644 --- a/apps/api/app/services/concerns/software_updater/software_persistence.rb +++ b/apps/api/app/services/concerns/software_updater/software_persistence.rb @@ -3,7 +3,7 @@ module SoftwareUpdater private def update_or_create_software(attrs) - software = Software.unscoped.find_or_initialize_by(name: attrs[:name]) + software = WarpEngine::Software.unscoped.find_or_initialize_by(name: attrs[:name]) software.assign_attributes(attrs.except(:name)) software.deleted_at = nil software.save! @@ -11,22 +11,22 @@ module SoftwareUpdater end def upsert_external_link(software_id, label, url) - link = ExternalLink.unscoped.find_or_initialize_by(software_id: software_id, label: label) + link = WarpEngine::ExternalLink.unscoped.find_or_initialize_by(software_id: software_id, label: label) link.url = url link.deleted_at = nil link.save! end def create_release_if_not_exists(attrs) - existing = Release.unscoped.find_by(software_id: attrs[:software_id], version: attrs[:version]) + existing = WarpEngine::Release.unscoped.find_by(software_id: attrs[:software_id], version: attrs[:version]) return existing if existing - Release.create!(attrs) + WarpEngine::Release.create!(attrs) end def sync_release_assets(release, kind_paths) kind_paths.each do |kind, path| - asset = ReleaseAsset.unscoped.find_or_initialize_by(release_id: release.id, kind: kind) + asset = WarpEngine::ReleaseAsset.unscoped.find_or_initialize_by(release_id: release.id, kind: kind) asset.path = path asset.deleted_at = nil asset.save! diff --git a/apps/api/app/services/download_service.rb b/apps/api/app/services/download_service.rb index bc242c7..bdfcb7f 100644 --- a/apps/api/app/services/download_service.rb +++ b/apps/api/app/services/download_service.rb @@ -16,10 +16,10 @@ class DownloadService return nil unless File.file?(full_path) escaped = sanitized.gsub("%", "\\%").gsub("_", "\\_") - asset = ReleaseAsset.find_by(path: File.join(self.class.container_base, sanitized)) || - ReleaseAsset.where("path LIKE ?", "%#{escaped}%").first + asset = WarpEngine::ReleaseAsset.find_by(path: File.join(self.class.container_base, sanitized)) || + WarpEngine::ReleaseAsset.where("path LIKE ?", "%#{escaped}%").first - Download.create!( + WarpEngine::Download.create!( file_path: sanitized, release: asset&.release, ip_address: ip, diff --git a/apps/api/app/services/image_service.rb b/apps/api/app/services/image_service.rb index f2209f2..2d637c4 100644 --- a/apps/api/app/services/image_service.rb +++ b/apps/api/app/services/image_service.rb @@ -1,5 +1,5 @@ class ImageService def show(input) - Image.find(input.id) + WarpEngine::Image.find(input.id) end end diff --git a/apps/api/app/services/rss_service.rb b/apps/api/app/services/rss_service.rb index a135fa5..9bbe087 100644 --- a/apps/api/app/services/rss_service.rb +++ b/apps/api/app/services/rss_service.rb @@ -26,7 +26,7 @@ class RssService end def releases_feed - releases = Release.includes(:software) + releases = WarpEngine::Release.includes(:software) .order(created_at: :desc) .limit(50) diff --git a/apps/api/app/services/software_highlighted_service.rb b/apps/api/app/services/software_highlighted_service.rb index 9a37015..971de78 100644 --- a/apps/api/app/services/software_highlighted_service.rb +++ b/apps/api/app/services/software_highlighted_service.rb @@ -2,13 +2,13 @@ class SoftwareHighlightedService include SoftwareResponseBuilder def index - software = Software.includes(:external_links, :software_images) + software = WarpEngine::Software.includes(:external_links, :software_images) .where(highlighted: true) .order(id: :desc) .first return nil unless software - releases = Release.includes(:release_assets).where(software_id: software.id).to_a + releases = WarpEngine::Release.includes(:release_assets).where(software_id: software.id).to_a build_response(software, releases, download_counts_for(releases.map(&:id))) end end diff --git a/apps/api/app/services/software_response_builder.rb b/apps/api/app/services/software_response_builder.rb index a507e8f..b7413b4 100644 --- a/apps/api/app/services/software_response_builder.rb +++ b/apps/api/app/services/software_response_builder.rb @@ -20,6 +20,6 @@ module SoftwareResponseBuilder # Egyetlen csoportosított lekérdezés release-enkénti letöltésszámokhoz (N+1 helyett). def download_counts_for(release_ids) return {} if release_ids.empty? - Download.where(release_id: release_ids).group(:release_id).count + WarpEngine::Download.where(release_id: release_ids).group(:release_id).count end end diff --git a/apps/api/app/services/software_service.rb b/apps/api/app/services/software_service.rb index 3e16178..3141b5f 100644 --- a/apps/api/app/services/software_service.rb +++ b/apps/api/app/services/software_service.rb @@ -2,7 +2,7 @@ class SoftwareService include SoftwareResponseBuilder def index - softwares = Software.includes(releases: [ :release_assets ]).includes(:external_links, :software_images).all + softwares = WarpEngine::Software.includes(releases: [ :release_assets ]).includes(:external_links, :software_images).all counts = download_counts_for(softwares.flat_map { |sw| sw.releases.map(&:id) }) { softwares: softwares.map { |sw| build_response(sw, sw.releases.to_a, counts) } } end diff --git a/apps/api/app/services/update_service.rb b/apps/api/app/services/update_service.rb index 688b0e6..a78fc3b 100644 --- a/apps/api/app/services/update_service.rb +++ b/apps/api/app/services/update_service.rb @@ -1,6 +1,6 @@ class UpdateService def update(input) - unless PlatformLink::SUPPORTED_PLATFORMS.include?(input.platform) + unless WarpEngine::PlatformLink::SUPPORTED_PLATFORMS.include?(input.platform) raise ArgumentError, "Unsupported platform: #{input.platform}" end diff --git a/apps/api/spec/controllers/software_builds_controller_spec.rb b/apps/api/spec/controllers/software_builds_controller_spec.rb index 33e12b3..9007b10 100644 --- a/apps/api/spec/controllers/software_builds_controller_spec.rb +++ b/apps/api/spec/controllers/software_builds_controller_spec.rb @@ -6,7 +6,7 @@ RSpec.describe Api::SoftwareBuildsController, type: :request do let!(:release) { create(:release, software: software, version: "2.0.0") } before do - ReleaseAsset.create!(release: release, kind: "html", path: "/test/html") + WarpEngine::ReleaseAsset.create!(release: release, kind: "html", path: "/test/html") end it "returns per-software build info" do diff --git a/apps/api/spec/factories/downloads.rb b/apps/api/spec/factories/downloads.rb index 7f70df3..c305277 100644 --- a/apps/api/spec/factories/downloads.rb +++ b/apps/api/spec/factories/downloads.rb @@ -1,5 +1,5 @@ FactoryBot.define do - factory :download do + factory :download, class: "WarpEngine::Download" do file_path { "/test/file.tic" } ip_address { "127.0.0.1" } end diff --git a/apps/api/spec/factories/platform_links.rb b/apps/api/spec/factories/platform_links.rb index 4847fd2..06b2ec7 100644 --- a/apps/api/spec/factories/platform_links.rb +++ b/apps/api/spec/factories/platform_links.rb @@ -1,5 +1,5 @@ FactoryBot.define do - factory :platform_link do + factory :platform_link, class: "WarpEngine::PlatformLink" do sequence(:name) { |n| "Link #{n}" } platform { "tic80" } url { "https://example.com/link" } diff --git a/apps/api/spec/factories/releases.rb b/apps/api/spec/factories/releases.rb index 2aafda3..6a8bae1 100644 --- a/apps/api/spec/factories/releases.rb +++ b/apps/api/spec/factories/releases.rb @@ -1,5 +1,5 @@ FactoryBot.define do - factory :release do + factory :release, class: "WarpEngine::Release" do software sequence(:version) { |n| "1.0.#{n}" } end diff --git a/apps/api/spec/factories/softwares.rb b/apps/api/spec/factories/softwares.rb index 346684f..353ceb7 100644 --- a/apps/api/spec/factories/softwares.rb +++ b/apps/api/spec/factories/softwares.rb @@ -1,5 +1,5 @@ FactoryBot.define do - factory :software do + factory :software, class: "WarpEngine::Software" do sequence(:name) { |n| "game-#{n}" } title { "Test Game" } author { "dev" } diff --git a/apps/api/spec/models/download_spec.rb b/apps/api/spec/models/download_spec.rb index 84286f6..aa196a7 100644 --- a/apps/api/spec/models/download_spec.rb +++ b/apps/api/spec/models/download_spec.rb @@ -1,6 +1,6 @@ require "rails_helper" -RSpec.describe Download, type: :model do +RSpec.describe WarpEngine::Download, type: :model do it { should validate_presence_of(:file_path) } it { should belong_to(:release).optional } end diff --git a/apps/api/spec/models/platform_link_spec.rb b/apps/api/spec/models/platform_link_spec.rb index fe7212c..b04ef90 100644 --- a/apps/api/spec/models/platform_link_spec.rb +++ b/apps/api/spec/models/platform_link_spec.rb @@ -1,6 +1,6 @@ require "rails_helper" -RSpec.describe PlatformLink, type: :model do +RSpec.describe WarpEngine::PlatformLink, type: :model do describe "validations" do it { should validate_presence_of(:name) } it { should validate_presence_of(:url) } @@ -13,7 +13,7 @@ RSpec.describe PlatformLink, type: :model do end it "accepts valid platforms" do - PlatformLink::SUPPORTED_PLATFORMS.each do |p| + WarpEngine::PlatformLink::SUPPORTED_PLATFORMS.each do |p| link = build(:platform_link, platform: p) expect(link).to be_valid end @@ -25,14 +25,14 @@ RSpec.describe PlatformLink, type: :model do active = create(:platform_link) create(:platform_link, deleted_at: Time.current) - expect(PlatformLink.all).to eq([active]) + expect(WarpEngine::PlatformLink.all).to eq([active]) end it "orders by position" do second = create(:platform_link, position: 2) first = create(:platform_link, position: 1) - expect(PlatformLink.all).to eq([first, second]) + expect(WarpEngine::PlatformLink.all).to eq([first, second]) end end @@ -41,12 +41,12 @@ RSpec.describe PlatformLink, type: :model do tic80_link = create(:platform_link, platform: "tic80") create(:platform_link, platform: "love") - result = PlatformLink.for_platform("tic80") + result = WarpEngine::PlatformLink.for_platform("tic80") expect(result).to eq([tic80_link]) end it "returns empty array for platform without links" do - expect(PlatformLink.for_platform("godot")).to eq([]) + expect(WarpEngine::PlatformLink.for_platform("godot")).to eq([]) end end end diff --git a/apps/api/spec/models/release_spec.rb b/apps/api/spec/models/release_spec.rb index 45b986b..1b5ebbb 100644 --- a/apps/api/spec/models/release_spec.rb +++ b/apps/api/spec/models/release_spec.rb @@ -1,6 +1,6 @@ require "rails_helper" -RSpec.describe Release, type: :model do +RSpec.describe WarpEngine::Release, type: :model do it { should belong_to(:software) } it { should have_many(:downloads) } diff --git a/apps/api/spec/models/software_spec.rb b/apps/api/spec/models/software_spec.rb index fdbfbfa..19c348a 100644 --- a/apps/api/spec/models/software_spec.rb +++ b/apps/api/spec/models/software_spec.rb @@ -1,6 +1,6 @@ require "rails_helper" -RSpec.describe Software, type: :model do +RSpec.describe WarpEngine::Software, type: :model do subject { build(:software) } it { should validate_presence_of(:name) } @@ -19,7 +19,7 @@ RSpec.describe Software, type: :model do active = create(:software) create(:software, deleted_at: Time.current) - expect(Software.all).to eq([active]) + expect(WarpEngine::Software.all).to eq([active]) end end end diff --git a/apps/api/spec/services/builds_service_spec.rb b/apps/api/spec/services/builds_service_spec.rb index feb27ad..33ecd91 100644 --- a/apps/api/spec/services/builds_service_spec.rb +++ b/apps/api/spec/services/builds_service_spec.rb @@ -14,12 +14,12 @@ RSpec.describe BuildsService do expect(result[:platforms]["c64"][:kinds]).to eq(["cartridge"]) end - it "returns allKinds matching ReleaseAsset::KINDS" do - expect(result[:allKinds]).to eq(ReleaseAsset::KINDS) + it "returns allKinds matching WarpEngine::ReleaseAsset::KINDS" do + expect(result[:allKinds]).to eq(WarpEngine::ReleaseAsset::KINDS) end it "includes all supported platforms" do - PlatformLink::SUPPORTED_PLATFORMS.each do |platform| + WarpEngine::PlatformLink::SUPPORTED_PLATFORMS.each do |platform| expect(result[:platforms]).to have_key(platform) end end @@ -30,8 +30,8 @@ RSpec.describe BuildsService do let!(:release) { create(:release, software: software, version: "1.0.0") } before do - ReleaseAsset.create!(release: release, kind: "html", path: "/test/html") - ReleaseAsset.create!(release: release, kind: "win_x64", path: "/test/win") + WarpEngine::ReleaseAsset.create!(release: release, kind: "html", path: "/test/html") + WarpEngine::ReleaseAsset.create!(release: release, kind: "win_x64", path: "/test/win") end it "returns expected and actual kinds per release" do diff --git a/apps/api/spec/services/software_updater/tic80_service_spec.rb b/apps/api/spec/services/software_updater/tic80_service_spec.rb index d891fe9..a3ac934 100644 --- a/apps/api/spec/services/software_updater/tic80_service_spec.rb +++ b/apps/api/spec/services/software_updater/tic80_service_spec.rb @@ -25,12 +25,12 @@ RSpec.describe SoftwareUpdater::Tic80Service do after do FileUtils.remove_entry(tmpdir) - Software.unscoped.where(name: name).each do |sw| - Release.unscoped.where(software_id: sw.id).each do |r| - ReleaseAsset.unscoped.where(release_id: r.id).delete_all + WarpEngine::Software.unscoped.where(name: name).each do |sw| + WarpEngine::Release.unscoped.where(software_id: sw.id).each do |r| + WarpEngine::ReleaseAsset.unscoped.where(release_id: r.id).delete_all r.delete end - ExternalLink.unscoped.where(software_id: sw.id).delete_all + WarpEngine::ExternalLink.unscoped.where(software_id: sw.id).delete_all sw.delete end end @@ -44,7 +44,7 @@ RSpec.describe SoftwareUpdater::Tic80Service do end def asset_kinds(release) - ReleaseAsset.unscoped.where(release_id: release.id).pluck(:kind) + WarpEngine::ReleaseAsset.unscoped.where(release_id: release.id).pluck(:kind) end it "registers the release without a docs zip" do diff --git a/libs/ruby/warp_engine/app/models/warp_engine/application_record.rb b/libs/ruby/warp_engine/app/models/warp_engine/application_record.rb new file mode 100644 index 0000000..9c62512 --- /dev/null +++ b/libs/ruby/warp_engine/app/models/warp_engine/application_record.rb @@ -0,0 +1,5 @@ +module WarpEngine + class ApplicationRecord < ActiveRecord::Base + self.abstract_class = true + end +end diff --git a/libs/ruby/warp_engine/app/models/warp_engine/download.rb b/libs/ruby/warp_engine/app/models/warp_engine/download.rb new file mode 100644 index 0000000..0247998 --- /dev/null +++ b/libs/ruby/warp_engine/app/models/warp_engine/download.rb @@ -0,0 +1,19 @@ +module WarpEngine + class Download < ApplicationRecord + belongs_to :release, optional: true + + validates :file_path, presence: true + + default_scope { where(deleted_at: nil) } + + def self.ransackable_attributes(auth_object = nil) + %w[created_at deleted_at file_path id ip_address referer release_id updated_at user_agent] + end + + def self.ransackable_associations(auth_object = nil) + %w[release] + end + + ActiveSupport.run_load_hooks(:warp_engine_download, self) + end +end diff --git a/libs/ruby/warp_engine/app/models/warp_engine/external_link.rb b/libs/ruby/warp_engine/app/models/warp_engine/external_link.rb new file mode 100644 index 0000000..b582f01 --- /dev/null +++ b/libs/ruby/warp_engine/app/models/warp_engine/external_link.rb @@ -0,0 +1,22 @@ +module WarpEngine + class ExternalLink < ApplicationRecord + self.table_name = "external_links" + + belongs_to :software + + validates :label, presence: true + validates :url, presence: true + + default_scope { where(deleted_at: nil) } + + def self.ransackable_attributes(auth_object = nil) + %w[created_at deleted_at id label software_id updated_at url] + end + + def self.ransackable_associations(auth_object = nil) + %w[software] + end + + ActiveSupport.run_load_hooks(:warp_engine_external_link, self) + end +end diff --git a/libs/ruby/warp_engine/app/models/warp_engine/image.rb b/libs/ruby/warp_engine/app/models/warp_engine/image.rb new file mode 100644 index 0000000..5b2212f --- /dev/null +++ b/libs/ruby/warp_engine/app/models/warp_engine/image.rb @@ -0,0 +1,36 @@ +module WarpEngine + class Image < ApplicationRecord + def self.upload_path + WarpEngine.config.image_container_path + end + + has_many :software_images, dependent: :restrict_with_error + + default_scope { where(deleted_at: nil) } + + attr_accessor :file_upload + + before_save :process_upload, if: -> { file_upload.present? } + + def self.ransackable_attributes(auth_object = nil) + %w[content_type created_at deleted_at filename id original_filename updated_at] + end + + def file_path + File.join(self.class.upload_path, filename.to_s) + end + + ActiveSupport.run_load_hooks(:warp_engine_image, self) + + private + + def process_upload + FileUtils.mkdir_p(self.class.upload_path) + self.original_filename = file_upload.original_filename + self.content_type = file_upload.content_type.presence || "application/octet-stream" + ext = File.extname(file_upload.original_filename) + self.filename = "#{SecureRandom.uuid}#{ext}" + IO.copy_stream(file_upload.to_io, file_path) + end + end +end diff --git a/libs/ruby/warp_engine/app/models/warp_engine/platform_link.rb b/libs/ruby/warp_engine/app/models/warp_engine/platform_link.rb new file mode 100644 index 0000000..c2de668 --- /dev/null +++ b/libs/ruby/warp_engine/app/models/warp_engine/platform_link.rb @@ -0,0 +1,27 @@ +module WarpEngine + class PlatformLink < ApplicationRecord + self.table_name = "platform_links" + + SUPPORTED_PLATFORMS = %w[tic80 ebitengine love c64 godot bevy phaser].freeze + + validates :name, presence: true + validates :url, presence: true + validates :platform, presence: true, inclusion: { in: SUPPORTED_PLATFORMS } + + default_scope { where(deleted_at: nil).order(:position) } + + def self.ransackable_attributes(auth_object = nil) + %w[created_at deleted_at id name platform position updated_at url] + end + + def self.ransackable_associations(auth_object = nil) + [] + end + + def self.for_platform(platform) + where(platform: platform).to_a + end + + ActiveSupport.run_load_hooks(:warp_engine_platform_link, self) + end +end diff --git a/libs/ruby/warp_engine/app/models/warp_engine/release.rb b/libs/ruby/warp_engine/app/models/warp_engine/release.rb new file mode 100644 index 0000000..8938887 --- /dev/null +++ b/libs/ruby/warp_engine/app/models/warp_engine/release.rb @@ -0,0 +1,37 @@ +module WarpEngine + class Release < ApplicationRecord + self.table_name = "releases" + + belongs_to :software + has_many :downloads + has_many :release_assets + + accepts_nested_attributes_for :release_assets, allow_destroy: true + + validates :version, presence: true + validates :version, uniqueness: { scope: :software_id } + + default_scope { where(deleted_at: nil) } + + validate :c64_cannot_be_web_playable + + def c64_cannot_be_web_playable + return unless software&.platform == "c64" + + has_html = release_assets.reject(&:marked_for_destruction?).any? { |a| a.kind == "html" } + if has_html + errors.add(:base, "C64 releases cannot be web playable (download only)") + end + end + + def self.ransackable_attributes(auth_object = nil) + %w[created_at deleted_at id software_id updated_at version] + end + + def self.ransackable_associations(auth_object = nil) + %w[downloads release_assets software] + end + + ActiveSupport.run_load_hooks(:warp_engine_release, self) + end +end diff --git a/libs/ruby/warp_engine/app/models/warp_engine/release_asset.rb b/libs/ruby/warp_engine/app/models/warp_engine/release_asset.rb new file mode 100644 index 0000000..001d69e --- /dev/null +++ b/libs/ruby/warp_engine/app/models/warp_engine/release_asset.rb @@ -0,0 +1,26 @@ +module WarpEngine + class ReleaseAsset < ApplicationRecord + KINDS = %w[cartridge source html docs + win_x86 win_x64 linux_x86 linux_x64 + mac_x64 mac_arm64 mac_universal].freeze + + belongs_to :release + + enum :kind, KINDS.index_by(&:itself) + + validates :path, presence: true + validates :kind, uniqueness: { scope: :release_id } + + default_scope { where(deleted_at: nil) } + + def self.ransackable_attributes(auth_object = nil) + %w[created_at deleted_at id kind path release_id updated_at] + end + + def self.ransackable_associations(auth_object = nil) + %w[release] + end + + ActiveSupport.run_load_hooks(:warp_engine_release_asset, self) + end +end diff --git a/libs/ruby/warp_engine/app/models/warp_engine/software.rb b/libs/ruby/warp_engine/app/models/warp_engine/software.rb new file mode 100644 index 0000000..b6d4d32 --- /dev/null +++ b/libs/ruby/warp_engine/app/models/warp_engine/software.rb @@ -0,0 +1,31 @@ +module WarpEngine + class Software < ApplicationRecord + self.table_name = "softwares" + + has_many :software_images, foreign_key: :software_id, dependent: :destroy + has_many :images, through: :software_images + has_many :releases, foreign_key: :software_id + has_many :downloads, through: :releases + has_many :external_links, foreign_key: :software_id + + accepts_nested_attributes_for :software_images, allow_destroy: true + accepts_nested_attributes_for :external_links, allow_destroy: true + accepts_nested_attributes_for :releases, allow_destroy: true + + validates :name, presence: true, uniqueness: true + validates :title, presence: true + validates :platform, presence: true + + default_scope { where(deleted_at: nil) } + + def self.ransackable_attributes(auth_object = nil) + %w[author created_at desc highlighted id license name platform site status story title updated_at] + end + + def self.ransackable_associations(auth_object = nil) + %w[releases external_links software_images images] + end + + ActiveSupport.run_load_hooks(:warp_engine_software, self) + end +end diff --git a/libs/ruby/warp_engine/app/models/warp_engine/software_image.rb b/libs/ruby/warp_engine/app/models/warp_engine/software_image.rb new file mode 100644 index 0000000..3a0165c --- /dev/null +++ b/libs/ruby/warp_engine/app/models/warp_engine/software_image.rb @@ -0,0 +1,45 @@ +module WarpEngine + class SoftwareImage < ApplicationRecord + belongs_to :software + belongs_to :image, optional: true + + attr_accessor :file_upload + + before_validation :create_image_from_upload, if: -> { file_upload.present? } + + validates :image_id, presence: true + validates :image_id, uniqueness: { scope: :software_id } + validates :position, numericality: { only_integer: true, greater_than_or_equal_to: 0 } + + before_save :unset_other_defaults, if: -> { is_default? && is_default_changed? } + + default_scope { order(:position) } + + def self.ransackable_attributes(auth_object = nil) + %w[created_at id image_id is_default position software_id updated_at] + end + + def self.ransackable_associations(auth_object = nil) + %w[image software] + end + + ActiveSupport.run_load_hooks(:warp_engine_software_image, self) + + private + + def create_image_from_upload + img = Image.new(file_upload: file_upload) + if img.save + self.image = img + else + errors.add(:file_upload, img.errors.full_messages.join(", ")) + end + end + + def unset_other_defaults + SoftwareImage.where(software_id: software_id, is_default: true) + .where.not(id: id) + .update_all(is_default: false) + end + end +end