diff --git a/apps/api/Gemfile.lock b/apps/api/Gemfile.lock index 345412c..8cb05d2 100644 --- a/apps/api/Gemfile.lock +++ b/apps/api/Gemfile.lock @@ -2,7 +2,9 @@ PATH remote: ../libs/ruby/warp_engine specs: warp_engine (0.1.0) + blueprinter rails (>= 8.0) + rubyzip (~> 2.3) GEM remote: https://rubygems.org/ diff --git a/apps/api/app/admin/files.rb b/apps/api/app/admin/files.rb index 75a4814..b88ae08 100644 --- a/apps/api/app/admin/files.rb +++ b/apps/api/app/admin/files.rb @@ -2,7 +2,7 @@ ActiveAdmin.register_page "Files" do menu priority: 6, label: "📁 Files" content do - service = FileManagerService.new + service = WarpEngine::FileManagerService.new current_dir = params[:dir].to_s.presence || "" picker_mode = params[:picker].present? picker_field = params[:field].to_s @@ -199,7 +199,7 @@ ActiveAdmin.register_page "Files" do # JSON API for AJAX file picker page_action :list, method: :get do - service = FileManagerService.new + service = WarpEngine::FileManagerService.new dir = params[:dir].to_s.presence || "" begin entries = service.list(dir) @@ -210,7 +210,7 @@ ActiveAdmin.register_page "Files" do end page_action :ajax_upload, method: :post do - service = FileManagerService.new + service = WarpEngine::FileManagerService.new path = service.upload(params[:dir].to_s, params[:file]) render json: { ok: true, path: path } rescue => e @@ -218,7 +218,7 @@ ActiveAdmin.register_page "Files" do end page_action :upload, method: :post do - service = FileManagerService.new + service = WarpEngine::FileManagerService.new service.upload(params[:dir].to_s, params[:file]) redirect_to admin_files_path(dir: params[:dir], picker: params[:picker], field: params[:field]), notice: "File uploaded." rescue => e @@ -226,7 +226,7 @@ ActiveAdmin.register_page "Files" do end page_action :mkdir, method: :post do - service = FileManagerService.new + service = WarpEngine::FileManagerService.new service.mkdir(params[:dir].to_s, params[:name].to_s) redirect_to admin_files_path(dir: params[:dir], picker: params[:picker], field: params[:field]), notice: "Folder created." rescue => e @@ -234,7 +234,7 @@ ActiveAdmin.register_page "Files" do end page_action :rename, method: :post do - service = FileManagerService.new + service = WarpEngine::FileManagerService.new service.rename(params[:path].to_s, params[:new_name].to_s) redirect_to admin_files_path(dir: params[:dir], picker: params[:picker], field: params[:field]), notice: "Renamed." rescue => e @@ -242,7 +242,7 @@ ActiveAdmin.register_page "Files" do end page_action :delete, method: :delete do - service = FileManagerService.new + service = WarpEngine::FileManagerService.new dir = params[:dir].to_s.presence || begin d = File.dirname(params[:path].to_s) d == "." ? "" : d diff --git a/apps/api/app/controllers/api/builds_controller.rb b/apps/api/app/controllers/api/builds_controller.rb index f81ddb2..b19ded8 100644 --- a/apps/api/app/controllers/api/builds_controller.rb +++ b/apps/api/app/controllers/api/builds_controller.rb @@ -1,5 +1,5 @@ class Api::BuildsController < ApiController def index - render json: BuildsService.new.index + render json: WarpEngine::BuildsService.new.index end end diff --git a/apps/api/app/controllers/api/downloads_controller.rb b/apps/api/app/controllers/api/downloads_controller.rb index 73464cc..b7d61d0 100644 --- a/apps/api/app/controllers/api/downloads_controller.rb +++ b/apps/api/app/controllers/api/downloads_controller.rb @@ -13,7 +13,7 @@ class Api::DownloadsController < ApiController path = params[:path] return render(json: { error: "Path is required" }, status: :bad_request) if path.blank? - full_path = DownloadService.new.create( + full_path = WarpEngine::DownloadService.new.create( path: path, ip: request.remote_ip, user_agent: request.user_agent, diff --git a/apps/api/app/controllers/api/images_controller.rb b/apps/api/app/controllers/api/images_controller.rb index bc45bdb..39a84b8 100644 --- a/apps/api/app/controllers/api/images_controller.rb +++ b/apps/api/app/controllers/api/images_controller.rb @@ -9,7 +9,7 @@ class Api::ImagesController < ApiController returns code: 200, desc: "Image binary data" error code: 404, desc: "Image not found" def show - image = ImageService.new.show(ImageShowInputDto.new(id: params[:id])) + image = WarpEngine::ImageService.new.show(WarpEngine::ImageShowInputDto.new(id: params[:id])) send_file image.file_path, type: image.content_type, disposition: "inline" end end diff --git a/apps/api/app/controllers/api/software_builds_controller.rb b/apps/api/app/controllers/api/software_builds_controller.rb index 77a696a..f92abbe 100644 --- a/apps/api/app/controllers/api/software_builds_controller.rb +++ b/apps/api/app/controllers/api/software_builds_controller.rb @@ -1,5 +1,5 @@ class Api::SoftwareBuildsController < ApiController def show - render json: BuildsService.new.show(params[:name]) + render json: WarpEngine::BuildsService.new.show(params[:name]) end end diff --git a/apps/api/app/controllers/api/software_controller.rb b/apps/api/app/controllers/api/software_controller.rb index 20c5ebe..c64e696 100644 --- a/apps/api/app/controllers/api/software_controller.rb +++ b/apps/api/app/controllers/api/software_controller.rb @@ -54,6 +54,6 @@ class Api::SoftwareController < ApiController end end def index - render json: SoftwareService.new.index + render json: WarpEngine::SoftwareService.new.index end end diff --git a/apps/api/app/controllers/api/software_highlighted_controller.rb b/apps/api/app/controllers/api/software_highlighted_controller.rb index 59ae8f3..5516670 100644 --- a/apps/api/app/controllers/api/software_highlighted_controller.rb +++ b/apps/api/app/controllers/api/software_highlighted_controller.rb @@ -33,7 +33,7 @@ class Api::SoftwareHighlightedController < ApiController end error code: 404, desc: "No highlighted software found" def index - result = SoftwareHighlightedService.new.index + result = WarpEngine::SoftwareHighlightedService.new.index if result render json: result else diff --git a/apps/api/app/controllers/files_controller.rb b/apps/api/app/controllers/files_controller.rb index 841760d..11f01e9 100644 --- a/apps/api/app/controllers/files_controller.rb +++ b/apps/api/app/controllers/files_controller.rb @@ -10,7 +10,7 @@ class FilesController < ApiController returns code: 301, desc: "Redirect to file URL" error code: 404, desc: "File not found" def show - result = FileService.new.show(FileShowInputDto.new(path: params[:path])) + result = WarpEngine::FileService.new.show(WarpEngine::FileShowInputDto.new(path: params[:path])) case result.type when :redirect then redirect_to result.url, status: :moved_permanently when :file then send_file result.path, disposition: "inline", type: resolve_mime(result.path) diff --git a/apps/api/app/controllers/update_controller.rb b/apps/api/app/controllers/update_controller.rb index b6afbb5..985ffec 100644 --- a/apps/api/app/controllers/update_controller.rb +++ b/apps/api/app/controllers/update_controller.rb @@ -26,13 +26,13 @@ class UpdateController < ApiController return render plain: "Unauthorized", status: :unauthorized unless authorized? return render plain: "Version not provided", status: :bad_request if params[:version].blank? - input = UpdateInputDto.new( + input = WarpEngine::UpdateInputDto.new( platform: params[:platform], name: params[:name], version: params[:version] ) - UpdateService.new.update(input) + WarpEngine::UpdateService.new.update(input) render plain: "Updated" end diff --git a/apps/api/app/dtos/file_result_dto.rb b/apps/api/app/dtos/file_result_dto.rb deleted file mode 100644 index 1809e60..0000000 --- a/apps/api/app/dtos/file_result_dto.rb +++ /dev/null @@ -1,13 +0,0 @@ -class FileResultDto - attr_reader :type, :path, :url - - def initialize(type:, path: nil, url: nil) - @type = type - @path = path - @url = url - end - - def self.file(path) = new(type: :file, path: path) - def self.redirect(url) = new(type: :redirect, url: url) - def self.not_found = new(type: :not_found) -end diff --git a/apps/api/app/dtos/file_show_input_dto.rb b/apps/api/app/dtos/file_show_input_dto.rb deleted file mode 100644 index ac27669..0000000 --- a/apps/api/app/dtos/file_show_input_dto.rb +++ /dev/null @@ -1 +0,0 @@ -FileShowInputDto = Struct.new(:path, keyword_init: true) diff --git a/apps/api/app/dtos/image_show_input_dto.rb b/apps/api/app/dtos/image_show_input_dto.rb deleted file mode 100644 index df74c61..0000000 --- a/apps/api/app/dtos/image_show_input_dto.rb +++ /dev/null @@ -1 +0,0 @@ -ImageShowInputDto = Struct.new(:id, keyword_init: true) diff --git a/apps/api/app/dtos/update_input_dto.rb b/apps/api/app/dtos/update_input_dto.rb deleted file mode 100644 index 8fc7f7c..0000000 --- a/apps/api/app/dtos/update_input_dto.rb +++ /dev/null @@ -1,5 +0,0 @@ -UpdateInputDto = Struct.new(:platform, :name, :version, keyword_init: true) do - def initialize(platform:, name:, version: nil) - super - end -end diff --git a/apps/api/app/serializers/concerns/timestamp_fields.rb b/apps/api/app/serializers/concerns/timestamp_fields.rb deleted file mode 100644 index e675a6f..0000000 --- a/apps/api/app/serializers/concerns/timestamp_fields.rb +++ /dev/null @@ -1,4 +0,0 @@ -module TimestampFields - GO_ZERO_TIME = "0001-01-01T00:00:00Z" - TS_FORMAT = "%Y-%m-%dT%H:%M:%S.%3NZ" -end diff --git a/apps/api/app/serializers/event_serializer.rb b/apps/api/app/serializers/event_serializer.rb index 2a1c986..c5a65de 100644 --- a/apps/api/app/serializers/event_serializer.rb +++ b/apps/api/app/serializers/event_serializer.rb @@ -1,5 +1,5 @@ class EventSerializer < Blueprinter::Base - include TimestampFields + include WarpEngine::TimestampFields field :name field(:date) { |event| event.date.utc.strftime(TS_FORMAT) } diff --git a/apps/api/app/serializers/external_link_serializer.rb b/apps/api/app/serializers/external_link_serializer.rb deleted file mode 100644 index a7e5f8a..0000000 --- a/apps/api/app/serializers/external_link_serializer.rb +++ /dev/null @@ -1,11 +0,0 @@ -class ExternalLinkSerializer < Blueprinter::Base - include TimestampFields - - field(:id) { |el| el.id } - field(:createdAt) { |el| el.created_at&.utc&.strftime(TS_FORMAT) || GO_ZERO_TIME } - field(:updatedAt) { |el| el.updated_at&.utc&.strftime(TS_FORMAT) || GO_ZERO_TIME } - field(:deletedAt) { |el| el.deleted_at&.utc&.strftime(TS_FORMAT) } - field(:softwareId) { |el| el.software_id } - field :label - field :url -end diff --git a/apps/api/app/serializers/platform_link_serializer.rb b/apps/api/app/serializers/platform_link_serializer.rb deleted file mode 100644 index 7ba962c..0000000 --- a/apps/api/app/serializers/platform_link_serializer.rb +++ /dev/null @@ -1,4 +0,0 @@ -class PlatformLinkSerializer < Blueprinter::Base - field :name - field :url -end diff --git a/apps/api/app/serializers/release_serializer.rb b/apps/api/app/serializers/release_serializer.rb deleted file mode 100644 index 80d8e7a..0000000 --- a/apps/api/app/serializers/release_serializer.rb +++ /dev/null @@ -1,33 +0,0 @@ -class ReleaseSerializer < Blueprinter::Base - include TimestampFields - - FILE_PATH_FROM = "/softwares/" - FILE_PATH_TO = "/file/" - - def self.assets_of(release) - release.association(:release_assets).loaded? ? release.release_assets : release.release_assets.to_a - end - - def self.asset_path(release, kind) - asset = assets_of(release).find { |a| a.kind == kind } - asset ? asset.path.gsub(FILE_PATH_FROM, FILE_PATH_TO) : "" - end - - field(:id) { |r| r.id } - field(:createdAt) { |r| r.created_at&.utc&.strftime(TS_FORMAT) || GO_ZERO_TIME } - field(:updatedAt) { |r| r.updated_at&.utc&.strftime(TS_FORMAT) || GO_ZERO_TIME } - field(:deletedAt) { |r| r.deleted_at&.utc&.strftime(TS_FORMAT) } - field(:softwareId) { |r| r.software_id } - field :version - field(:cartridgePath) { |r| asset_path(r, "cartridge") } - field(:sourcePath) { |r| asset_path(r, "source") } - field(:htmlFolderPath) { |r| asset_path(r, "html") } - field(:docsFolderPath) { |r| asset_path(r, "docs") } - field(:assets) do |r| - assets_of(r).map { |a| { kind: a.kind, path: a.path.gsub(FILE_PATH_FROM, FILE_PATH_TO) } } - end - field(:downloadCount) do |r, opts| - counts = opts[:download_counts] - counts ? counts.fetch(r.id, 0) : r.downloads.count - end -end diff --git a/apps/api/app/serializers/software_detail_serializer.rb b/apps/api/app/serializers/software_detail_serializer.rb deleted file mode 100644 index 727e34d..0000000 --- a/apps/api/app/serializers/software_detail_serializer.rb +++ /dev/null @@ -1,7 +0,0 @@ -class SoftwareDetailSerializer < Blueprinter::Base - field(:software) { |sw, _| SoftwareSerializer.render_as_hash(sw) } - field(:releases) { |_, opts| ReleaseSerializer.render_as_hash(opts[:releases], download_counts: opts[:download_counts]) } - field(:latestRelease) { |_, opts| opts[:latest] ? ReleaseSerializer.render_as_hash(opts[:latest], download_counts: opts[:download_counts]) : nil } - field(:webPlayableRelease) { |_, opts| opts[:web_playable] ? ReleaseSerializer.render_as_hash(opts[:web_playable], download_counts: opts[:download_counts]) : nil } - field(:totalDownloads) { |_, opts| opts[:total_downloads] || 0 } -end diff --git a/apps/api/app/serializers/software_serializer.rb b/apps/api/app/serializers/software_serializer.rb deleted file mode 100644 index a145f7d..0000000 --- a/apps/api/app/serializers/software_serializer.rb +++ /dev/null @@ -1,28 +0,0 @@ -class SoftwareSerializer < Blueprinter::Base - include TimestampFields - - field(:id) { |sw| sw.id } - field(:createdAt) { |sw| sw.created_at&.utc&.strftime(TS_FORMAT) || GO_ZERO_TIME } - field(:updatedAt) { |sw| sw.updated_at&.utc&.strftime(TS_FORMAT) || GO_ZERO_TIME } - field(:deletedAt) { |sw| sw.deleted_at&.utc&.strftime(TS_FORMAT) } - field :name - field :title - field :author - field(:desc) { |sw| sw.desc.to_s } - field(:story) { |sw| sw.story.to_s } - field(:license) { |sw| sw.license.to_s } - field :platform - 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(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 - } - field(:images) { |sw| - sw.software_images.map { |si| - { url: "/api/image/#{si.image_id}", isDefault: si.is_default?, position: si.position } - } - } -end diff --git a/apps/api/app/services/builds_service.rb b/apps/api/app/services/builds_service.rb deleted file mode 100644 index 2e0e3ee..0000000 --- a/apps/api/app/services/builds_service.rb +++ /dev/null @@ -1,37 +0,0 @@ -class BuildsService - def index - platforms = WarpEngine::PlatformLink::SUPPORTED_PLATFORMS.each_with_object({}) do |platform, hash| - service_class = "SoftwareUpdater::#{platform.camelize}Service".constantize - hash[platform] = { - label: service_class.label, - kinds: service_class.expected_kinds - } - end - - all_kinds = WarpEngine::ReleaseAsset::KINDS - - { platforms: platforms, allKinds: all_kinds } - end - - def show(name) - software = WarpEngine::Software.find_by!(name: name) - service_class = "SoftwareUpdater::#{software.platform.camelize}Service".constantize - expected = service_class.expected_kinds - - releases = software.releases.includes(:release_assets).order(updated_at: :desc) - - release_data = releases.each_with_object({}) do |release, hash| - actual = release.release_assets.map(&:kind) - hash[release.version] = { - actual: actual, - missing: expected - actual - } - end - - { - platform: software.platform, - expected: expected, - releases: release_data - } - end -end diff --git a/apps/api/app/services/concerns/software_updater/archive_extraction.rb b/apps/api/app/services/concerns/software_updater/archive_extraction.rb deleted file mode 100644 index 1d2d874..0000000 --- a/apps/api/app/services/concerns/software_updater/archive_extraction.rb +++ /dev/null @@ -1,45 +0,0 @@ -require "zip" -require "fileutils" - -module SoftwareUpdater - module ArchiveExtraction - private - - def base_path - @base_path ||= ENV.fetch("FILE_CONTAINER_PATH", "/softwares") - end - - def full_path(filename) - File.join(base_path, filename) - end - - def dir_exists?(dirname) - File.directory?(full_path(dirname)) - end - - def delete_dir(dirname) - FileUtils.rm_rf(full_path(dirname)) - end - - def create_dir(dirname) - FileUtils.mkdir_p(full_path(dirname)) - end - - def unzip_file(zip_filename, dest_dirname) - dest = full_path(dest_dirname) - Zip::File.open(full_path(zip_filename)) do |zip| - zip.each do |entry| - entry_dest = File.join(dest, entry.name) - FileUtils.mkdir_p(File.dirname(entry_dest)) - entry.extract(entry_dest) { true } - end - end - end - - def extract_zip_to_dir(zip_file, dir_name) - delete_dir(dir_name) if dir_exists?(dir_name) - create_dir(dir_name) - unzip_file(zip_file, dir_name) - end - end -end diff --git a/apps/api/app/services/concerns/software_updater/builds/build_cartridge.rb b/apps/api/app/services/concerns/software_updater/builds/build_cartridge.rb deleted file mode 100644 index fcea2a8..0000000 --- a/apps/api/app/services/concerns/software_updater/builds/build_cartridge.rb +++ /dev/null @@ -1,22 +0,0 @@ -module SoftwareUpdater - module Builds - module BuildCartridge - extend ActiveSupport::Concern - - included do - register_expected_kind "cartridge" - end - - def cartridge_ext - raise NotImplementedError, "#{self.class} must implement #cartridge_ext" - end - - private - - def cartridge_asset_path(versioned) - path = full_path("#{versioned}#{cartridge_ext}") - { "cartridge" => path } if File.file?(path) - end - end - end -end diff --git a/apps/api/app/services/concerns/software_updater/builds/build_docs.rb b/apps/api/app/services/concerns/software_updater/builds/build_docs.rb deleted file mode 100644 index 5a6f63b..0000000 --- a/apps/api/app/services/concerns/software_updater/builds/build_docs.rb +++ /dev/null @@ -1,24 +0,0 @@ -module SoftwareUpdater - module Builds - module BuildDocs - extend ActiveSupport::Concern - - included do - register_expected_kind "docs" - register_prepare_step :prepare_docs - end - - private - - def prepare_docs(versioned) - docs_zip = "#{versioned}-docs.zip" - extract_zip_to_dir(docs_zip, "#{versioned}-docs") if File.file?(full_path(docs_zip)) - end - - def docs_asset_path(versioned) - docs_dir = "#{versioned}-docs" - { "docs" => full_path(docs_dir) } if dir_exists?(docs_dir) - end - end - end -end diff --git a/apps/api/app/services/concerns/software_updater/builds/build_linux_x64.rb b/apps/api/app/services/concerns/software_updater/builds/build_linux_x64.rb deleted file mode 100644 index 0f6054d..0000000 --- a/apps/api/app/services/concerns/software_updater/builds/build_linux_x64.rb +++ /dev/null @@ -1,18 +0,0 @@ -module SoftwareUpdater - module Builds - module BuildLinuxX64 - extend ActiveSupport::Concern - - included do - register_expected_kind "linux_x64" - end - - private - - def linux_x64_asset_path(versioned) - path = full_path("#{versioned}-linux-x64.zip") - { "linux_x64" => path } if File.file?(path) - end - end - end -end diff --git a/apps/api/app/services/concerns/software_updater/builds/build_mac_arm64.rb b/apps/api/app/services/concerns/software_updater/builds/build_mac_arm64.rb deleted file mode 100644 index 49cccd2..0000000 --- a/apps/api/app/services/concerns/software_updater/builds/build_mac_arm64.rb +++ /dev/null @@ -1,18 +0,0 @@ -module SoftwareUpdater - module Builds - module BuildMacArm64 - extend ActiveSupport::Concern - - included do - register_expected_kind "mac_arm64" - end - - private - - def mac_arm64_asset_path(versioned) - path = full_path("#{versioned}-mac-arm64.zip") - { "mac_arm64" => path } if File.file?(path) - end - end - end -end diff --git a/apps/api/app/services/concerns/software_updater/builds/build_mac_universal.rb b/apps/api/app/services/concerns/software_updater/builds/build_mac_universal.rb deleted file mode 100644 index a038b65..0000000 --- a/apps/api/app/services/concerns/software_updater/builds/build_mac_universal.rb +++ /dev/null @@ -1,18 +0,0 @@ -module SoftwareUpdater - module Builds - module BuildMacUniversal - extend ActiveSupport::Concern - - included do - register_expected_kind "mac_universal" - end - - private - - def mac_universal_asset_path(versioned) - path = full_path("#{versioned}-mac-universal.zip") - { "mac_universal" => path } if File.file?(path) - end - end - end -end diff --git a/apps/api/app/services/concerns/software_updater/builds/build_mac_x64.rb b/apps/api/app/services/concerns/software_updater/builds/build_mac_x64.rb deleted file mode 100644 index cea3027..0000000 --- a/apps/api/app/services/concerns/software_updater/builds/build_mac_x64.rb +++ /dev/null @@ -1,18 +0,0 @@ -module SoftwareUpdater - module Builds - module BuildMacX64 - extend ActiveSupport::Concern - - included do - register_expected_kind "mac_x64" - end - - private - - def mac_x64_asset_path(versioned) - path = full_path("#{versioned}-mac-x64.zip") - { "mac_x64" => path } if File.file?(path) - end - end - end -end diff --git a/apps/api/app/services/concerns/software_updater/builds/build_source.rb b/apps/api/app/services/concerns/software_updater/builds/build_source.rb deleted file mode 100644 index 832e4a3..0000000 --- a/apps/api/app/services/concerns/software_updater/builds/build_source.rb +++ /dev/null @@ -1,22 +0,0 @@ -module SoftwareUpdater - module Builds - module BuildSource - extend ActiveSupport::Concern - - included do - register_expected_kind "source" - end - - def source_ext - raise NotImplementedError, "#{self.class} must implement #source_ext" - end - - private - - def source_asset_path(versioned) - path = full_path("#{versioned}#{source_ext}") - { "source" => path } if File.file?(path) - end - end - end -end diff --git a/apps/api/app/services/concerns/software_updater/builds/build_web.rb b/apps/api/app/services/concerns/software_updater/builds/build_web.rb deleted file mode 100644 index 89eeb9a..0000000 --- a/apps/api/app/services/concerns/software_updater/builds/build_web.rb +++ /dev/null @@ -1,23 +0,0 @@ -module SoftwareUpdater - module Builds - module BuildWeb - extend ActiveSupport::Concern - - included do - register_expected_kind "html" - register_prepare_step :prepare_web - end - - private - - def prepare_web(versioned) - extract_zip_to_dir("#{versioned}.html.zip", versioned) - end - - def html_asset_path(versioned) - path = full_path(versioned) - { "html" => path } if dir_exists?(versioned) - end - end - end -end diff --git a/apps/api/app/services/concerns/software_updater/builds/build_win_x64.rb b/apps/api/app/services/concerns/software_updater/builds/build_win_x64.rb deleted file mode 100644 index fbbef2c..0000000 --- a/apps/api/app/services/concerns/software_updater/builds/build_win_x64.rb +++ /dev/null @@ -1,18 +0,0 @@ -module SoftwareUpdater - module Builds - module BuildWinX64 - extend ActiveSupport::Concern - - included do - register_expected_kind "win_x64" - end - - private - - def win_x64_asset_path(versioned) - path = full_path("#{versioned}-win-x64.zip") - { "win_x64" => path } if File.file?(path) - end - end - end -end diff --git a/apps/api/app/services/concerns/software_updater/builds/build_win_x86.rb b/apps/api/app/services/concerns/software_updater/builds/build_win_x86.rb deleted file mode 100644 index 777882d..0000000 --- a/apps/api/app/services/concerns/software_updater/builds/build_win_x86.rb +++ /dev/null @@ -1,18 +0,0 @@ -module SoftwareUpdater - module Builds - module BuildWinX86 - extend ActiveSupport::Concern - - included do - register_expected_kind "win_x86" - end - - private - - def win_x86_asset_path(versioned) - path = full_path("#{versioned}-win-x86.zip") - { "win_x86" => path } if File.file?(path) - end - end - end -end diff --git a/apps/api/app/services/concerns/software_updater/metadata_parsing.rb b/apps/api/app/services/concerns/software_updater/metadata_parsing.rb deleted file mode 100644 index a8cd0da..0000000 --- a/apps/api/app/services/concerns/software_updater/metadata_parsing.rb +++ /dev/null @@ -1,14 +0,0 @@ -require "json" - -module SoftwareUpdater - module MetadataParsing - METADATA_KEYS = %i[name title author desc site repo license].freeze - - private - - def parse_json_metadata(path) - raw = JSON.parse(File.read(path), symbolize_names: true) - raw.slice(*METADATA_KEYS) - end - end -end diff --git a/apps/api/app/services/concerns/software_updater/software_persistence.rb b/apps/api/app/services/concerns/software_updater/software_persistence.rb deleted file mode 100644 index c36c997..0000000 --- a/apps/api/app/services/concerns/software_updater/software_persistence.rb +++ /dev/null @@ -1,36 +0,0 @@ -module SoftwareUpdater - module SoftwarePersistence - private - - def update_or_create_software(attrs) - software = WarpEngine::Software.unscoped.find_or_initialize_by(name: attrs[:name]) - software.assign_attributes(attrs.except(:name)) - software.deleted_at = nil - software.save! - software - end - - def upsert_external_link(software_id, label, url) - 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 = WarpEngine::Release.unscoped.find_by(software_id: attrs[:software_id], version: attrs[:version]) - return existing if existing - - WarpEngine::Release.create!(attrs) - end - - def sync_release_assets(release, kind_paths) - kind_paths.each do |kind, path| - asset = WarpEngine::ReleaseAsset.unscoped.find_or_initialize_by(release_id: release.id, kind: kind) - asset.path = path - asset.deleted_at = nil - asset.save! - end - end - end -end diff --git a/apps/api/app/services/concerns/software_updater/updatable.rb b/apps/api/app/services/concerns/software_updater/updatable.rb deleted file mode 100644 index d9d7e55..0000000 --- a/apps/api/app/services/concerns/software_updater/updatable.rb +++ /dev/null @@ -1,72 +0,0 @@ -module SoftwareUpdater - module Updatable - extend ActiveSupport::Concern - include ArchiveExtraction - include MetadataParsing - include SoftwarePersistence - - class_methods do - def platform(value = nil) - @platform = value if value - @platform ||= name.demodulize.delete_suffix("Service").downcase - end - - def label(value = nil) - @label = value if value - @label ||= platform.titleize - end - - def expected_kinds - @expected_kinds ||= [] - end - - def register_expected_kind(kind) - expected_kinds << kind unless expected_kinds.include?(kind) - end - - def prepare_steps - @prepare_steps ||= [] - end - - def register_prepare_step(method_name) - prepare_steps << method_name unless prepare_steps.include?(method_name) - end - end - - def update(name, version) - versioned = "#{name}-#{version}" - prepare_files(versioned) - - metadata = parse_metadata(versioned) - site_url = metadata.delete(:site) - repo_url = metadata.delete(:repo) - - ActiveRecord::Base.transaction do - software = update_or_create_software(metadata.merge(platform: self.class.platform)) - upsert_external_link(software.id, "Source Code", site_url) if site_url.present? - upsert_external_link(software.id, "Repository", repo_url) if repo_url.present? - - release = create_release_if_not_exists(software_id: software.id, version: version) - sync_release_assets(release, collect_asset_paths(versioned)) - release - end - end - - private - - def prepare_files(versioned) - self.class.prepare_steps.each { |step| send(step, versioned) } - end - - def parse_metadata(versioned) - parse_json_metadata(full_path("#{versioned}.metadata.json")) - end - - def collect_asset_paths(versioned) - self.class.expected_kinds.each_with_object({}) do |kind, hash| - result = send(:"#{kind}_asset_path", versioned) - hash.merge!(result) if result - end - end - end -end diff --git a/apps/api/app/services/download_service.rb b/apps/api/app/services/download_service.rb deleted file mode 100644 index bdfcb7f..0000000 --- a/apps/api/app/services/download_service.rb +++ /dev/null @@ -1,32 +0,0 @@ -class DownloadService - def self.container_base - ENV.fetch("FILE_CONTAINER_PATH", "/softwares") - end - - # Lazy: a container path csak az első használatkor kötelező, boot/teszt közben nem. - def self.base_path - Pathname.new(container_base).realpath - end - - def create(path:, ip:, user_agent:, referer:) - sanitized = path.to_s - base_path = self.class.base_path - full_path = base_path.join(sanitized).realpath - return nil unless full_path.to_s.start_with?(base_path.to_s) - return nil unless File.file?(full_path) - - escaped = sanitized.gsub("%", "\\%").gsub("_", "\\_") - asset = WarpEngine::ReleaseAsset.find_by(path: File.join(self.class.container_base, sanitized)) || - WarpEngine::ReleaseAsset.where("path LIKE ?", "%#{escaped}%").first - - WarpEngine::Download.create!( - file_path: sanitized, - release: asset&.release, - ip_address: ip, - user_agent: user_agent&.truncate(500), - referer: referer&.truncate(500) - ) - - full_path - end -end diff --git a/apps/api/app/services/file_manager_service.rb b/apps/api/app/services/file_manager_service.rb deleted file mode 100644 index 97861be..0000000 --- a/apps/api/app/services/file_manager_service.rb +++ /dev/null @@ -1,88 +0,0 @@ -class FileManagerService - BASE_PATH = Pathname.new(ENV.fetch("FILE_CONTAINER_PATH", "/softwares")) - - def list(relative_path = "") - full = safe_path!(relative_path) - raise ArgumentError, "Not a directory" unless full.directory? - - entries = full.children.sort_by { |c| [ c.directory? ? 0 : 1, c.basename.to_s.downcase ] } - entries.map do |child| - stat = child.stat - { - name: child.basename.to_s, - path: child.relative_path_from(BASE_PATH).to_s, - type: child.directory? ? :directory : :file, - size: child.directory? ? nil : stat.size, - mtime: stat.mtime - } - end - end - - MAX_UPLOAD_SIZE = 100 * 1024 * 1024 # 100MB - - def upload(relative_dir, uploaded_file) - raise ArgumentError, "File too large (max 100MB)" if uploaded_file.size > MAX_UPLOAD_SIZE - dir = safe_path!(relative_dir) - raise ArgumentError, "Not a directory" unless dir.directory? - - safe_name = sanitize_name(uploaded_file.original_filename) - target = dir.join(safe_name) - raise ArgumentError, "Path escape" unless target.to_s.start_with?(BASE_PATH.to_s) - - IO.copy_stream(uploaded_file.to_io, target.to_s) - target.relative_path_from(BASE_PATH).to_s - end - - def delete(relative_path) - full = safe_path!(relative_path) - raise ArgumentError, "Cannot delete root" if full == BASE_PATH - - if full.directory? - full.rmdir - else - full.delete - end - end - - def rename(relative_path, new_name) - full = safe_path!(relative_path) - raise ArgumentError, "Cannot rename root" if full == BASE_PATH - - safe_name = sanitize_name(new_name) - new_full = full.parent.join(safe_name) - raise ArgumentError, "Path escape" unless new_full.to_s.start_with?(BASE_PATH.to_s) - - full.rename(new_full) - new_full.relative_path_from(BASE_PATH).to_s - end - - def mkdir(relative_path, folder_name) - parent = safe_path!(relative_path) - raise ArgumentError, "Not a directory" unless parent.directory? - - safe_name = sanitize_name(folder_name) - new_dir = parent.join(safe_name) - raise ArgumentError, "Path escape" unless new_dir.to_s.start_with?(BASE_PATH.to_s) - - new_dir.mkdir - new_dir.relative_path_from(BASE_PATH).to_s - end - - private - - def safe_path!(relative_path) - cleaned = relative_path.to_s.gsub("..", "").squeeze("/").gsub(%r{^/|/$}, "") - full = BASE_PATH.join(cleaned) - resolved = full.exist? ? full.realpath : full.cleanpath - unless resolved.to_s.start_with?(BASE_PATH.to_s) - raise ArgumentError, "Path traversal detected" - end - resolved - end - - def sanitize_name(name) - name.to_s.gsub("..", "").gsub("/", "").gsub("\\", "").strip.tap do |n| - raise ArgumentError, "Invalid name" if n.blank? - end - end -end diff --git a/apps/api/app/services/file_service.rb b/apps/api/app/services/file_service.rb deleted file mode 100644 index 3c60e5e..0000000 --- a/apps/api/app/services/file_service.rb +++ /dev/null @@ -1,33 +0,0 @@ -class FileService - # Lazy: a container path csak az első használatkor kötelező, boot/teszt közben nem. - def self.base_path - Pathname.new(ENV.fetch("FILE_CONTAINER_PATH", "/softwares")).realpath - end - - def show(input) - full_path = base_path.join(input.path.to_s) - return FileResultDto.not_found unless safe_path?(full_path) - - if File.directory?(full_path) - index_path = full_path.join("index.html") - return FileResultDto.not_found unless File.file?(index_path) - return FileResultDto.redirect("/file/#{input.path.to_s.chomp("/")}/index.html") - end - - if File.file?(full_path) - FileResultDto.file(full_path) - else - FileResultDto.not_found - end - end - - private - - def base_path - @base_path ||= self.class.base_path - end - - def safe_path?(path) - File.exist?(path) && Pathname.new(path).realpath.to_s.start_with?(base_path.to_s) - end -end diff --git a/apps/api/app/services/image_service.rb b/apps/api/app/services/image_service.rb deleted file mode 100644 index 2d637c4..0000000 --- a/apps/api/app/services/image_service.rb +++ /dev/null @@ -1,5 +0,0 @@ -class ImageService - def show(input) - WarpEngine::Image.find(input.id) - end -end diff --git a/apps/api/app/services/software_highlighted_service.rb b/apps/api/app/services/software_highlighted_service.rb deleted file mode 100644 index 971de78..0000000 --- a/apps/api/app/services/software_highlighted_service.rb +++ /dev/null @@ -1,14 +0,0 @@ -class SoftwareHighlightedService - include SoftwareResponseBuilder - - def index - software = WarpEngine::Software.includes(:external_links, :software_images) - .where(highlighted: true) - .order(id: :desc) - .first - return nil unless software - - 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 deleted file mode 100644 index b7413b4..0000000 --- a/apps/api/app/services/software_response_builder.rb +++ /dev/null @@ -1,25 +0,0 @@ -module SoftwareResponseBuilder - private - - def build_response(software, releases, download_counts) - sorted = releases.sort_by { |r| r.created_at || Time.at(0) }.reverse - latest = sorted.reject { |r| r.version.to_s.start_with?("dev-") }.first - # web-playable, ha az utolsó (stabil) release-nek van webes assetje - web_playable = latest if latest&.release_assets&.any? { |a| a.kind == "html" } - total_downloads = releases.sum { |r| download_counts.fetch(r.id, 0) } - - SoftwareDetailSerializer.render_as_hash(software, - releases: sorted, - latest: latest, - web_playable: web_playable, - total_downloads: total_downloads, - download_counts: download_counts - ) - end - - # 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? - 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 deleted file mode 100644 index 3141b5f..0000000 --- a/apps/api/app/services/software_service.rb +++ /dev/null @@ -1,9 +0,0 @@ -class SoftwareService - include SoftwareResponseBuilder - - def index - 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 -end diff --git a/apps/api/app/services/software_updater/bevy_service.rb b/apps/api/app/services/software_updater/bevy_service.rb deleted file mode 100644 index 30f8a38..0000000 --- a/apps/api/app/services/software_updater/bevy_service.rb +++ /dev/null @@ -1,10 +0,0 @@ -module SoftwareUpdater - class BevyService - include Updatable - include Builds::BuildWeb - include Builds::BuildWinX64 - include Builds::BuildLinuxX64 - - label "Bevy" - end -end diff --git a/apps/api/app/services/software_updater/c64_service.rb b/apps/api/app/services/software_updater/c64_service.rb deleted file mode 100644 index 3032693..0000000 --- a/apps/api/app/services/software_updater/c64_service.rb +++ /dev/null @@ -1,10 +0,0 @@ -module SoftwareUpdater - class C64Service - include Updatable - include Builds::BuildCartridge - - label "C64" - - def cartridge_ext = ".prg" - end -end diff --git a/apps/api/app/services/software_updater/ebitengine_service.rb b/apps/api/app/services/software_updater/ebitengine_service.rb deleted file mode 100644 index 2f44406..0000000 --- a/apps/api/app/services/software_updater/ebitengine_service.rb +++ /dev/null @@ -1,13 +0,0 @@ -module SoftwareUpdater - class EbitengineService - include Updatable - include Builds::BuildWeb - include Builds::BuildWinX86 - include Builds::BuildWinX64 - include Builds::BuildLinuxX64 - include Builds::BuildMacX64 - include Builds::BuildMacArm64 - - label "Ebitengine" - end -end diff --git a/apps/api/app/services/software_updater/godot_service.rb b/apps/api/app/services/software_updater/godot_service.rb deleted file mode 100644 index 6e292c2..0000000 --- a/apps/api/app/services/software_updater/godot_service.rb +++ /dev/null @@ -1,12 +0,0 @@ -module SoftwareUpdater - class GodotService - include Updatable - include Builds::BuildWeb - include Builds::BuildWinX86 - include Builds::BuildWinX64 - include Builds::BuildLinuxX64 - include Builds::BuildMacUniversal - - label "Godot" - end -end diff --git a/apps/api/app/services/software_updater/love_service.rb b/apps/api/app/services/software_updater/love_service.rb deleted file mode 100644 index d07eb27..0000000 --- a/apps/api/app/services/software_updater/love_service.rb +++ /dev/null @@ -1,11 +0,0 @@ -module SoftwareUpdater - class LoveService - include Updatable - include Builds::BuildWeb - include Builds::BuildWinX64 - include Builds::BuildLinuxX64 - include Builds::BuildMacUniversal - - label "LÖVE" - end -end diff --git a/apps/api/app/services/software_updater/phaser_service.rb b/apps/api/app/services/software_updater/phaser_service.rb deleted file mode 100644 index 407cb91..0000000 --- a/apps/api/app/services/software_updater/phaser_service.rb +++ /dev/null @@ -1,8 +0,0 @@ -module SoftwareUpdater - class PhaserService - include Updatable - include Builds::BuildWeb - - label "Phaser" - end -end diff --git a/apps/api/app/services/software_updater/tic80_service.rb b/apps/api/app/services/software_updater/tic80_service.rb deleted file mode 100644 index 928fe0a..0000000 --- a/apps/api/app/services/software_updater/tic80_service.rb +++ /dev/null @@ -1,36 +0,0 @@ -module SoftwareUpdater - class Tic80Service - include Updatable - include Builds::BuildCartridge - include Builds::BuildSource - include Builds::BuildWeb - include Builds::BuildDocs - include Builds::BuildWinX64 - include Builds::BuildLinuxX64 - include Builds::BuildMacX64 - - label "TIC-80" - - def cartridge_ext = ".tic" - def source_ext = ".lua" - - private - - def parse_metadata(versioned) - parse_lua_metadata(full_path("#{versioned}.lua")) - end - - def parse_lua_metadata(source_path) - metadata = {} - File.foreach(source_path) do |line| - break unless line.start_with?("--") - parts = line[2..].split(":", 2) - next if parts.length != 2 - key = parts[0].strip.downcase.to_sym - value = parts[1].strip - metadata[key] = value - end - metadata.slice(*MetadataParsing::METADATA_KEYS) - end - end -end diff --git a/apps/api/app/services/update_service.rb b/apps/api/app/services/update_service.rb deleted file mode 100644 index a78fc3b..0000000 --- a/apps/api/app/services/update_service.rb +++ /dev/null @@ -1,9 +0,0 @@ -class UpdateService - def update(input) - unless WarpEngine::PlatformLink::SUPPORTED_PLATFORMS.include?(input.platform) - raise ArgumentError, "Unsupported platform: #{input.platform}" - end - - "SoftwareUpdater::#{input.platform.camelize}Service".constantize.new.update(input.name, input.version) - end -end diff --git a/apps/api/spec/rails_helper.rb b/apps/api/spec/rails_helper.rb index 8cd9ec3..2b89d3b 100644 --- a/apps/api/spec/rails_helper.rb +++ b/apps/api/spec/rails_helper.rb @@ -11,6 +11,10 @@ rescue ActiveRecord::PendingMigrationError => e abort e.to_s.strip end +# A katalógus-factory-k (software, release, download, platform_link) az engine-ben élnek +FactoryBot.definition_file_paths << WarpEngine::Engine.root.join("spec/factories") +FactoryBot.reload + RSpec.configure do |config| config.fixture_paths = [ Rails.root.join("spec/fixtures") ] config.use_transactional_fixtures = true diff --git a/apps/api/spec/services/update_service_spec.rb b/apps/api/spec/services/update_service_spec.rb deleted file mode 100644 index af29d4c..0000000 --- a/apps/api/spec/services/update_service_spec.rb +++ /dev/null @@ -1,59 +0,0 @@ -require "rails_helper" - -RSpec.describe UpdateService do - describe "#update" do - it "raises ArgumentError for unsupported platform" do - input = UpdateInputDto.new(platform: "unknown", name: "game", version: "1.0") - - expect { described_class.new.update(input) }.to raise_error(ArgumentError, /Unsupported platform/) - end - - it "routes to correct platform service" do - input = UpdateInputDto.new(platform: "tic80", name: "game", version: "1.0") - mock_service = instance_double(SoftwareUpdater::Tic80Service) - - allow(SoftwareUpdater::Tic80Service).to receive(:new).and_return(mock_service) - allow(mock_service).to receive(:update) - - described_class.new.update(input) - - expect(mock_service).to have_received(:update).with("game", "1.0") - end - - it "routes godot platform to GodotService" do - input = UpdateInputDto.new(platform: "godot", name: "game", version: "1.0") - mock_service = instance_double(SoftwareUpdater::GodotService) - - allow(SoftwareUpdater::GodotService).to receive(:new).and_return(mock_service) - allow(mock_service).to receive(:update) - - described_class.new.update(input) - - expect(mock_service).to have_received(:update).with("game", "1.0") - end - - it "routes bevy platform to BevyService" do - input = UpdateInputDto.new(platform: "bevy", name: "game", version: "1.0") - mock_service = instance_double(SoftwareUpdater::BevyService) - - allow(SoftwareUpdater::BevyService).to receive(:new).and_return(mock_service) - allow(mock_service).to receive(:update) - - described_class.new.update(input) - - expect(mock_service).to have_received(:update).with("game", "1.0") - end - - it "routes phaser platform to PhaserService" do - input = UpdateInputDto.new(platform: "phaser", name: "game", version: "1.0") - mock_service = instance_double(SoftwareUpdater::PhaserService) - - allow(SoftwareUpdater::PhaserService).to receive(:new).and_return(mock_service) - allow(mock_service).to receive(:update) - - described_class.new.update(input) - - expect(mock_service).to have_received(:update).with("game", "1.0") - end - end -end diff --git a/libs/ruby/warp_engine/.gitignore b/libs/ruby/warp_engine/.gitignore new file mode 100644 index 0000000..119a2bf --- /dev/null +++ b/libs/ruby/warp_engine/.gitignore @@ -0,0 +1,4 @@ +log/ +spec/dummy/log/ +spec/dummy/tmp/ +Gemfile.lock diff --git a/libs/ruby/warp_engine/.rspec b/libs/ruby/warp_engine/.rspec new file mode 100644 index 0000000..7a2cc1a --- /dev/null +++ b/libs/ruby/warp_engine/.rspec @@ -0,0 +1,3 @@ +--require spec_helper +--format documentation +--color diff --git a/libs/ruby/warp_engine/Gemfile b/libs/ruby/warp_engine/Gemfile new file mode 100644 index 0000000..c46ae88 --- /dev/null +++ b/libs/ruby/warp_engine/Gemfile @@ -0,0 +1,12 @@ +source "https://rubygems.org" + +gemspec + +gem "mysql2", "~> 0.5" + +group :development, :test do + gem "rspec-rails", "~> 7.0" + gem "factory_bot_rails" + gem "shoulda-matchers", "~> 6.0" + gem "debug", platforms: %i[mri windows] +end diff --git a/libs/ruby/warp_engine/Rakefile b/libs/ruby/warp_engine/Rakefile new file mode 100644 index 0000000..e8de9d4 --- /dev/null +++ b/libs/ruby/warp_engine/Rakefile @@ -0,0 +1,8 @@ +require "bundler/setup" + +APP_RAKEFILE = File.expand_path("spec/dummy/Rakefile", __dir__) +load "rails/tasks/engine.rake" + +require "rspec/core/rake_task" +RSpec::Core::RakeTask.new(:spec) +task default: :spec diff --git a/libs/ruby/warp_engine/app/dtos/warp_engine/file_result_dto.rb b/libs/ruby/warp_engine/app/dtos/warp_engine/file_result_dto.rb new file mode 100644 index 0000000..5cbe69c --- /dev/null +++ b/libs/ruby/warp_engine/app/dtos/warp_engine/file_result_dto.rb @@ -0,0 +1,15 @@ +module WarpEngine + class FileResultDto + attr_reader :type, :path, :url + + def initialize(type:, path: nil, url: nil) + @type = type + @path = path + @url = url + end + + def self.file(path) = new(type: :file, path: path) + def self.redirect(url) = new(type: :redirect, url: url) + def self.not_found = new(type: :not_found) + end +end diff --git a/libs/ruby/warp_engine/app/dtos/warp_engine/file_show_input_dto.rb b/libs/ruby/warp_engine/app/dtos/warp_engine/file_show_input_dto.rb new file mode 100644 index 0000000..e4e6adb --- /dev/null +++ b/libs/ruby/warp_engine/app/dtos/warp_engine/file_show_input_dto.rb @@ -0,0 +1,3 @@ +module WarpEngine + FileShowInputDto = Struct.new(:path, keyword_init: true) +end diff --git a/libs/ruby/warp_engine/app/dtos/warp_engine/image_show_input_dto.rb b/libs/ruby/warp_engine/app/dtos/warp_engine/image_show_input_dto.rb new file mode 100644 index 0000000..65af195 --- /dev/null +++ b/libs/ruby/warp_engine/app/dtos/warp_engine/image_show_input_dto.rb @@ -0,0 +1,3 @@ +module WarpEngine + ImageShowInputDto = Struct.new(:id, keyword_init: true) +end diff --git a/libs/ruby/warp_engine/app/dtos/warp_engine/update_input_dto.rb b/libs/ruby/warp_engine/app/dtos/warp_engine/update_input_dto.rb new file mode 100644 index 0000000..7f64251 --- /dev/null +++ b/libs/ruby/warp_engine/app/dtos/warp_engine/update_input_dto.rb @@ -0,0 +1,7 @@ +module WarpEngine + UpdateInputDto = Struct.new(:platform, :name, :version, keyword_init: true) do + def initialize(platform:, name:, version: nil) + super + end + end +end diff --git a/libs/ruby/warp_engine/app/serializers/concerns/warp_engine/timestamp_fields.rb b/libs/ruby/warp_engine/app/serializers/concerns/warp_engine/timestamp_fields.rb new file mode 100644 index 0000000..0de9ca5 --- /dev/null +++ b/libs/ruby/warp_engine/app/serializers/concerns/warp_engine/timestamp_fields.rb @@ -0,0 +1,6 @@ +module WarpEngine + module TimestampFields + GO_ZERO_TIME = "0001-01-01T00:00:00Z" + TS_FORMAT = "%Y-%m-%dT%H:%M:%S.%3NZ" + end +end diff --git a/libs/ruby/warp_engine/app/serializers/warp_engine/external_link_serializer.rb b/libs/ruby/warp_engine/app/serializers/warp_engine/external_link_serializer.rb new file mode 100644 index 0000000..f60b72c --- /dev/null +++ b/libs/ruby/warp_engine/app/serializers/warp_engine/external_link_serializer.rb @@ -0,0 +1,13 @@ +module WarpEngine + class ExternalLinkSerializer < Blueprinter::Base + include TimestampFields + + field(:id) { |el| el.id } + field(:createdAt) { |el| el.created_at&.utc&.strftime(TS_FORMAT) || GO_ZERO_TIME } + field(:updatedAt) { |el| el.updated_at&.utc&.strftime(TS_FORMAT) || GO_ZERO_TIME } + field(:deletedAt) { |el| el.deleted_at&.utc&.strftime(TS_FORMAT) } + field(:softwareId) { |el| el.software_id } + field :label + field :url + end +end diff --git a/libs/ruby/warp_engine/app/serializers/warp_engine/platform_link_serializer.rb b/libs/ruby/warp_engine/app/serializers/warp_engine/platform_link_serializer.rb new file mode 100644 index 0000000..f6ad273 --- /dev/null +++ b/libs/ruby/warp_engine/app/serializers/warp_engine/platform_link_serializer.rb @@ -0,0 +1,6 @@ +module WarpEngine + class PlatformLinkSerializer < Blueprinter::Base + field :name + field :url + end +end diff --git a/libs/ruby/warp_engine/app/serializers/warp_engine/release_serializer.rb b/libs/ruby/warp_engine/app/serializers/warp_engine/release_serializer.rb new file mode 100644 index 0000000..1ad2d3d --- /dev/null +++ b/libs/ruby/warp_engine/app/serializers/warp_engine/release_serializer.rb @@ -0,0 +1,39 @@ +module WarpEngine + class ReleaseSerializer < Blueprinter::Base + include TimestampFields + + FILE_PATH_TO = "/file/" + + # A lemezen tárolt asset-útvonalak publikus /file/ URL-lé írása configból. + def self.file_path_from + "#{WarpEngine.config.file_container_path.chomp("/")}/" + end + + def self.assets_of(release) + release.association(:release_assets).loaded? ? release.release_assets : release.release_assets.to_a + end + + def self.asset_path(release, kind) + asset = assets_of(release).find { |a| a.kind == kind } + asset ? asset.path.gsub(file_path_from, FILE_PATH_TO) : "" + end + + field(:id) { |r| r.id } + field(:createdAt) { |r| r.created_at&.utc&.strftime(TS_FORMAT) || GO_ZERO_TIME } + field(:updatedAt) { |r| r.updated_at&.utc&.strftime(TS_FORMAT) || GO_ZERO_TIME } + field(:deletedAt) { |r| r.deleted_at&.utc&.strftime(TS_FORMAT) } + field(:softwareId) { |r| r.software_id } + field :version + field(:cartridgePath) { |r| asset_path(r, "cartridge") } + field(:sourcePath) { |r| asset_path(r, "source") } + field(:htmlFolderPath) { |r| asset_path(r, "html") } + field(:docsFolderPath) { |r| asset_path(r, "docs") } + field(:assets) do |r| + assets_of(r).map { |a| { kind: a.kind, path: a.path.gsub(file_path_from, FILE_PATH_TO) } } + end + field(:downloadCount) do |r, opts| + counts = opts[:download_counts] + counts ? counts.fetch(r.id, 0) : r.downloads.count + end + end +end diff --git a/libs/ruby/warp_engine/app/serializers/warp_engine/software_detail_serializer.rb b/libs/ruby/warp_engine/app/serializers/warp_engine/software_detail_serializer.rb new file mode 100644 index 0000000..d553be4 --- /dev/null +++ b/libs/ruby/warp_engine/app/serializers/warp_engine/software_detail_serializer.rb @@ -0,0 +1,9 @@ +module WarpEngine + class SoftwareDetailSerializer < Blueprinter::Base + field(:software) { |sw, _| SoftwareSerializer.render_as_hash(sw) } + field(:releases) { |_, opts| ReleaseSerializer.render_as_hash(opts[:releases], download_counts: opts[:download_counts]) } + field(:latestRelease) { |_, opts| opts[:latest] ? ReleaseSerializer.render_as_hash(opts[:latest], download_counts: opts[:download_counts]) : nil } + field(:webPlayableRelease) { |_, opts| opts[:web_playable] ? ReleaseSerializer.render_as_hash(opts[:web_playable], download_counts: opts[:download_counts]) : nil } + field(:totalDownloads) { |_, opts| opts[:total_downloads] || 0 } + end +end diff --git a/libs/ruby/warp_engine/app/serializers/warp_engine/software_serializer.rb b/libs/ruby/warp_engine/app/serializers/warp_engine/software_serializer.rb new file mode 100644 index 0000000..069a472 --- /dev/null +++ b/libs/ruby/warp_engine/app/serializers/warp_engine/software_serializer.rb @@ -0,0 +1,30 @@ +module WarpEngine + class SoftwareSerializer < Blueprinter::Base + include TimestampFields + + field(:id) { |sw| sw.id } + field(:createdAt) { |sw| sw.created_at&.utc&.strftime(TS_FORMAT) || GO_ZERO_TIME } + field(:updatedAt) { |sw| sw.updated_at&.utc&.strftime(TS_FORMAT) || GO_ZERO_TIME } + field(:deletedAt) { |sw| sw.deleted_at&.utc&.strftime(TS_FORMAT) } + field :name + field :title + field :author + field(:desc) { |sw| sw.desc.to_s } + field(:story) { |sw| sw.story.to_s } + field(:license) { |sw| sw.license.to_s } + field :platform + 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(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 + } + field(:images) { |sw| + sw.software_images.map { |si| + { url: "/api/image/#{si.image_id}", isDefault: si.is_default?, position: si.position } + } + } + end +end diff --git a/libs/ruby/warp_engine/app/services/concerns/warp_engine/software_updater/archive_extraction.rb b/libs/ruby/warp_engine/app/services/concerns/warp_engine/software_updater/archive_extraction.rb new file mode 100644 index 0000000..1c8ec46 --- /dev/null +++ b/libs/ruby/warp_engine/app/services/concerns/warp_engine/software_updater/archive_extraction.rb @@ -0,0 +1,47 @@ +require "zip" +require "fileutils" + +module WarpEngine + module SoftwareUpdater + module ArchiveExtraction + private + + def base_path + @base_path ||= WarpEngine.config.file_container_path + end + + def full_path(filename) + File.join(base_path, filename) + end + + def dir_exists?(dirname) + File.directory?(full_path(dirname)) + end + + def delete_dir(dirname) + FileUtils.rm_rf(full_path(dirname)) + end + + def create_dir(dirname) + FileUtils.mkdir_p(full_path(dirname)) + end + + def unzip_file(zip_filename, dest_dirname) + dest = full_path(dest_dirname) + Zip::File.open(full_path(zip_filename)) do |zip| + zip.each do |entry| + entry_dest = File.join(dest, entry.name) + FileUtils.mkdir_p(File.dirname(entry_dest)) + entry.extract(entry_dest) { true } + end + end + end + + def extract_zip_to_dir(zip_file, dir_name) + delete_dir(dir_name) if dir_exists?(dir_name) + create_dir(dir_name) + unzip_file(zip_file, dir_name) + end + end + end +end diff --git a/libs/ruby/warp_engine/app/services/concerns/warp_engine/software_updater/builds/build_cartridge.rb b/libs/ruby/warp_engine/app/services/concerns/warp_engine/software_updater/builds/build_cartridge.rb new file mode 100644 index 0000000..ff4ada6 --- /dev/null +++ b/libs/ruby/warp_engine/app/services/concerns/warp_engine/software_updater/builds/build_cartridge.rb @@ -0,0 +1,24 @@ +module WarpEngine + module SoftwareUpdater + module Builds + module BuildCartridge + extend ActiveSupport::Concern + + included do + register_expected_kind "cartridge" + end + + def cartridge_ext + raise NotImplementedError, "#{self.class} must implement #cartridge_ext" + end + + private + + def cartridge_asset_path(versioned) + path = full_path("#{versioned}#{cartridge_ext}") + { "cartridge" => path } if File.file?(path) + end + end + end + end +end diff --git a/libs/ruby/warp_engine/app/services/concerns/warp_engine/software_updater/builds/build_docs.rb b/libs/ruby/warp_engine/app/services/concerns/warp_engine/software_updater/builds/build_docs.rb new file mode 100644 index 0000000..a9f42eb --- /dev/null +++ b/libs/ruby/warp_engine/app/services/concerns/warp_engine/software_updater/builds/build_docs.rb @@ -0,0 +1,26 @@ +module WarpEngine + module SoftwareUpdater + module Builds + module BuildDocs + extend ActiveSupport::Concern + + included do + register_expected_kind "docs" + register_prepare_step :prepare_docs + end + + private + + def prepare_docs(versioned) + docs_zip = "#{versioned}-docs.zip" + extract_zip_to_dir(docs_zip, "#{versioned}-docs") if File.file?(full_path(docs_zip)) + end + + def docs_asset_path(versioned) + docs_dir = "#{versioned}-docs" + { "docs" => full_path(docs_dir) } if dir_exists?(docs_dir) + end + end + end + end +end diff --git a/libs/ruby/warp_engine/app/services/concerns/warp_engine/software_updater/builds/build_linux_x64.rb b/libs/ruby/warp_engine/app/services/concerns/warp_engine/software_updater/builds/build_linux_x64.rb new file mode 100644 index 0000000..f53406b --- /dev/null +++ b/libs/ruby/warp_engine/app/services/concerns/warp_engine/software_updater/builds/build_linux_x64.rb @@ -0,0 +1,20 @@ +module WarpEngine + module SoftwareUpdater + module Builds + module BuildLinuxX64 + extend ActiveSupport::Concern + + included do + register_expected_kind "linux_x64" + end + + private + + def linux_x64_asset_path(versioned) + path = full_path("#{versioned}-linux-x64.zip") + { "linux_x64" => path } if File.file?(path) + end + end + end + end +end diff --git a/libs/ruby/warp_engine/app/services/concerns/warp_engine/software_updater/builds/build_mac_arm64.rb b/libs/ruby/warp_engine/app/services/concerns/warp_engine/software_updater/builds/build_mac_arm64.rb new file mode 100644 index 0000000..17d3c55 --- /dev/null +++ b/libs/ruby/warp_engine/app/services/concerns/warp_engine/software_updater/builds/build_mac_arm64.rb @@ -0,0 +1,20 @@ +module WarpEngine + module SoftwareUpdater + module Builds + module BuildMacArm64 + extend ActiveSupport::Concern + + included do + register_expected_kind "mac_arm64" + end + + private + + def mac_arm64_asset_path(versioned) + path = full_path("#{versioned}-mac-arm64.zip") + { "mac_arm64" => path } if File.file?(path) + end + end + end + end +end diff --git a/libs/ruby/warp_engine/app/services/concerns/warp_engine/software_updater/builds/build_mac_universal.rb b/libs/ruby/warp_engine/app/services/concerns/warp_engine/software_updater/builds/build_mac_universal.rb new file mode 100644 index 0000000..003022a --- /dev/null +++ b/libs/ruby/warp_engine/app/services/concerns/warp_engine/software_updater/builds/build_mac_universal.rb @@ -0,0 +1,20 @@ +module WarpEngine + module SoftwareUpdater + module Builds + module BuildMacUniversal + extend ActiveSupport::Concern + + included do + register_expected_kind "mac_universal" + end + + private + + def mac_universal_asset_path(versioned) + path = full_path("#{versioned}-mac-universal.zip") + { "mac_universal" => path } if File.file?(path) + end + end + end + end +end diff --git a/libs/ruby/warp_engine/app/services/concerns/warp_engine/software_updater/builds/build_mac_x64.rb b/libs/ruby/warp_engine/app/services/concerns/warp_engine/software_updater/builds/build_mac_x64.rb new file mode 100644 index 0000000..eafa2bb --- /dev/null +++ b/libs/ruby/warp_engine/app/services/concerns/warp_engine/software_updater/builds/build_mac_x64.rb @@ -0,0 +1,20 @@ +module WarpEngine + module SoftwareUpdater + module Builds + module BuildMacX64 + extend ActiveSupport::Concern + + included do + register_expected_kind "mac_x64" + end + + private + + def mac_x64_asset_path(versioned) + path = full_path("#{versioned}-mac-x64.zip") + { "mac_x64" => path } if File.file?(path) + end + end + end + end +end diff --git a/libs/ruby/warp_engine/app/services/concerns/warp_engine/software_updater/builds/build_source.rb b/libs/ruby/warp_engine/app/services/concerns/warp_engine/software_updater/builds/build_source.rb new file mode 100644 index 0000000..04c8b99 --- /dev/null +++ b/libs/ruby/warp_engine/app/services/concerns/warp_engine/software_updater/builds/build_source.rb @@ -0,0 +1,24 @@ +module WarpEngine + module SoftwareUpdater + module Builds + module BuildSource + extend ActiveSupport::Concern + + included do + register_expected_kind "source" + end + + def source_ext + raise NotImplementedError, "#{self.class} must implement #source_ext" + end + + private + + def source_asset_path(versioned) + path = full_path("#{versioned}#{source_ext}") + { "source" => path } if File.file?(path) + end + end + end + end +end diff --git a/libs/ruby/warp_engine/app/services/concerns/warp_engine/software_updater/builds/build_web.rb b/libs/ruby/warp_engine/app/services/concerns/warp_engine/software_updater/builds/build_web.rb new file mode 100644 index 0000000..185851c --- /dev/null +++ b/libs/ruby/warp_engine/app/services/concerns/warp_engine/software_updater/builds/build_web.rb @@ -0,0 +1,25 @@ +module WarpEngine + module SoftwareUpdater + module Builds + module BuildWeb + extend ActiveSupport::Concern + + included do + register_expected_kind "html" + register_prepare_step :prepare_web + end + + private + + def prepare_web(versioned) + extract_zip_to_dir("#{versioned}.html.zip", versioned) + end + + def html_asset_path(versioned) + path = full_path(versioned) + { "html" => path } if dir_exists?(versioned) + end + end + end + end +end diff --git a/libs/ruby/warp_engine/app/services/concerns/warp_engine/software_updater/builds/build_win_x64.rb b/libs/ruby/warp_engine/app/services/concerns/warp_engine/software_updater/builds/build_win_x64.rb new file mode 100644 index 0000000..7ffaeb8 --- /dev/null +++ b/libs/ruby/warp_engine/app/services/concerns/warp_engine/software_updater/builds/build_win_x64.rb @@ -0,0 +1,20 @@ +module WarpEngine + module SoftwareUpdater + module Builds + module BuildWinX64 + extend ActiveSupport::Concern + + included do + register_expected_kind "win_x64" + end + + private + + def win_x64_asset_path(versioned) + path = full_path("#{versioned}-win-x64.zip") + { "win_x64" => path } if File.file?(path) + end + end + end + end +end diff --git a/libs/ruby/warp_engine/app/services/concerns/warp_engine/software_updater/builds/build_win_x86.rb b/libs/ruby/warp_engine/app/services/concerns/warp_engine/software_updater/builds/build_win_x86.rb new file mode 100644 index 0000000..ab1750f --- /dev/null +++ b/libs/ruby/warp_engine/app/services/concerns/warp_engine/software_updater/builds/build_win_x86.rb @@ -0,0 +1,20 @@ +module WarpEngine + module SoftwareUpdater + module Builds + module BuildWinX86 + extend ActiveSupport::Concern + + included do + register_expected_kind "win_x86" + end + + private + + def win_x86_asset_path(versioned) + path = full_path("#{versioned}-win-x86.zip") + { "win_x86" => path } if File.file?(path) + end + end + end + end +end diff --git a/libs/ruby/warp_engine/app/services/concerns/warp_engine/software_updater/metadata_parsing.rb b/libs/ruby/warp_engine/app/services/concerns/warp_engine/software_updater/metadata_parsing.rb new file mode 100644 index 0000000..805cc43 --- /dev/null +++ b/libs/ruby/warp_engine/app/services/concerns/warp_engine/software_updater/metadata_parsing.rb @@ -0,0 +1,16 @@ +require "json" + +module WarpEngine + module SoftwareUpdater + module MetadataParsing + METADATA_KEYS = %i[name title author desc site repo license].freeze + + private + + def parse_json_metadata(path) + raw = JSON.parse(File.read(path), symbolize_names: true) + raw.slice(*METADATA_KEYS) + end + end + end +end diff --git a/libs/ruby/warp_engine/app/services/concerns/warp_engine/software_updater/software_persistence.rb b/libs/ruby/warp_engine/app/services/concerns/warp_engine/software_updater/software_persistence.rb new file mode 100644 index 0000000..5fc87e5 --- /dev/null +++ b/libs/ruby/warp_engine/app/services/concerns/warp_engine/software_updater/software_persistence.rb @@ -0,0 +1,38 @@ +module WarpEngine + module SoftwareUpdater + module SoftwarePersistence + private + + def update_or_create_software(attrs) + software = WarpEngine::Software.unscoped.find_or_initialize_by(name: attrs[:name]) + software.assign_attributes(attrs.except(:name)) + software.deleted_at = nil + software.save! + software + end + + def upsert_external_link(software_id, label, url) + 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 = WarpEngine::Release.unscoped.find_by(software_id: attrs[:software_id], version: attrs[:version]) + return existing if existing + + WarpEngine::Release.create!(attrs) + end + + def sync_release_assets(release, kind_paths) + kind_paths.each do |kind, path| + asset = WarpEngine::ReleaseAsset.unscoped.find_or_initialize_by(release_id: release.id, kind: kind) + asset.path = path + asset.deleted_at = nil + asset.save! + end + end + end + end +end diff --git a/libs/ruby/warp_engine/app/services/concerns/warp_engine/software_updater/updatable.rb b/libs/ruby/warp_engine/app/services/concerns/warp_engine/software_updater/updatable.rb new file mode 100644 index 0000000..ce2646a --- /dev/null +++ b/libs/ruby/warp_engine/app/services/concerns/warp_engine/software_updater/updatable.rb @@ -0,0 +1,74 @@ +module WarpEngine + module SoftwareUpdater + module Updatable + extend ActiveSupport::Concern + include ArchiveExtraction + include MetadataParsing + include SoftwarePersistence + + class_methods do + def platform(value = nil) + @platform = value if value + @platform ||= name.demodulize.delete_suffix("Service").downcase + end + + def label(value = nil) + @label = value if value + @label ||= platform.titleize + end + + def expected_kinds + @expected_kinds ||= [] + end + + def register_expected_kind(kind) + expected_kinds << kind unless expected_kinds.include?(kind) + end + + def prepare_steps + @prepare_steps ||= [] + end + + def register_prepare_step(method_name) + prepare_steps << method_name unless prepare_steps.include?(method_name) + end + end + + def update(name, version) + versioned = "#{name}-#{version}" + prepare_files(versioned) + + metadata = parse_metadata(versioned) + site_url = metadata.delete(:site) + repo_url = metadata.delete(:repo) + + ActiveRecord::Base.transaction do + software = update_or_create_software(metadata.merge(platform: self.class.platform)) + upsert_external_link(software.id, "Source Code", site_url) if site_url.present? + upsert_external_link(software.id, "Repository", repo_url) if repo_url.present? + + release = create_release_if_not_exists(software_id: software.id, version: version) + sync_release_assets(release, collect_asset_paths(versioned)) + release + end + end + + private + + def prepare_files(versioned) + self.class.prepare_steps.each { |step| send(step, versioned) } + end + + def parse_metadata(versioned) + parse_json_metadata(full_path("#{versioned}.metadata.json")) + end + + def collect_asset_paths(versioned) + self.class.expected_kinds.each_with_object({}) do |kind, hash| + result = send(:"#{kind}_asset_path", versioned) + hash.merge!(result) if result + end + end + end + end +end diff --git a/libs/ruby/warp_engine/app/services/warp_engine/builds_service.rb b/libs/ruby/warp_engine/app/services/warp_engine/builds_service.rb new file mode 100644 index 0000000..270c41b --- /dev/null +++ b/libs/ruby/warp_engine/app/services/warp_engine/builds_service.rb @@ -0,0 +1,39 @@ +module WarpEngine + class BuildsService + def index + platforms = WarpEngine::PlatformLink::SUPPORTED_PLATFORMS.each_with_object({}) do |platform, hash| + service_class = "WarpEngine::SoftwareUpdater::#{platform.camelize}Service".constantize + hash[platform] = { + label: service_class.label, + kinds: service_class.expected_kinds + } + end + + all_kinds = WarpEngine::ReleaseAsset::KINDS + + { platforms: platforms, allKinds: all_kinds } + end + + def show(name) + software = WarpEngine::Software.find_by!(name: name) + service_class = "WarpEngine::SoftwareUpdater::#{software.platform.camelize}Service".constantize + expected = service_class.expected_kinds + + releases = software.releases.includes(:release_assets).order(updated_at: :desc) + + release_data = releases.each_with_object({}) do |release, hash| + actual = release.release_assets.map(&:kind) + hash[release.version] = { + actual: actual, + missing: expected - actual + } + end + + { + platform: software.platform, + expected: expected, + releases: release_data + } + end + end +end diff --git a/libs/ruby/warp_engine/app/services/warp_engine/download_service.rb b/libs/ruby/warp_engine/app/services/warp_engine/download_service.rb new file mode 100644 index 0000000..f21f473 --- /dev/null +++ b/libs/ruby/warp_engine/app/services/warp_engine/download_service.rb @@ -0,0 +1,34 @@ +module WarpEngine + class DownloadService + def self.container_base + WarpEngine.config.file_container_path + end + + # Lazy: a container path csak az első használatkor kötelező, boot/teszt közben nem. + def self.base_path + Pathname.new(container_base).realpath + end + + def create(path:, ip:, user_agent:, referer:) + sanitized = path.to_s + base_path = self.class.base_path + full_path = base_path.join(sanitized).realpath + return nil unless full_path.to_s.start_with?(base_path.to_s) + return nil unless File.file?(full_path) + + escaped = sanitized.gsub("%", "\\%").gsub("_", "\\_") + asset = WarpEngine::ReleaseAsset.find_by(path: File.join(self.class.container_base, sanitized)) || + WarpEngine::ReleaseAsset.where("path LIKE ?", "%#{escaped}%").first + + WarpEngine::Download.create!( + file_path: sanitized, + release: asset&.release, + ip_address: ip, + user_agent: user_agent&.truncate(500), + referer: referer&.truncate(500) + ) + + full_path + end + end +end diff --git a/libs/ruby/warp_engine/app/services/warp_engine/file_manager_service.rb b/libs/ruby/warp_engine/app/services/warp_engine/file_manager_service.rb new file mode 100644 index 0000000..59e52e7 --- /dev/null +++ b/libs/ruby/warp_engine/app/services/warp_engine/file_manager_service.rb @@ -0,0 +1,93 @@ +module WarpEngine + class FileManagerService + # Lazy: a container path csak az első használatkor kötelező, boot/teszt közben nem. + def base_path + @base_path ||= Pathname.new(WarpEngine.config.file_container_path) + end + + def list(relative_path = "") + full = safe_path!(relative_path) + raise ArgumentError, "Not a directory" unless full.directory? + + entries = full.children.sort_by { |c| [ c.directory? ? 0 : 1, c.basename.to_s.downcase ] } + entries.map do |child| + stat = child.stat + { + name: child.basename.to_s, + path: child.relative_path_from(base_path).to_s, + type: child.directory? ? :directory : :file, + size: child.directory? ? nil : stat.size, + mtime: stat.mtime + } + end + end + + MAX_UPLOAD_SIZE = 100 * 1024 * 1024 # 100MB + + def upload(relative_dir, uploaded_file) + raise ArgumentError, "File too large (max 100MB)" if uploaded_file.size > MAX_UPLOAD_SIZE + dir = safe_path!(relative_dir) + raise ArgumentError, "Not a directory" unless dir.directory? + + safe_name = sanitize_name(uploaded_file.original_filename) + target = dir.join(safe_name) + raise ArgumentError, "Path escape" unless target.to_s.start_with?(base_path.to_s) + + IO.copy_stream(uploaded_file.to_io, target.to_s) + target.relative_path_from(base_path).to_s + end + + def delete(relative_path) + full = safe_path!(relative_path) + raise ArgumentError, "Cannot delete root" if full == base_path + + if full.directory? + full.rmdir + else + full.delete + end + end + + def rename(relative_path, new_name) + full = safe_path!(relative_path) + raise ArgumentError, "Cannot rename root" if full == base_path + + safe_name = sanitize_name(new_name) + new_full = full.parent.join(safe_name) + raise ArgumentError, "Path escape" unless new_full.to_s.start_with?(base_path.to_s) + + full.rename(new_full) + new_full.relative_path_from(base_path).to_s + end + + def mkdir(relative_path, folder_name) + parent = safe_path!(relative_path) + raise ArgumentError, "Not a directory" unless parent.directory? + + safe_name = sanitize_name(folder_name) + new_dir = parent.join(safe_name) + raise ArgumentError, "Path escape" unless new_dir.to_s.start_with?(base_path.to_s) + + new_dir.mkdir + new_dir.relative_path_from(base_path).to_s + end + + private + + def safe_path!(relative_path) + cleaned = relative_path.to_s.gsub("..", "").squeeze("/").gsub(%r{^/|/$}, "") + full = base_path.join(cleaned) + resolved = full.exist? ? full.realpath : full.cleanpath + unless resolved.to_s.start_with?(base_path.to_s) + raise ArgumentError, "Path traversal detected" + end + resolved + end + + def sanitize_name(name) + name.to_s.gsub("..", "").gsub("/", "").gsub("\\", "").strip.tap do |n| + raise ArgumentError, "Invalid name" if n.blank? + end + end + end +end diff --git a/libs/ruby/warp_engine/app/services/warp_engine/file_service.rb b/libs/ruby/warp_engine/app/services/warp_engine/file_service.rb new file mode 100644 index 0000000..d50191e --- /dev/null +++ b/libs/ruby/warp_engine/app/services/warp_engine/file_service.rb @@ -0,0 +1,35 @@ +module WarpEngine + class FileService + # Lazy: a container path csak az első használatkor kötelező, boot/teszt közben nem. + def self.base_path + Pathname.new(WarpEngine.config.file_container_path).realpath + end + + def show(input) + full_path = base_path.join(input.path.to_s) + return FileResultDto.not_found unless safe_path?(full_path) + + if File.directory?(full_path) + index_path = full_path.join("index.html") + return FileResultDto.not_found unless File.file?(index_path) + return FileResultDto.redirect("/file/#{input.path.to_s.chomp("/")}/index.html") + end + + if File.file?(full_path) + FileResultDto.file(full_path) + else + FileResultDto.not_found + end + end + + private + + def base_path + @base_path ||= self.class.base_path + end + + def safe_path?(path) + File.exist?(path) && Pathname.new(path).realpath.to_s.start_with?(base_path.to_s) + end + end +end diff --git a/libs/ruby/warp_engine/app/services/warp_engine/image_service.rb b/libs/ruby/warp_engine/app/services/warp_engine/image_service.rb new file mode 100644 index 0000000..69be38d --- /dev/null +++ b/libs/ruby/warp_engine/app/services/warp_engine/image_service.rb @@ -0,0 +1,7 @@ +module WarpEngine + class ImageService + def show(input) + WarpEngine::Image.find(input.id) + end + end +end diff --git a/libs/ruby/warp_engine/app/services/warp_engine/software_highlighted_service.rb b/libs/ruby/warp_engine/app/services/warp_engine/software_highlighted_service.rb new file mode 100644 index 0000000..b898109 --- /dev/null +++ b/libs/ruby/warp_engine/app/services/warp_engine/software_highlighted_service.rb @@ -0,0 +1,16 @@ +module WarpEngine + class SoftwareHighlightedService + include SoftwareResponseBuilder + + def index + software = WarpEngine::Software.includes(:external_links, :software_images) + .where(highlighted: true) + .order(id: :desc) + .first + return nil unless software + + 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 +end diff --git a/libs/ruby/warp_engine/app/services/warp_engine/software_response_builder.rb b/libs/ruby/warp_engine/app/services/warp_engine/software_response_builder.rb new file mode 100644 index 0000000..a895ef1 --- /dev/null +++ b/libs/ruby/warp_engine/app/services/warp_engine/software_response_builder.rb @@ -0,0 +1,27 @@ +module WarpEngine + module SoftwareResponseBuilder + private + + def build_response(software, releases, download_counts) + sorted = releases.sort_by { |r| r.created_at || Time.at(0) }.reverse + latest = sorted.reject { |r| r.version.to_s.start_with?("dev-") }.first + # web-playable, ha az utolsó (stabil) release-nek van webes assetje + web_playable = latest if latest&.release_assets&.any? { |a| a.kind == "html" } + total_downloads = releases.sum { |r| download_counts.fetch(r.id, 0) } + + SoftwareDetailSerializer.render_as_hash(software, + releases: sorted, + latest: latest, + web_playable: web_playable, + total_downloads: total_downloads, + download_counts: download_counts + ) + end + + # 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? + WarpEngine::Download.where(release_id: release_ids).group(:release_id).count + end + end +end diff --git a/libs/ruby/warp_engine/app/services/warp_engine/software_service.rb b/libs/ruby/warp_engine/app/services/warp_engine/software_service.rb new file mode 100644 index 0000000..45b2174 --- /dev/null +++ b/libs/ruby/warp_engine/app/services/warp_engine/software_service.rb @@ -0,0 +1,11 @@ +module WarpEngine + class SoftwareService + include SoftwareResponseBuilder + + def index + 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 + end +end diff --git a/libs/ruby/warp_engine/app/services/warp_engine/software_updater/bevy_service.rb b/libs/ruby/warp_engine/app/services/warp_engine/software_updater/bevy_service.rb new file mode 100644 index 0000000..d6a6683 --- /dev/null +++ b/libs/ruby/warp_engine/app/services/warp_engine/software_updater/bevy_service.rb @@ -0,0 +1,12 @@ +module WarpEngine + module SoftwareUpdater + class BevyService + include Updatable + include Builds::BuildWeb + include Builds::BuildWinX64 + include Builds::BuildLinuxX64 + + label "Bevy" + end + end +end diff --git a/libs/ruby/warp_engine/app/services/warp_engine/software_updater/c64_service.rb b/libs/ruby/warp_engine/app/services/warp_engine/software_updater/c64_service.rb new file mode 100644 index 0000000..978422a --- /dev/null +++ b/libs/ruby/warp_engine/app/services/warp_engine/software_updater/c64_service.rb @@ -0,0 +1,12 @@ +module WarpEngine + module SoftwareUpdater + class C64Service + include Updatable + include Builds::BuildCartridge + + label "C64" + + def cartridge_ext = ".prg" + end + end +end diff --git a/libs/ruby/warp_engine/app/services/warp_engine/software_updater/ebitengine_service.rb b/libs/ruby/warp_engine/app/services/warp_engine/software_updater/ebitengine_service.rb new file mode 100644 index 0000000..4109562 --- /dev/null +++ b/libs/ruby/warp_engine/app/services/warp_engine/software_updater/ebitengine_service.rb @@ -0,0 +1,15 @@ +module WarpEngine + module SoftwareUpdater + class EbitengineService + include Updatable + include Builds::BuildWeb + include Builds::BuildWinX86 + include Builds::BuildWinX64 + include Builds::BuildLinuxX64 + include Builds::BuildMacX64 + include Builds::BuildMacArm64 + + label "Ebitengine" + end + end +end diff --git a/libs/ruby/warp_engine/app/services/warp_engine/software_updater/godot_service.rb b/libs/ruby/warp_engine/app/services/warp_engine/software_updater/godot_service.rb new file mode 100644 index 0000000..4a72a55 --- /dev/null +++ b/libs/ruby/warp_engine/app/services/warp_engine/software_updater/godot_service.rb @@ -0,0 +1,14 @@ +module WarpEngine + module SoftwareUpdater + class GodotService + include Updatable + include Builds::BuildWeb + include Builds::BuildWinX86 + include Builds::BuildWinX64 + include Builds::BuildLinuxX64 + include Builds::BuildMacUniversal + + label "Godot" + end + end +end diff --git a/libs/ruby/warp_engine/app/services/warp_engine/software_updater/love_service.rb b/libs/ruby/warp_engine/app/services/warp_engine/software_updater/love_service.rb new file mode 100644 index 0000000..667ce4b --- /dev/null +++ b/libs/ruby/warp_engine/app/services/warp_engine/software_updater/love_service.rb @@ -0,0 +1,13 @@ +module WarpEngine + module SoftwareUpdater + class LoveService + include Updatable + include Builds::BuildWeb + include Builds::BuildWinX64 + include Builds::BuildLinuxX64 + include Builds::BuildMacUniversal + + label "LÖVE" + end + end +end diff --git a/libs/ruby/warp_engine/app/services/warp_engine/software_updater/phaser_service.rb b/libs/ruby/warp_engine/app/services/warp_engine/software_updater/phaser_service.rb new file mode 100644 index 0000000..3beb402 --- /dev/null +++ b/libs/ruby/warp_engine/app/services/warp_engine/software_updater/phaser_service.rb @@ -0,0 +1,10 @@ +module WarpEngine + module SoftwareUpdater + class PhaserService + include Updatable + include Builds::BuildWeb + + label "Phaser" + end + end +end diff --git a/libs/ruby/warp_engine/app/services/warp_engine/software_updater/tic80_service.rb b/libs/ruby/warp_engine/app/services/warp_engine/software_updater/tic80_service.rb new file mode 100644 index 0000000..2eaa6f0 --- /dev/null +++ b/libs/ruby/warp_engine/app/services/warp_engine/software_updater/tic80_service.rb @@ -0,0 +1,38 @@ +module WarpEngine + module SoftwareUpdater + class Tic80Service + include Updatable + include Builds::BuildCartridge + include Builds::BuildSource + include Builds::BuildWeb + include Builds::BuildDocs + include Builds::BuildWinX64 + include Builds::BuildLinuxX64 + include Builds::BuildMacX64 + + label "TIC-80" + + def cartridge_ext = ".tic" + def source_ext = ".lua" + + private + + def parse_metadata(versioned) + parse_lua_metadata(full_path("#{versioned}.lua")) + end + + def parse_lua_metadata(source_path) + metadata = {} + File.foreach(source_path) do |line| + break unless line.start_with?("--") + parts = line[2..].split(":", 2) + next if parts.length != 2 + key = parts[0].strip.downcase.to_sym + value = parts[1].strip + metadata[key] = value + end + metadata.slice(*MetadataParsing::METADATA_KEYS) + end + end + end +end diff --git a/libs/ruby/warp_engine/app/services/warp_engine/update_service.rb b/libs/ruby/warp_engine/app/services/warp_engine/update_service.rb new file mode 100644 index 0000000..5a498ec --- /dev/null +++ b/libs/ruby/warp_engine/app/services/warp_engine/update_service.rb @@ -0,0 +1,11 @@ +module WarpEngine + class UpdateService + def update(input) + unless WarpEngine::PlatformLink::SUPPORTED_PLATFORMS.include?(input.platform) + raise ArgumentError, "Unsupported platform: #{input.platform}" + end + + "WarpEngine::SoftwareUpdater::#{input.platform.camelize}Service".constantize.new.update(input.name, input.version) + end + end +end diff --git a/libs/ruby/warp_engine/lib/warp_engine.rb b/libs/ruby/warp_engine/lib/warp_engine.rb index a396622..8ca83e8 100644 --- a/libs/ruby/warp_engine/lib/warp_engine.rb +++ b/libs/ruby/warp_engine/lib/warp_engine.rb @@ -1,3 +1,5 @@ +require "blueprinter" + require "warp_engine/version" require "warp_engine/configuration" diff --git a/libs/ruby/warp_engine/spec/dummy/Rakefile b/libs/ruby/warp_engine/spec/dummy/Rakefile new file mode 100644 index 0000000..7820749 --- /dev/null +++ b/libs/ruby/warp_engine/spec/dummy/Rakefile @@ -0,0 +1,2 @@ +require_relative "config/application" +Rails.application.load_tasks diff --git a/libs/ruby/warp_engine/spec/dummy/config.ru b/libs/ruby/warp_engine/spec/dummy/config.ru new file mode 100644 index 0000000..5c59c6f --- /dev/null +++ b/libs/ruby/warp_engine/spec/dummy/config.ru @@ -0,0 +1,4 @@ +require_relative "config/environment" + +run Rails.application +Rails.application.load_server diff --git a/libs/ruby/warp_engine/spec/dummy/config/application.rb b/libs/ruby/warp_engine/spec/dummy/config/application.rb new file mode 100644 index 0000000..616833d --- /dev/null +++ b/libs/ruby/warp_engine/spec/dummy/config/application.rb @@ -0,0 +1,23 @@ +require_relative "boot" + +require "rails" +require "active_model/railtie" +require "active_record/railtie" +require "action_controller/railtie" +require "action_view/railtie" +require "action_dispatch/railtie" + +Bundler.require(*Rails.groups) +require "warp_engine" + +module Dummy + class Application < Rails::Application + config.load_defaults 8.0 + config.eager_load = false + + config.time_zone = "UTC" + config.active_record.default_timezone = :utc + + config.hosts.clear + end +end diff --git a/libs/ruby/warp_engine/spec/dummy/config/boot.rb b/libs/ruby/warp_engine/spec/dummy/config/boot.rb new file mode 100644 index 0000000..5624a9d --- /dev/null +++ b/libs/ruby/warp_engine/spec/dummy/config/boot.rb @@ -0,0 +1,3 @@ +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../../Gemfile", __dir__) + +require "bundler/setup" diff --git a/libs/ruby/warp_engine/spec/dummy/config/database.yml b/libs/ruby/warp_engine/spec/dummy/config/database.yml new file mode 100644 index 0000000..24441b0 --- /dev/null +++ b/libs/ruby/warp_engine/spec/dummy/config/database.yml @@ -0,0 +1,8 @@ +test: + adapter: mysql2 + encoding: utf8mb4 + username: <%= ENV.fetch("DB_USER", "root") %> + password: <%= ENV.fetch("DB_PASSWORD", "") %> + host: <%= ENV.fetch("DB_HOST", "mysql") %> + port: <%= ENV.fetch("DB_PORT", "3306") %> + database: warp_engine_test diff --git a/libs/ruby/warp_engine/spec/dummy/config/environment.rb b/libs/ruby/warp_engine/spec/dummy/config/environment.rb new file mode 100644 index 0000000..73a3979 --- /dev/null +++ b/libs/ruby/warp_engine/spec/dummy/config/environment.rb @@ -0,0 +1,3 @@ +require_relative "application" + +Rails.application.initialize! diff --git a/libs/ruby/warp_engine/spec/dummy/config/environments/test.rb b/libs/ruby/warp_engine/spec/dummy/config/environments/test.rb new file mode 100644 index 0000000..7388880 --- /dev/null +++ b/libs/ruby/warp_engine/spec/dummy/config/environments/test.rb @@ -0,0 +1,7 @@ +Rails.application.configure do + config.cache_classes = true + config.consider_all_requests_local = true + config.action_dispatch.show_exceptions = :rescuable + config.active_support.deprecation = :stderr + config.log_level = :warn +end diff --git a/libs/ruby/warp_engine/spec/dummy/config/routes.rb b/libs/ruby/warp_engine/spec/dummy/config/routes.rb new file mode 100644 index 0000000..7de15df --- /dev/null +++ b/libs/ruby/warp_engine/spec/dummy/config/routes.rb @@ -0,0 +1,3 @@ +Rails.application.routes.draw do + mount WarpEngine::Engine => "/" +end diff --git a/libs/ruby/warp_engine/spec/dummy/db/schema.rb b/libs/ruby/warp_engine/spec/dummy/db/schema.rb new file mode 100644 index 0000000..32f8c5c --- /dev/null +++ b/libs/ruby/warp_engine/spec/dummy/db/schema.rb @@ -0,0 +1,108 @@ +# A katalógus-táblák a host schema.rb-vel megegyező definícióval. +ActiveRecord::Schema[8.1].define(version: 1) do + create_table "downloads", id: { type: :bigint, unsigned: true }, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| + t.datetime "created_at", precision: 3 + t.datetime "deleted_at", precision: 3 + t.string "file_path", null: false + t.string "ip_address" + t.string "referer", limit: 500 + t.bigint "release_id", unsigned: true + t.datetime "updated_at", precision: 3 + t.string "user_agent", limit: 500 + t.index ["deleted_at"], name: "idx_downloads_deleted_at" + t.index ["file_path"], name: "index_downloads_on_file_path" + t.index ["release_id"], name: "index_downloads_on_release_id" + end + + create_table "external_links", id: { type: :bigint, unsigned: true }, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| + t.datetime "created_at", precision: 3 + t.datetime "deleted_at", precision: 3 + t.string "label", limit: 128 + t.bigint "software_id", unsigned: true + t.datetime "updated_at", precision: 3 + t.string "url" + t.index ["deleted_at"], name: "idx_external_links_deleted_at" + t.index ["software_id"], name: "idx_external_links_software_id" + end + + create_table "images", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| + t.string "content_type", default: "application/octet-stream", null: false + t.datetime "created_at", null: false + t.datetime "deleted_at", precision: 3 + t.string "filename", null: false + t.string "original_filename", null: false + t.datetime "updated_at", null: false + t.index ["deleted_at"], name: "idx_images_deleted_at" + end + + create_table "platform_links", id: { type: :bigint, unsigned: true }, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| + t.datetime "created_at", precision: 3 + t.datetime "deleted_at", precision: 3 + t.string "name", limit: 128 + t.string "platform", limit: 128 + t.integer "position", default: 0, null: false + t.datetime "updated_at", precision: 3 + t.string "url" + t.index ["deleted_at"], name: "idx_platform_links_deleted_at" + t.index ["platform"], name: "idx_platform_links_platform" + end + + create_table "release_assets", id: { type: :bigint, unsigned: true }, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| + t.datetime "created_at", precision: 3, null: false + t.datetime "deleted_at", precision: 3 + t.string "kind", limit: 32, null: false + t.string "path", null: false + t.bigint "release_id", null: false, unsigned: true + t.datetime "updated_at", precision: 3, null: false + t.index ["deleted_at"], name: "idx_release_assets_deleted_at" + t.index ["path"], name: "idx_release_assets_path" + t.index ["release_id", "kind"], name: "idx_release_assets_release_kind", unique: true + end + + create_table "releases", id: { type: :bigint, unsigned: true }, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| + t.datetime "created_at", precision: 3 + t.datetime "deleted_at", precision: 3 + t.bigint "software_id", unsigned: true + t.datetime "updated_at", precision: 3 + t.string "version", limit: 64 + t.index ["deleted_at"], name: "idx_releases_deleted_at" + t.index ["software_id"], name: "idx_releases_software_id" + end + + create_table "software_images", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| + t.datetime "created_at", null: false + t.bigint "image_id", null: false + t.boolean "is_default", default: false, null: false + t.integer "position", default: 0, null: false + t.bigint "software_id", null: false, unsigned: true + t.datetime "updated_at", null: false + t.index ["image_id"], name: "fk_rails_a456fbc278" + t.index ["software_id", "image_id"], name: "index_software_images_on_software_id_and_image_id", unique: true + t.index ["software_id", "position"], name: "index_software_images_on_software_id_and_position" + end + + create_table "softwares", id: { type: :bigint, unsigned: true }, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| + t.string "author" + t.datetime "created_at", precision: 3 + t.datetime "deleted_at", precision: 3 + t.text "desc" + t.boolean "highlighted", default: false + t.string "license", limit: 128 + t.string "name", limit: 128 + t.string "platform", limit: 128 + t.string "site" + t.string "status", limit: 20, default: "development" + t.text "story" + t.string "title" + t.datetime "updated_at", precision: 3 + t.index ["deleted_at"], name: "idx_softwares_deleted_at" + t.index ["name"], name: "idx_softwares_name", unique: true + end + + add_foreign_key "downloads", "releases", name: "fk_downloads_release", on_delete: :nullify + add_foreign_key "external_links", "softwares", name: "fk_softwares_external_links", on_delete: :cascade + add_foreign_key "release_assets", "releases", name: "fk_releases_release_assets", on_delete: :cascade + add_foreign_key "releases", "softwares", name: "fk_softwares_releases", on_delete: :cascade + add_foreign_key "software_images", "images" + add_foreign_key "software_images", "softwares", on_delete: :cascade +end diff --git a/apps/api/spec/factories/downloads.rb b/libs/ruby/warp_engine/spec/factories/downloads.rb similarity index 100% rename from apps/api/spec/factories/downloads.rb rename to libs/ruby/warp_engine/spec/factories/downloads.rb diff --git a/apps/api/spec/factories/platform_links.rb b/libs/ruby/warp_engine/spec/factories/platform_links.rb similarity index 100% rename from apps/api/spec/factories/platform_links.rb rename to libs/ruby/warp_engine/spec/factories/platform_links.rb diff --git a/apps/api/spec/factories/releases.rb b/libs/ruby/warp_engine/spec/factories/releases.rb similarity index 100% rename from apps/api/spec/factories/releases.rb rename to libs/ruby/warp_engine/spec/factories/releases.rb diff --git a/apps/api/spec/factories/softwares.rb b/libs/ruby/warp_engine/spec/factories/softwares.rb similarity index 100% rename from apps/api/spec/factories/softwares.rb rename to libs/ruby/warp_engine/spec/factories/softwares.rb diff --git a/apps/api/spec/models/download_spec.rb b/libs/ruby/warp_engine/spec/models/download_spec.rb similarity index 100% rename from apps/api/spec/models/download_spec.rb rename to libs/ruby/warp_engine/spec/models/download_spec.rb diff --git a/apps/api/spec/models/platform_link_spec.rb b/libs/ruby/warp_engine/spec/models/platform_link_spec.rb similarity index 100% rename from apps/api/spec/models/platform_link_spec.rb rename to libs/ruby/warp_engine/spec/models/platform_link_spec.rb diff --git a/apps/api/spec/models/release_spec.rb b/libs/ruby/warp_engine/spec/models/release_spec.rb similarity index 100% rename from apps/api/spec/models/release_spec.rb rename to libs/ruby/warp_engine/spec/models/release_spec.rb diff --git a/apps/api/spec/models/software_spec.rb b/libs/ruby/warp_engine/spec/models/software_spec.rb similarity index 100% rename from apps/api/spec/models/software_spec.rb rename to libs/ruby/warp_engine/spec/models/software_spec.rb diff --git a/libs/ruby/warp_engine/spec/rails_helper.rb b/libs/ruby/warp_engine/spec/rails_helper.rb new file mode 100644 index 0000000..6a43560 --- /dev/null +++ b/libs/ruby/warp_engine/spec/rails_helper.rb @@ -0,0 +1,31 @@ +require "spec_helper" +ENV["RAILS_ENV"] ||= "test" +require_relative "dummy/config/environment" + +abort("The Rails environment is running in production mode!") if Rails.env.production? +require "rspec/rails" + +begin + ActiveRecord::Migration.maintain_test_schema! +rescue ActiveRecord::PendingMigrationError => e + abort e.to_s.strip +end + +# A factory_bot_rails a dummy app gyökerében keresne; a factory-k az engine spec/ alatt élnek +FactoryBot.definition_file_paths = [ File.expand_path("factories", __dir__) ] +FactoryBot.reload + +RSpec.configure do |config| + config.use_transactional_fixtures = true + config.infer_spec_type_from_file_location! + config.filter_rails_from_backtrace! + + config.include FactoryBot::Syntax::Methods +end + +Shoulda::Matchers.configure do |config| + config.integrate do |with| + with.test_framework :rspec + with.library :rails + end +end diff --git a/apps/api/spec/services/builds_service_spec.rb b/libs/ruby/warp_engine/spec/services/builds_service_spec.rb similarity index 97% rename from apps/api/spec/services/builds_service_spec.rb rename to libs/ruby/warp_engine/spec/services/builds_service_spec.rb index 33ecd91..a19f529 100644 --- a/apps/api/spec/services/builds_service_spec.rb +++ b/libs/ruby/warp_engine/spec/services/builds_service_spec.rb @@ -1,6 +1,6 @@ require "rails_helper" -RSpec.describe BuildsService do +RSpec.describe WarpEngine::BuildsService do describe "#index" do subject(:result) { described_class.new.index } diff --git a/apps/api/spec/services/software_highlighted_service_spec.rb b/libs/ruby/warp_engine/spec/services/software_highlighted_service_spec.rb similarity index 89% rename from apps/api/spec/services/software_highlighted_service_spec.rb rename to libs/ruby/warp_engine/spec/services/software_highlighted_service_spec.rb index 17c6f86..d19f44b 100644 --- a/apps/api/spec/services/software_highlighted_service_spec.rb +++ b/libs/ruby/warp_engine/spec/services/software_highlighted_service_spec.rb @@ -1,6 +1,6 @@ require "rails_helper" -RSpec.describe SoftwareHighlightedService do +RSpec.describe WarpEngine::SoftwareHighlightedService do describe "#index" do it "returns nil when no highlighted software" do create(:software, highlighted: false) diff --git a/apps/api/spec/services/software_updater/tic80_service_spec.rb b/libs/ruby/warp_engine/spec/services/software_updater/tic80_service_spec.rb similarity index 92% rename from apps/api/spec/services/software_updater/tic80_service_spec.rb rename to libs/ruby/warp_engine/spec/services/software_updater/tic80_service_spec.rb index a3ac934..4170300 100644 --- a/apps/api/spec/services/software_updater/tic80_service_spec.rb +++ b/libs/ruby/warp_engine/spec/services/software_updater/tic80_service_spec.rb @@ -1,15 +1,14 @@ require "rails_helper" require "zip" -RSpec.describe SoftwareUpdater::Tic80Service do +RSpec.describe WarpEngine::SoftwareUpdater::Tic80Service do let(:tmpdir) { Dir.mktmpdir } let(:name) { "spectic" } let(:version) { "9.9" } let(:versioned) { "#{name}-#{version}" } before do - allow(ENV).to receive(:fetch).and_call_original - allow(ENV).to receive(:fetch).with("FILE_CONTAINER_PATH", "/softwares").and_return(tmpdir) + allow(WarpEngine.config).to receive(:file_container_path).and_return(tmpdir) write_zip("#{versioned}.html.zip", "index.html" => "") File.write(File.join(tmpdir, "#{versioned}.tic"), "TIC!") diff --git a/libs/ruby/warp_engine/spec/services/update_service_spec.rb b/libs/ruby/warp_engine/spec/services/update_service_spec.rb new file mode 100644 index 0000000..ca874a9 --- /dev/null +++ b/libs/ruby/warp_engine/spec/services/update_service_spec.rb @@ -0,0 +1,59 @@ +require "rails_helper" + +RSpec.describe WarpEngine::UpdateService do + describe "#update" do + it "raises ArgumentError for unsupported platform" do + input = WarpEngine::UpdateInputDto.new(platform: "unknown", name: "game", version: "1.0") + + expect { described_class.new.update(input) }.to raise_error(ArgumentError, /Unsupported platform/) + end + + it "routes to correct platform service" do + input = WarpEngine::UpdateInputDto.new(platform: "tic80", name: "game", version: "1.0") + mock_service = instance_double(WarpEngine::SoftwareUpdater::Tic80Service) + + allow(WarpEngine::SoftwareUpdater::Tic80Service).to receive(:new).and_return(mock_service) + allow(mock_service).to receive(:update) + + described_class.new.update(input) + + expect(mock_service).to have_received(:update).with("game", "1.0") + end + + it "routes godot platform to GodotService" do + input = WarpEngine::UpdateInputDto.new(platform: "godot", name: "game", version: "1.0") + mock_service = instance_double(WarpEngine::SoftwareUpdater::GodotService) + + allow(WarpEngine::SoftwareUpdater::GodotService).to receive(:new).and_return(mock_service) + allow(mock_service).to receive(:update) + + described_class.new.update(input) + + expect(mock_service).to have_received(:update).with("game", "1.0") + end + + it "routes bevy platform to BevyService" do + input = WarpEngine::UpdateInputDto.new(platform: "bevy", name: "game", version: "1.0") + mock_service = instance_double(WarpEngine::SoftwareUpdater::BevyService) + + allow(WarpEngine::SoftwareUpdater::BevyService).to receive(:new).and_return(mock_service) + allow(mock_service).to receive(:update) + + described_class.new.update(input) + + expect(mock_service).to have_received(:update).with("game", "1.0") + end + + it "routes phaser platform to PhaserService" do + input = WarpEngine::UpdateInputDto.new(platform: "phaser", name: "game", version: "1.0") + mock_service = instance_double(WarpEngine::SoftwareUpdater::PhaserService) + + allow(WarpEngine::SoftwareUpdater::PhaserService).to receive(:new).and_return(mock_service) + allow(mock_service).to receive(:update) + + described_class.new.update(input) + + expect(mock_service).to have_received(:update).with("game", "1.0") + end + end +end diff --git a/libs/ruby/warp_engine/spec/spec_helper.rb b/libs/ruby/warp_engine/spec/spec_helper.rb new file mode 100644 index 0000000..fe819e0 --- /dev/null +++ b/libs/ruby/warp_engine/spec/spec_helper.rb @@ -0,0 +1,14 @@ +RSpec.configure do |config| + config.expect_with :rspec do |expectations| + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + config.mock_with :rspec do |mocks| + mocks.verify_partial_doubles = true + end + + config.shared_context_metadata_behavior = :apply_to_host_groups + config.filter_run_when_matching :focus + config.order = :random + Kernel.srand config.seed +end diff --git a/libs/ruby/warp_engine/warp_engine.gemspec b/libs/ruby/warp_engine/warp_engine.gemspec index 1a1d5c8..2fd85a2 100644 --- a/libs/ruby/warp_engine/warp_engine.gemspec +++ b/libs/ruby/warp_engine/warp_engine.gemspec @@ -18,4 +18,6 @@ Gem::Specification.new do |spec| spec.files = Dir["{app,config,db,lib}/**/*", "README.md"] spec.add_dependency "rails", ">= 8.0" + spec.add_dependency "blueprinter" + spec.add_dependency "rubyzip", "~> 2.3" end