WarpEngine 0.7.0: a képtár és a CI is a hoszté, adapteren keresztül
Két dolog volt beépítve az engine-be, ami nem az övé. A **képtár** eddig `WarpEngine::Image` volt, pedig a modell teljesen általános: a teletypegames-ben a tagok arcképét is ez hordozza, nem csak a katalógus borítóit. Az `Image` modell, a feltöltött fájlok, az `/api/image/:id` végpont és az admin oldal ezért átkerült a hosztba, az engine pedig adapteren szól hozzá (`WarpEngine::Images`): `url_for` adja a katalógus JSON `imageUrl`-jét, `select_options` a software-form képválasztóját, `build_from_upload` a "tölts fel új képet" ágat. Az alapértelmezés az `Image` osztály, tehát a default útvonal bitre a régi. A `SoftwareImage` (a katalógus-kapcsolat) maradt az engine-ben, és **az `images` tábla nem mozdult**: az engine csak abbahagyta a létrehozását, a generátor írja meg hoszt-kódként. A **CI** eddig végig Woodpecker volt: kliens, aláírás-ellenőrzés, pipeline-receptek, repo-szinkron, secret-kiosztás. Mindez egy adapter mögé került (`WarpEngine.ci`), a Woodpecker-implementáció pedig az engine-ben maradt `WarpEngine::CI::Woodpecker` néven — kliens, adapter, httpsig-ellenőrző és a platformonkénti pipeline-receptek, mert a YAML-dialektus a szolgáltatóé. Az engine saját kódja már nem nevez szolgáltatót: `CI::Repo` és `CI::Run` értékeket kap, `CI::ConnectionError`/`ApiError`/`NotConfigured` hibákat dob, a `Pipeline` pedig `remote_repo_id`-t ad a történelmi `woodpecker_repo_id` kolumna fölött (a tábla itt sem mozdult). `c.ci_adapter = :none` azt jelenti, hogy ez a hoszt nem buildel: az `/api/ci/*` 503, a `/build/config` elutasít, az admin akciók elbújnak. Mindkét seam a hoszt initializerében van kimondva, nem alapértelmezésre hagyva — a hoszt megnevezi, mi a képtára és mi a CI-ja. Törés a 0.6-hoz képest: `image_container_path`, `image_owners`, `ci_platforms`, `ci_update_server`, `ci_extension_public_key(_url)`, `woodpecker_url`, `woodpecker_api_token`, `woodpecker_repo_owner` és a `WarpEngine.woodpecker_configured?` megszűnt; a helyük `c.image_class_name` / `c.image_adapter` és `c.ci_adapter`. Az `/api/ci/*` `latest_run`/`trigger` válasza a normalizált `CI::Run` alakot adja (number, status, branch, message, createdAt, url), a `pipelines` lista pedig `repo_id`-t is közöl a megtartott `woodpecker_repo_id` mellett. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
ActiveAdmin.register Image do
|
||||
permit_params :file_upload
|
||||
|
||||
menu parent: "🌀 WarpEngine", priority: 5, label: "🖼️ Images"
|
||||
|
||||
used_ids = -> {
|
||||
WarpEngine::SoftwareImage.unscope(:order).distinct.pluck(:image_id) +
|
||||
Member.where.not(image_id: nil).distinct.pluck(:image_id)
|
||||
}
|
||||
|
||||
scope :all, default: true
|
||||
scope("In use") { |scope| scope.where(id: used_ids.call) }
|
||||
scope("Orphan") { |scope| scope.where.not(id: used_ids.call) }
|
||||
|
||||
batch_action :delete_orphans, confirm: "Delete all selected orphan images and their files?" do |ids|
|
||||
orphan_ids = ids.map(&:to_i) - used_ids.call
|
||||
Image.where(id: orphan_ids).find_each do |img|
|
||||
File.delete(img.file_path) if File.exist?(img.file_path)
|
||||
img.destroy
|
||||
end
|
||||
redirect_to admin_images_path, notice: "Deleted #{orphan_ids.size} orphan image(s)."
|
||||
end
|
||||
|
||||
index do
|
||||
selectable_column
|
||||
id_column
|
||||
column :original_filename
|
||||
column :content_type
|
||||
column(:preview) do |img|
|
||||
if File.exist?(img.file_path)
|
||||
image_tag("/api/image/#{img.id}", style: "max-height:60px;max-width:120px;object-fit:contain;")
|
||||
end
|
||||
end
|
||||
column(:usage) do |img|
|
||||
uses = []
|
||||
uses << "#{img.software_images.size} software(s)" if img.software_images.any?
|
||||
uses << "member" if img.members.any?
|
||||
uses.any? ? uses.join(", ") : status_tag("orphan", class: "warning")
|
||||
end
|
||||
column :created_at
|
||||
actions
|
||||
end
|
||||
|
||||
filter :original_filename
|
||||
filter :content_type
|
||||
|
||||
show do
|
||||
attributes_table do
|
||||
row :id
|
||||
row :original_filename
|
||||
row :filename
|
||||
row :content_type
|
||||
row(:preview) do |img|
|
||||
if File.exist?(img.file_path)
|
||||
image_tag("/api/image/#{img.id}", style: "max-height:300px;max-width:100%;object-fit:contain;")
|
||||
else
|
||||
"File not found on disk"
|
||||
end
|
||||
end
|
||||
row :created_at
|
||||
row :updated_at
|
||||
end
|
||||
end
|
||||
|
||||
form html: { multipart: true } do |f|
|
||||
f.inputs do
|
||||
f.input :file_upload, as: :file, label: "Image File"
|
||||
end
|
||||
f.actions
|
||||
end
|
||||
|
||||
controller do
|
||||
def scoped_collection
|
||||
super.includes(:software_images, :members)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -41,7 +41,7 @@ ActiveAdmin.register Member do
|
||||
f.template.image_tag("/api/image/#{resource.image_id}", style: "max-height:80px;max-width:160px;object-fit:contain;margin-top:6px;").html_safe
|
||||
end
|
||||
f.input :image_id, as: :select, label: "Image",
|
||||
collection: WarpEngine::Image.order(:original_filename).map { |img| [ img.original_filename, img.id ] },
|
||||
collection: Image.order(:original_filename).map { |img| [ img.original_filename, img.id ] },
|
||||
include_blank: "— no image (use avatar_filename) —",
|
||||
hint: img_hint
|
||||
end
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
class Api::ImagesController < ApiController
|
||||
resource_description do
|
||||
short "Images"
|
||||
formats [ "binary" ]
|
||||
end
|
||||
|
||||
api :GET, "/api/image/:id", "Get image by ID"
|
||||
param :id, :number, required: true, desc: "Image ID"
|
||||
returns code: 200, desc: "Image binary data"
|
||||
error code: 404, desc: "Image not found"
|
||||
def show
|
||||
image = Image.find(params[:id])
|
||||
send_file image.file_path, type: image.content_type, disposition: "inline"
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,33 @@
|
||||
class Image < ApplicationRecord
|
||||
def self.upload_path
|
||||
ENV.fetch("IMAGE_CONTAINER_PATH", "/images")
|
||||
end
|
||||
|
||||
has_many :software_images, class_name: "WarpEngine::SoftwareImage", dependent: :restrict_with_error
|
||||
has_many :members
|
||||
|
||||
default_scope { where(deleted_at: nil) }
|
||||
|
||||
attr_accessor :file_upload
|
||||
|
||||
before_save :process_upload, if: -> { file_upload.present? }
|
||||
|
||||
def self.ransackable_attributes(auth_object = nil)
|
||||
%w[content_type created_at deleted_at filename id original_filename updated_at]
|
||||
end
|
||||
|
||||
def file_path
|
||||
File.join(self.class.upload_path, filename.to_s)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def process_upload
|
||||
FileUtils.mkdir_p(self.class.upload_path)
|
||||
self.original_filename = file_upload.original_filename
|
||||
self.content_type = file_upload.content_type.presence || "application/octet-stream"
|
||||
ext = File.extname(file_upload.original_filename)
|
||||
self.filename = "#{SecureRandom.uuid}#{ext}"
|
||||
IO.copy_stream(file_upload.to_io, file_path)
|
||||
end
|
||||
end
|
||||
@@ -1,5 +1,5 @@
|
||||
class Member < ApplicationRecord
|
||||
belongs_to :image, class_name: "WarpEngine::Image", optional: true
|
||||
belongs_to :image, optional: true
|
||||
has_one :admin_user
|
||||
|
||||
validates :nick, presence: true, uniqueness: true
|
||||
|
||||
Reference in New Issue
Block a user