release assets
This commit is contained in:
@@ -10,7 +10,7 @@ ActiveAdmin.register Release do
|
||||
id_column
|
||||
column(:software) { |r| link_to r.software.title, admin_software_path(r.software) }
|
||||
column :version
|
||||
column :html_folder_path
|
||||
column(:assets) { |r| r.release_assets.map(&:kind).sort.join(", ") }
|
||||
column :created_at
|
||||
actions only: [ :show ]
|
||||
end
|
||||
@@ -22,14 +22,21 @@ ActiveAdmin.register Release 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
|
||||
|
||||
panel "Assets" do
|
||||
if resource.release_assets.any?
|
||||
table_for resource.release_assets.order(:kind) do
|
||||
column :kind
|
||||
column(:path) { |a| span a.path, style: "font-family:monospace;font-size:12px;" }
|
||||
end
|
||||
else
|
||||
para "No assets for this release.", style: "color:#999;font-style:italic;"
|
||||
end
|
||||
end
|
||||
|
||||
panel "Downloads by File" do
|
||||
file_stats = Download.where(release_id: resource.id)
|
||||
.group(:file_path)
|
||||
|
||||
@@ -3,7 +3,8 @@ ActiveAdmin.register Software do
|
||||
:license, :platform, :status, :highlighted,
|
||||
software_images_attributes: [ :id, :image_id, :is_default, :position, :file_upload, :_destroy ],
|
||||
external_links_attributes: [ :id, :label, :url, :_destroy ],
|
||||
releases_attributes: [ :id, :version, :html_folder_path, :cartridge_path, :source_path, :docs_folder_path, :web_playable, :_destroy ]
|
||||
releases_attributes: [ :id, :version, :web_playable, :_destroy,
|
||||
{ release_assets_attributes: [ :id, :kind, :path, :_destroy ] } ]
|
||||
|
||||
menu priority: 2, label: "🎮 Softwares"
|
||||
|
||||
@@ -126,11 +127,11 @@ ActiveAdmin.register Software do
|
||||
f.inputs "Releases" do
|
||||
f.has_many :releases, allow_destroy: true, new_record: true do |r|
|
||||
r.input :version
|
||||
r.input :html_folder_path
|
||||
r.input :cartridge_path
|
||||
r.input :source_path
|
||||
r.input :docs_folder_path
|
||||
r.input :web_playable
|
||||
r.has_many :release_assets, allow_destroy: true, new_record: true do |ra|
|
||||
ra.input :kind, as: :select, collection: ReleaseAsset::KINDS, include_blank: false
|
||||
ra.input :path
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -140,7 +141,7 @@ ActiveAdmin.register Software do
|
||||
|
||||
controller do
|
||||
def scoped_collection
|
||||
super.includes(:releases, software_images: :image)
|
||||
super.includes({ releases: :release_assets }, software_images: :image)
|
||||
end
|
||||
|
||||
def find_resource
|
||||
|
||||
@@ -1,25 +1,23 @@
|
||||
//= require active_admin/base
|
||||
|
||||
(function() {
|
||||
var PATH_FIELDS = ['html_folder_path', 'cartridge_path', 'source_path', 'docs_folder_path'];
|
||||
var PATH_INPUT_SELECTOR = 'input[id*="release_assets_attributes"][id$="_path"]:not([data-browse-added])';
|
||||
var _pickerTarget = null;
|
||||
|
||||
function addBrowseButtons() {
|
||||
PATH_FIELDS.forEach(function(fieldName) {
|
||||
var inputs = document.querySelectorAll('input[id*="' + fieldName + '"]:not([data-browse-added])');
|
||||
inputs.forEach(function(input) {
|
||||
input.setAttribute('data-browse-added', 'true');
|
||||
var btn = document.createElement('a');
|
||||
btn.href = '#';
|
||||
btn.textContent = 'Browse';
|
||||
btn.className = 'fm-browse-btn';
|
||||
btn.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
_pickerTarget = input;
|
||||
openPicker('');
|
||||
});
|
||||
input.parentNode.insertBefore(btn, input.nextSibling);
|
||||
var inputs = document.querySelectorAll(PATH_INPUT_SELECTOR);
|
||||
inputs.forEach(function(input) {
|
||||
input.setAttribute('data-browse-added', 'true');
|
||||
var btn = document.createElement('a');
|
||||
btn.href = '#';
|
||||
btn.textContent = 'Browse';
|
||||
btn.className = 'fm-browse-btn';
|
||||
btn.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
_pickerTarget = input;
|
||||
openPicker('');
|
||||
});
|
||||
input.parentNode.insertBefore(btn, input.nextSibling);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,9 @@ class Release < ApplicationRecord
|
||||
|
||||
belongs_to :software
|
||||
has_many :downloads
|
||||
has_many :release_assets
|
||||
|
||||
accepts_nested_attributes_for :release_assets, allow_destroy: true
|
||||
|
||||
validates :version, presence: true
|
||||
validates :version, uniqueness: { scope: :software_id }
|
||||
@@ -14,16 +17,17 @@ class Release < ApplicationRecord
|
||||
def c64_cannot_be_web_playable
|
||||
return unless software&.platform == "c64"
|
||||
|
||||
if html_folder_path.present? || web_playable?
|
||||
has_html = release_assets.reject(&:marked_for_destruction?).any? { |a| a.kind == "html" }
|
||||
if has_html || web_playable?
|
||||
errors.add(:base, "C64 releases cannot be web playable (download only)")
|
||||
end
|
||||
end
|
||||
|
||||
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]
|
||||
%w[created_at deleted_at id software_id updated_at version web_playable]
|
||||
end
|
||||
|
||||
def self.ransackable_associations(auth_object = nil)
|
||||
%w[downloads software]
|
||||
%w[downloads release_assets software]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
class ReleaseAsset < ApplicationRecord
|
||||
KINDS = %w[cartridge source html docs
|
||||
win_x86 win_x64 linux_x86 linux_x64
|
||||
mac_x64 mac_arm64 mac_universal].freeze
|
||||
|
||||
belongs_to :release
|
||||
|
||||
enum :kind, KINDS.index_by(&:itself)
|
||||
|
||||
validates :path, presence: true
|
||||
validates :kind, uniqueness: { scope: :release_id }
|
||||
|
||||
default_scope { where(deleted_at: nil) }
|
||||
|
||||
def self.ransackable_attributes(auth_object = nil)
|
||||
%w[created_at deleted_at id kind path release_id updated_at]
|
||||
end
|
||||
|
||||
def self.ransackable_associations(auth_object = nil)
|
||||
%w[release]
|
||||
end
|
||||
end
|
||||
@@ -4,15 +4,27 @@ class ReleaseSerializer < Blueprinter::Base
|
||||
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| r.cartridge_path.blank? ? "" : r.cartridge_path.gsub(FILE_PATH_FROM, FILE_PATH_TO) }
|
||||
field(:sourcePath) { |r| r.source_path.blank? ? "" : r.source_path.gsub(FILE_PATH_FROM, FILE_PATH_TO) }
|
||||
field(:htmlFolderPath) { |r| r.html_folder_path.blank? ? "" : r.html_folder_path.gsub(FILE_PATH_FROM, FILE_PATH_TO) }
|
||||
field(:docsFolderPath) { |r| r.docs_folder_path.blank? ? "" : r.docs_folder_path.gsub(FILE_PATH_FROM, FILE_PATH_TO) }
|
||||
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) { |r| r.association(:downloads).loaded? ? r.downloads.size : r.downloads.count }
|
||||
end
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
module SoftwareUpdater
|
||||
module BinaryAttachment
|
||||
# fájlnév slug → ReleaseAsset kind
|
||||
TARGET_KINDS = {
|
||||
"win-x86" => "win_x86",
|
||||
"win-x64" => "win_x64",
|
||||
"linux-x86" => "linux_x86",
|
||||
"linux-x64" => "linux_x64",
|
||||
"mac-x64" => "mac_x64",
|
||||
"mac-arm64" => "mac_arm64",
|
||||
"mac-universal" => "mac_universal"
|
||||
}.freeze
|
||||
|
||||
private
|
||||
|
||||
def binary_asset_paths(versioned)
|
||||
TARGET_KINDS.each_with_object({}) do |(slug, kind), assets|
|
||||
filename = "#{versioned}-#{slug}.zip"
|
||||
assets[kind] = full_path(filename) if File.file?(full_path(filename))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -23,5 +23,14 @@ module SoftwareUpdater
|
||||
|
||||
Release.create!(attrs)
|
||||
end
|
||||
|
||||
def sync_release_assets(release, kind_paths)
|
||||
kind_paths.each do |kind, path|
|
||||
asset = ReleaseAsset.unscoped.find_or_initialize_by(release_id: release.id, kind: kind)
|
||||
asset.path = path
|
||||
asset.deleted_at = nil
|
||||
asset.save!
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,6 +4,7 @@ module SoftwareUpdater
|
||||
include ArchiveExtraction
|
||||
include MetadataParsing
|
||||
include SoftwarePersistence
|
||||
include BinaryAttachment
|
||||
|
||||
class_methods do
|
||||
def platform(value = nil)
|
||||
@@ -25,9 +26,9 @@ module SoftwareUpdater
|
||||
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?
|
||||
|
||||
create_release_if_not_exists(
|
||||
{ software_id: software.id, version: version }.merge(release_paths(versioned))
|
||||
)
|
||||
release = create_release_if_not_exists(software_id: software.id, version: version)
|
||||
sync_release_assets(release, asset_paths(versioned).merge(binary_asset_paths(versioned)))
|
||||
release
|
||||
end
|
||||
end
|
||||
|
||||
@@ -41,8 +42,8 @@ module SoftwareUpdater
|
||||
parse_json_metadata(full_path("#{versioned}.metadata.json"))
|
||||
end
|
||||
|
||||
def release_paths(versioned)
|
||||
{ html_folder_path: full_path(versioned) }
|
||||
def asset_paths(versioned)
|
||||
{ "html" => full_path(versioned) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
class DownloadService
|
||||
BASE_PATH = Pathname.new(ENV.fetch("FILE_CONTAINER_PATH", "/softwares")).realpath
|
||||
CONTAINER_BASE = ENV.fetch("FILE_CONTAINER_PATH", "/softwares")
|
||||
BASE_PATH = Pathname.new(CONTAINER_BASE).realpath
|
||||
|
||||
def call(path:, ip:, user_agent:, referer:)
|
||||
sanitized = path.to_s
|
||||
@@ -8,12 +9,12 @@ class DownloadService
|
||||
return nil unless File.file?(full_path)
|
||||
|
||||
escaped = sanitized.gsub("%", "\\%").gsub("_", "\\_")
|
||||
release = Release.find_by("cartridge_path LIKE ? OR source_path LIKE ?",
|
||||
"%#{escaped}%", "%#{escaped}%")
|
||||
asset = ReleaseAsset.find_by(path: File.join(CONTAINER_BASE, sanitized)) ||
|
||||
ReleaseAsset.where("path LIKE ?", "%#{escaped}%").first
|
||||
|
||||
Download.create!(
|
||||
file_path: sanitized,
|
||||
release: release,
|
||||
release: asset&.release,
|
||||
ip_address: ip,
|
||||
user_agent: user_agent&.truncate(500),
|
||||
referer: referer&.truncate(500)
|
||||
|
||||
@@ -8,7 +8,7 @@ class SoftwareHighlightedService
|
||||
.first
|
||||
return nil unless software
|
||||
|
||||
releases = Release.includes(:downloads).where(software_id: software.id).to_a
|
||||
releases = Release.includes(:downloads, :release_assets).where(software_id: software.id).to_a
|
||||
build_response(software, releases)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,7 +4,7 @@ module SoftwareResponseBuilder
|
||||
def build_response(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? }
|
||||
web_playable = sorted.find { |r| r.release_assets.any? { |a| a.kind == "html" } }
|
||||
total_downloads = releases.sum { |r| r.association(:downloads).loaded? ? r.downloads.size : 0 }
|
||||
|
||||
SoftwareDetailSerializer.render_as_hash(software,
|
||||
|
||||
@@ -2,7 +2,7 @@ class SoftwareService
|
||||
include SoftwareResponseBuilder
|
||||
|
||||
def index
|
||||
softwares = Software.includes(releases: :downloads).includes(:external_links, :software_images).all
|
||||
softwares = Software.includes(releases: [ :downloads, :release_assets ]).includes(:external_links, :software_images).all
|
||||
{ softwares: softwares.map { |sw| build_response(sw, sw.releases.to_a) } }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -8,8 +8,8 @@ module SoftwareUpdater
|
||||
# a c64 buildhez nem tartozik html zip, csak a .prg fájl
|
||||
end
|
||||
|
||||
def release_paths(versioned)
|
||||
{ cartridge_path: full_path("#{versioned}.prg") }
|
||||
def asset_paths(versioned)
|
||||
{ "cartridge" => full_path("#{versioned}.prg") }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -13,12 +13,12 @@ module SoftwareUpdater
|
||||
parse_lua_metadata(full_path("#{versioned}.lua"))
|
||||
end
|
||||
|
||||
def release_paths(versioned)
|
||||
def asset_paths(versioned)
|
||||
{
|
||||
cartridge_path: full_path("#{versioned}.tic"),
|
||||
source_path: full_path("#{versioned}.lua"),
|
||||
html_folder_path: full_path(versioned),
|
||||
docs_folder_path: full_path("#{versioned}-docs")
|
||||
"cartridge" => full_path("#{versioned}.tic"),
|
||||
"source" => full_path("#{versioned}.lua"),
|
||||
"html" => full_path(versioned),
|
||||
"docs" => full_path("#{versioned}-docs")
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
Reference in New Issue
Block a user