replace catalog service to api
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
ActiveAdmin.register AdminUser do
|
||||
permit_params :email, :password, :password_confirmation
|
||||
|
||||
index do
|
||||
selectable_column
|
||||
id_column
|
||||
column :email
|
||||
column :created_at
|
||||
actions
|
||||
end
|
||||
|
||||
filter :email
|
||||
filter :created_at
|
||||
|
||||
form do |f|
|
||||
f.inputs do
|
||||
f.input :email
|
||||
f.input :password
|
||||
f.input :password_confirmation
|
||||
end
|
||||
f.actions
|
||||
end
|
||||
|
||||
show do
|
||||
attributes_table do
|
||||
row :id
|
||||
row :email
|
||||
row :created_at
|
||||
row :updated_at
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,27 @@
|
||||
ActiveAdmin.register_page "Dashboard" do
|
||||
menu priority: 1, label: proc { I18n.t("active_admin.dashboard") }
|
||||
|
||||
content title: proc { I18n.t("active_admin.dashboard") } do
|
||||
columns do
|
||||
column do
|
||||
panel "Softwares" do
|
||||
ul do
|
||||
li "Total: #{Software.count}"
|
||||
li "Released: #{Software.where(status: 'released').count}"
|
||||
li "In development: #{Software.where(status: 'development').count}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
column do
|
||||
panel "Recent Releases" do
|
||||
table_for Release.order(created_at: :desc).limit(10) do
|
||||
column(:software) { |r| link_to r.software.title, admin_software_path(r.software) rescue r.software_id }
|
||||
column :version
|
||||
column :created_at
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,34 @@
|
||||
ActiveAdmin.register ExternalLink do
|
||||
permit_params :software_id, :label, :url
|
||||
|
||||
menu false
|
||||
|
||||
belongs_to :software
|
||||
|
||||
index do
|
||||
selectable_column
|
||||
id_column
|
||||
column :label
|
||||
column(:url) { |el| link_to el.url, el.url, target: "_blank" }
|
||||
column :created_at
|
||||
actions
|
||||
end
|
||||
|
||||
form do |f|
|
||||
f.inputs do
|
||||
f.input :label
|
||||
f.input :url
|
||||
end
|
||||
f.actions
|
||||
end
|
||||
|
||||
show do
|
||||
attributes_table do
|
||||
row :id
|
||||
row :label
|
||||
row(:url) { |el| link_to el.url, el.url, target: "_blank" }
|
||||
row :created_at
|
||||
row :updated_at
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,33 @@
|
||||
ActiveAdmin.register Release do
|
||||
actions :index, :show
|
||||
|
||||
menu false
|
||||
|
||||
belongs_to :software, optional: true
|
||||
|
||||
index do
|
||||
selectable_column
|
||||
id_column
|
||||
column(:software) { |r| link_to r.software.title, admin_software_path(r.software) }
|
||||
column :version
|
||||
column :html_folder_path
|
||||
column :created_at
|
||||
actions only: [:show]
|
||||
end
|
||||
|
||||
filter :version
|
||||
|
||||
show do
|
||||
attributes_table do
|
||||
row :id
|
||||
row(:software) { |r| link_to r.software.title, admin_software_path(r.software) }
|
||||
row :version
|
||||
row :cartridge_path
|
||||
row :source_path
|
||||
row :html_folder_path
|
||||
row :docs_folder_path
|
||||
row :created_at
|
||||
row :updated_at
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,97 @@
|
||||
ActiveAdmin.register Software do
|
||||
permit_params :name, :title, :author, :desc, :story,
|
||||
:license, :platform, :status, :highlighted,
|
||||
external_links_attributes: [:id, :label, :url, :_destroy],
|
||||
releases_attributes: [:id, :_destroy]
|
||||
|
||||
menu priority: 2
|
||||
|
||||
index do
|
||||
selectable_column
|
||||
id_column
|
||||
column :name
|
||||
column :title
|
||||
column :platform
|
||||
column :status do |sw|
|
||||
status_tag sw.status
|
||||
end
|
||||
column :highlighted
|
||||
column :created_at
|
||||
actions
|
||||
end
|
||||
|
||||
filter :name
|
||||
filter :title
|
||||
filter :author
|
||||
filter :platform, as: :select, collection: %w[tic80 ebitengine love]
|
||||
filter :status, as: :select, collection: %w[development demo released archived]
|
||||
filter :highlighted
|
||||
|
||||
show do
|
||||
attributes_table do
|
||||
row :id
|
||||
row :name
|
||||
row :title
|
||||
row :author
|
||||
row :platform
|
||||
row :status
|
||||
row :highlighted
|
||||
row :license
|
||||
row(:desc) { |sw| simple_format sw.desc }
|
||||
row(:story) { |sw| simple_format sw.story }
|
||||
row :created_at
|
||||
row :updated_at
|
||||
end
|
||||
|
||||
panel "External Links" do
|
||||
table_for resource.external_links do
|
||||
column :label
|
||||
column(:url) { |el| link_to el.url, el.url, target: "_blank" }
|
||||
column :created_at
|
||||
column do |el|
|
||||
link_to "Edit", edit_admin_software_external_link_path(resource, el)
|
||||
concat " "
|
||||
link_to "Delete", admin_software_external_link_path(resource, el),
|
||||
method: :delete, data: { confirm: "Are you sure?" }
|
||||
end
|
||||
end
|
||||
br
|
||||
link_to "Add External Link",
|
||||
new_admin_software_external_link_path(resource)
|
||||
end
|
||||
|
||||
panel "Releases" do
|
||||
table_for resource.releases.order(created_at: :desc) do
|
||||
column :version
|
||||
column :html_folder_path
|
||||
column :cartridge_path
|
||||
column :source_path
|
||||
column :docs_folder_path
|
||||
column :created_at
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
form do |f|
|
||||
f.inputs "Details" do
|
||||
f.input :name
|
||||
f.input :title
|
||||
f.input :author
|
||||
f.input :platform, as: :select, collection: %w[tic80 ebitengine love]
|
||||
f.input :status, as: :select, collection: %w[development demo released archived]
|
||||
f.input :highlighted
|
||||
f.input :license
|
||||
f.input :desc, as: :text, input_html: { rows: 4 }
|
||||
f.input :story, as: :text, input_html: { rows: 8 }
|
||||
end
|
||||
|
||||
f.inputs "External Links" do
|
||||
f.has_many :external_links, allow_destroy: true, new_record: true do |el|
|
||||
el.input :label
|
||||
el.input :url
|
||||
end
|
||||
end
|
||||
|
||||
f.actions
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,2 @@
|
||||
//= link active_admin.css
|
||||
//= link active_admin.js
|
||||
@@ -0,0 +1 @@
|
||||
//= require active_admin/base
|
||||
@@ -0,0 +1,2 @@
|
||||
@import "active_admin/mixins";
|
||||
@import "active_admin/base";
|
||||
@@ -0,0 +1,8 @@
|
||||
module Api
|
||||
class SoftwareController < ApiController
|
||||
def index
|
||||
result = SoftwareListService.new.call
|
||||
render json: result
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,12 @@
|
||||
module Api
|
||||
class SoftwareHighlightedController < ApiController
|
||||
def index
|
||||
result = SoftwareHighlightedService.new.call
|
||||
if result.nil?
|
||||
render json: { error: "no highlighted software found" }, status: :not_found
|
||||
else
|
||||
render json: result
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,2 @@
|
||||
class ApiController < ActionController::API
|
||||
end
|
||||
@@ -0,0 +1,3 @@
|
||||
class ApplicationController < ActionController::Base
|
||||
protect_from_forgery with: :null_session
|
||||
end
|
||||
@@ -0,0 +1,13 @@
|
||||
class FilesController < ApplicationController
|
||||
def show
|
||||
base = Pathname.new(ENV.fetch("FILE_CONTAINER_PATH", "/softwares")).realpath
|
||||
full = base.join(params[:path])
|
||||
|
||||
return render plain: "Not Found", status: :not_found unless full.to_s.start_with?(base.to_s)
|
||||
return render plain: "Not Found", status: :not_found unless full.exist? && full.file?
|
||||
|
||||
send_file full.to_s, disposition: "inline"
|
||||
rescue Errno::ENOENT
|
||||
render plain: "Not Found", status: :not_found
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,32 @@
|
||||
class UpdateController < ApiController
|
||||
PLATFORM_SERVICES = {
|
||||
"tic80" => "SoftwareUpdater::Tic80Service",
|
||||
"ebitengine" => "SoftwareUpdater::EbitengineService",
|
||||
"love" => "SoftwareUpdater::LoveService"
|
||||
}.freeze
|
||||
|
||||
def update
|
||||
return render plain: "Unauthorized", status: :unauthorized unless authorized?
|
||||
|
||||
platform = params[:platform]
|
||||
name = params[:name]
|
||||
version = params[:version]
|
||||
|
||||
return render plain: "Version not provided", status: :bad_request if version.blank?
|
||||
|
||||
service_class_name = PLATFORM_SERVICES[platform]
|
||||
return render plain: "Unsupported platform: #{platform}", status: :bad_request unless service_class_name
|
||||
|
||||
service_class_name.constantize.new.update(name, version)
|
||||
render plain: "Updated"
|
||||
rescue => e
|
||||
Rails.logger.error("[UpdateController] #{e.class}: #{e.message}\n#{e.backtrace.first(5).join("\n")}")
|
||||
render plain: e.message, status: :internal_server_error
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def authorized?
|
||||
params[:secret] == ENV.fetch("UPDATE_SECRET", "")
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,8 @@
|
||||
class AdminUser < ApplicationRecord
|
||||
devise :database_authenticatable,
|
||||
:recoverable, :rememberable, :validatable
|
||||
|
||||
def self.ransackable_attributes(auth_object = nil)
|
||||
%w[created_at email id updated_at]
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,3 @@
|
||||
class ApplicationRecord < ActiveRecord::Base
|
||||
primary_abstract_class
|
||||
end
|
||||
@@ -0,0 +1,15 @@
|
||||
class ExternalLink < ApplicationRecord
|
||||
self.table_name = "external_links"
|
||||
|
||||
belongs_to :software
|
||||
|
||||
default_scope { where(deleted_at: nil) }
|
||||
|
||||
def self.ransackable_attributes(auth_object = nil)
|
||||
%w[created_at deleted_at id label software_id updated_at url]
|
||||
end
|
||||
|
||||
def self.ransackable_associations(auth_object = nil)
|
||||
%w[software]
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,15 @@
|
||||
class Release < ApplicationRecord
|
||||
self.table_name = "releases"
|
||||
|
||||
belongs_to :software
|
||||
|
||||
default_scope { where(deleted_at: nil) }
|
||||
|
||||
def self.ransackable_attributes(auth_object = nil)
|
||||
%w[cartridge_path created_at deleted_at docs_folder_path html_folder_path id software_id source_path updated_at version web_playable]
|
||||
end
|
||||
|
||||
def self.ransackable_associations(auth_object = nil)
|
||||
%w[software]
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,18 @@
|
||||
class Software < ApplicationRecord
|
||||
self.table_name = "softwares"
|
||||
|
||||
has_many :releases, foreign_key: :software_id
|
||||
has_many :external_links, foreign_key: :software_id
|
||||
|
||||
accepts_nested_attributes_for :external_links, allow_destroy: true
|
||||
|
||||
default_scope { where(deleted_at: nil) }
|
||||
|
||||
def self.ransackable_attributes(auth_object = nil)
|
||||
%w[author created_at desc highlighted id license name platform site status story title updated_at]
|
||||
end
|
||||
|
||||
def self.ransackable_associations(auth_object = nil)
|
||||
%w[releases external_links]
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,12 @@
|
||||
class SoftwareHighlightedService
|
||||
def call
|
||||
software = Software.includes(:external_links)
|
||||
.where(highlighted: true)
|
||||
.order(id: :desc)
|
||||
.first
|
||||
return nil unless software
|
||||
|
||||
releases = Release.where(software_id: software.id).to_a
|
||||
SoftwareSerializer.show_data(software, releases)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,9 @@
|
||||
class SoftwareListService
|
||||
def call
|
||||
softwares = Software.includes(:releases, :external_links).all
|
||||
items = softwares.map do |sw|
|
||||
SoftwareSerializer.show_data(sw, sw.releases.to_a)
|
||||
end
|
||||
{ softwares: items }
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,82 @@
|
||||
class SoftwareSerializer
|
||||
GO_ZERO_TIME = "0001-01-01T00:00:00Z"
|
||||
FILE_PATH_FROM = "/softwares/"
|
||||
FILE_PATH_TO = "/file/"
|
||||
|
||||
def self.show_data(software, releases)
|
||||
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 = sorted.find { |r| r.html_folder_path.present? }
|
||||
|
||||
{
|
||||
software: serialize_software(software),
|
||||
releases: sorted.map { |r| serialize_release(r) },
|
||||
latestRelease: latest ? serialize_release(latest) : nil,
|
||||
webPlayableRelease: web_playable ? serialize_release(web_playable) : nil
|
||||
}
|
||||
end
|
||||
|
||||
def self.serialize_software(sw)
|
||||
{
|
||||
"ID" => sw.id,
|
||||
"CreatedAt" => fmt(sw.created_at),
|
||||
"UpdatedAt" => fmt(sw.updated_at),
|
||||
"DeletedAt" => fmt_nullable(sw.deleted_at),
|
||||
"name" => sw.name,
|
||||
"title" => sw.title,
|
||||
"author" => sw.author,
|
||||
"desc" => sw.desc.to_s,
|
||||
"story" => sw.story.to_s,
|
||||
"license" => sw.license.to_s,
|
||||
"platform" => sw.platform,
|
||||
"status" => sw.status,
|
||||
"highlighted" => sw.highlighted ? true : false,
|
||||
"externalLinks" => sw.external_links.map { |el| serialize_external_link(el) }
|
||||
}
|
||||
end
|
||||
|
||||
def self.serialize_release(r)
|
||||
{
|
||||
"ID" => r.id,
|
||||
"CreatedAt" => fmt(r.created_at),
|
||||
"UpdatedAt" => fmt(r.updated_at),
|
||||
"DeletedAt" => fmt_nullable(r.deleted_at),
|
||||
"softwareId" => r.software_id,
|
||||
"version" => r.version,
|
||||
"cartridgePath" => rewrite(r.cartridge_path),
|
||||
"sourcePath" => rewrite(r.source_path),
|
||||
"htmlFolderPath" => rewrite(r.html_folder_path),
|
||||
"docsFolderPath" => rewrite(r.docs_folder_path)
|
||||
}
|
||||
end
|
||||
|
||||
def self.serialize_external_link(el)
|
||||
{
|
||||
"ID" => el.id,
|
||||
"CreatedAt" => fmt(el.created_at),
|
||||
"UpdatedAt" => fmt(el.updated_at),
|
||||
"DeletedAt" => fmt_nullable(el.deleted_at),
|
||||
"softwareId" => el.software_id,
|
||||
"label" => el.label,
|
||||
"url" => el.url
|
||||
}
|
||||
end
|
||||
|
||||
# null → Go zero-time string (matches GORM behavior)
|
||||
def self.fmt(t)
|
||||
return GO_ZERO_TIME if t.nil?
|
||||
t.utc.strftime("%Y-%m-%dT%H:%M:%S.%3NZ")
|
||||
end
|
||||
|
||||
# null → JSON null (matches gorm.DeletedAt behavior)
|
||||
def self.fmt_nullable(t)
|
||||
return nil if t.nil?
|
||||
t.utc.strftime("%Y-%m-%dT%H:%M:%S.%3NZ")
|
||||
end
|
||||
|
||||
def self.rewrite(path)
|
||||
return "" if path.blank?
|
||||
path.gsub(FILE_PATH_FROM, FILE_PATH_TO)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,11 @@
|
||||
class SoftwareShowService
|
||||
def initialize(name)
|
||||
@name = name
|
||||
end
|
||||
|
||||
def call
|
||||
software = Software.includes(:external_links).find_by!(name: @name)
|
||||
releases = Release.where(software_id: software.id).to_a
|
||||
SoftwareSerializer.show_data(software, releases)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,61 @@
|
||||
require "zip"
|
||||
require "fileutils"
|
||||
|
||||
module SoftwareUpdater
|
||||
class BaseService
|
||||
def initialize
|
||||
@base_path = ENV.fetch("FILE_CONTAINER_PATH", "/softwares")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
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 update_or_create_software(attrs)
|
||||
software = 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 = ExternalLink.unscoped.find_or_initialize_by(software_id: software_id, label: label)
|
||||
link.url = url
|
||||
link.deleted_at = nil
|
||||
link.save!
|
||||
end
|
||||
|
||||
def create_release_if_not_exists(attrs)
|
||||
existing = Release.unscoped.find_by(software_id: attrs[:software_id], version: attrs[:version])
|
||||
return existing if existing
|
||||
|
||||
Release.create!(attrs)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,34 @@
|
||||
require "json"
|
||||
|
||||
module SoftwareUpdater
|
||||
class EbitengineService < BaseService
|
||||
def update(name, version)
|
||||
versioned = "#{name}-#{version}"
|
||||
zip_file = "#{versioned}.html.zip"
|
||||
html_dir = versioned
|
||||
|
||||
delete_dir(html_dir) if dir_exists?(html_dir)
|
||||
create_dir(html_dir)
|
||||
unzip_file(zip_file, html_dir)
|
||||
|
||||
metadata = parse_metadata(full_path("#{versioned}.metadata.json"))
|
||||
site_url = metadata.delete(:site)
|
||||
|
||||
software = update_or_create_software(metadata.merge(platform: "ebitengine"))
|
||||
upsert_external_link(software.id, "Source Code", site_url) if site_url.present?
|
||||
|
||||
create_release_if_not_exists(
|
||||
software_id: software.id,
|
||||
version: version,
|
||||
html_folder_path: full_path(html_dir)
|
||||
)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def parse_metadata(path)
|
||||
raw = JSON.parse(File.read(path), symbolize_names: true)
|
||||
raw.slice(:name, :title, :author, :desc, :site, :license)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,34 @@
|
||||
require "json"
|
||||
|
||||
module SoftwareUpdater
|
||||
class LoveService < BaseService
|
||||
def update(name, version)
|
||||
versioned = "#{name}-#{version}"
|
||||
zip_file = "#{versioned}.html.zip"
|
||||
html_dir = versioned
|
||||
|
||||
delete_dir(html_dir) if dir_exists?(html_dir)
|
||||
create_dir(html_dir)
|
||||
unzip_file(zip_file, html_dir)
|
||||
|
||||
metadata = parse_metadata(full_path("#{versioned}.metadata.json"))
|
||||
site_url = metadata.delete(:site)
|
||||
|
||||
software = update_or_create_software(metadata.merge(platform: "love"))
|
||||
upsert_external_link(software.id, "Source Code", site_url) if site_url.present?
|
||||
|
||||
create_release_if_not_exists(
|
||||
software_id: software.id,
|
||||
version: version,
|
||||
html_folder_path: full_path(html_dir)
|
||||
)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def parse_metadata(path)
|
||||
raw = JSON.parse(File.read(path), symbolize_names: true)
|
||||
raw.slice(:name, :title, :author, :desc, :site, :license)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,50 @@
|
||||
module SoftwareUpdater
|
||||
class Tic80Service < BaseService
|
||||
def update(name, version)
|
||||
versioned = "#{name}-#{version}"
|
||||
zip_file = "#{versioned}.html.zip"
|
||||
docs_zip_file = "#{versioned}-docs.zip"
|
||||
html_dir = versioned
|
||||
docs_dir = "#{versioned}-docs"
|
||||
|
||||
delete_dir(html_dir) if dir_exists?(html_dir)
|
||||
delete_dir(docs_dir) if dir_exists?(docs_dir)
|
||||
|
||||
create_dir(html_dir)
|
||||
unzip_file(zip_file, html_dir)
|
||||
|
||||
create_dir(docs_dir)
|
||||
unzip_file(docs_zip_file, docs_dir)
|
||||
|
||||
metadata = parse_lua_metadata(full_path("#{versioned}.lua"))
|
||||
site_url = metadata.delete(:site)
|
||||
|
||||
software = update_or_create_software(metadata.merge(platform: "tic80"))
|
||||
upsert_external_link(software.id, "Source Code", site_url) if site_url.present?
|
||||
|
||||
create_release_if_not_exists(
|
||||
software_id: software.id,
|
||||
version: version,
|
||||
cartridge_path: full_path("#{versioned}.tic"),
|
||||
source_path: full_path("#{versioned}.lua"),
|
||||
html_folder_path: full_path(html_dir),
|
||||
docs_folder_path: full_path(docs_dir)
|
||||
)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
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
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user