replace catalog service to api
This commit is contained in:
@@ -2,7 +2,10 @@
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(go build *)",
|
||||
"Bash(go vet *)"
|
||||
"Bash(go vet *)",
|
||||
"Bash(chmod +x *)",
|
||||
"Bash(xargs ruby *)",
|
||||
"Bash(docker compose exec *)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,3 +5,5 @@ teletype_softwares
|
||||
pma-config.user.inc.php
|
||||
data
|
||||
apps/frontend/src/events.yaml
|
||||
NOTES_*
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
.bundle/
|
||||
log/
|
||||
tmp/
|
||||
*.sqlite3
|
||||
.env
|
||||
@@ -0,0 +1,24 @@
|
||||
FROM --platform=linux/amd64 ruby:3.3-slim
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
default-libmysqlclient-dev \
|
||||
git \
|
||||
tzdata \
|
||||
unzip \
|
||||
nodejs \
|
||||
libffi-dev \
|
||||
libyaml-dev \
|
||||
pkg-config \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY Gemfile ./
|
||||
RUN bundle install
|
||||
|
||||
COPY . .
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["/bin/sh", "entrypoint.sh"]
|
||||
@@ -0,0 +1,16 @@
|
||||
source "https://rubygems.org"
|
||||
|
||||
gem "rails", "~> 8.0"
|
||||
gem "mysql2", "~> 0.5"
|
||||
gem "puma", ">= 5.0"
|
||||
gem "rubyzip", "~> 2.3"
|
||||
gem "bootsnap", require: false
|
||||
|
||||
gem "activeadmin"
|
||||
gem "devise"
|
||||
gem "sprockets-rails"
|
||||
gem "sassc-rails"
|
||||
|
||||
group :development, :test do
|
||||
gem "debug", platforms: %i[mri windows]
|
||||
end
|
||||
@@ -0,0 +1,298 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
action_text-trix (2.1.18)
|
||||
railties
|
||||
actioncable (8.1.3)
|
||||
actionpack (= 8.1.3)
|
||||
activesupport (= 8.1.3)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
zeitwerk (~> 2.6)
|
||||
actionmailbox (8.1.3)
|
||||
actionpack (= 8.1.3)
|
||||
activejob (= 8.1.3)
|
||||
activerecord (= 8.1.3)
|
||||
activestorage (= 8.1.3)
|
||||
activesupport (= 8.1.3)
|
||||
mail (>= 2.8.0)
|
||||
actionmailer (8.1.3)
|
||||
actionpack (= 8.1.3)
|
||||
actionview (= 8.1.3)
|
||||
activejob (= 8.1.3)
|
||||
activesupport (= 8.1.3)
|
||||
mail (>= 2.8.0)
|
||||
rails-dom-testing (~> 2.2)
|
||||
actionpack (8.1.3)
|
||||
actionview (= 8.1.3)
|
||||
activesupport (= 8.1.3)
|
||||
nokogiri (>= 1.8.5)
|
||||
rack (>= 2.2.4)
|
||||
rack-session (>= 1.0.1)
|
||||
rack-test (>= 0.6.3)
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
useragent (~> 0.16)
|
||||
actiontext (8.1.3)
|
||||
action_text-trix (~> 2.1.15)
|
||||
actionpack (= 8.1.3)
|
||||
activerecord (= 8.1.3)
|
||||
activestorage (= 8.1.3)
|
||||
activesupport (= 8.1.3)
|
||||
globalid (>= 0.6.0)
|
||||
nokogiri (>= 1.8.5)
|
||||
actionview (8.1.3)
|
||||
activesupport (= 8.1.3)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.11)
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
activeadmin (3.5.1)
|
||||
arbre (~> 1.2, >= 1.2.1)
|
||||
csv
|
||||
formtastic (>= 3.1)
|
||||
formtastic_i18n (>= 0.4)
|
||||
inherited_resources (~> 1.7)
|
||||
jquery-rails (>= 4.2)
|
||||
kaminari (>= 1.2.1)
|
||||
railties (>= 6.1)
|
||||
ransack (>= 4.0)
|
||||
activejob (8.1.3)
|
||||
activesupport (= 8.1.3)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (8.1.3)
|
||||
activesupport (= 8.1.3)
|
||||
activerecord (8.1.3)
|
||||
activemodel (= 8.1.3)
|
||||
activesupport (= 8.1.3)
|
||||
timeout (>= 0.4.0)
|
||||
activestorage (8.1.3)
|
||||
actionpack (= 8.1.3)
|
||||
activejob (= 8.1.3)
|
||||
activerecord (= 8.1.3)
|
||||
activesupport (= 8.1.3)
|
||||
marcel (~> 1.0)
|
||||
activesupport (8.1.3)
|
||||
base64
|
||||
bigdecimal
|
||||
concurrent-ruby (~> 1.0, >= 1.3.1)
|
||||
connection_pool (>= 2.2.5)
|
||||
drb
|
||||
i18n (>= 1.6, < 2)
|
||||
json
|
||||
logger (>= 1.4.2)
|
||||
minitest (>= 5.1)
|
||||
securerandom (>= 0.3)
|
||||
tzinfo (~> 2.0, >= 2.0.5)
|
||||
uri (>= 0.13.1)
|
||||
arbre (1.7.0)
|
||||
activesupport (>= 3.0.0)
|
||||
ruby2_keywords (>= 0.0.2)
|
||||
base64 (0.3.0)
|
||||
bcrypt (3.1.22)
|
||||
bigdecimal (4.1.2)
|
||||
bootsnap (1.24.3)
|
||||
msgpack (~> 1.2)
|
||||
builder (3.3.0)
|
||||
concurrent-ruby (1.3.6)
|
||||
connection_pool (3.0.2)
|
||||
crass (1.0.6)
|
||||
csv (3.3.5)
|
||||
date (3.5.1)
|
||||
debug (1.11.1)
|
||||
irb (~> 1.10)
|
||||
reline (>= 0.3.8)
|
||||
devise (5.0.3)
|
||||
bcrypt (~> 3.0)
|
||||
orm_adapter (~> 0.1)
|
||||
railties (>= 7.0)
|
||||
responders
|
||||
warden (~> 1.2.3)
|
||||
drb (2.2.3)
|
||||
erb (6.0.4)
|
||||
erubi (1.13.1)
|
||||
ffi (1.17.4-x86_64-linux-gnu)
|
||||
formtastic (6.0.0)
|
||||
actionpack (>= 7.2.0)
|
||||
formtastic_i18n (0.7.0)
|
||||
globalid (1.3.0)
|
||||
activesupport (>= 6.1)
|
||||
has_scope (0.9.0)
|
||||
actionpack (>= 7.0)
|
||||
activesupport (>= 7.0)
|
||||
i18n (1.14.8)
|
||||
concurrent-ruby (~> 1.0)
|
||||
inherited_resources (1.14.0)
|
||||
actionpack (>= 6.0)
|
||||
has_scope (>= 0.6)
|
||||
railties (>= 6.0)
|
||||
responders (>= 2)
|
||||
io-console (0.8.2)
|
||||
irb (1.18.0)
|
||||
pp (>= 0.6.0)
|
||||
prism (>= 1.3.0)
|
||||
rdoc (>= 4.0.0)
|
||||
reline (>= 0.4.2)
|
||||
jquery-rails (4.6.1)
|
||||
rails-dom-testing (>= 1, < 3)
|
||||
railties (>= 4.2.0)
|
||||
thor (>= 0.14, < 2.0)
|
||||
json (2.19.5)
|
||||
kaminari (1.2.2)
|
||||
activesupport (>= 4.1.0)
|
||||
kaminari-actionview (= 1.2.2)
|
||||
kaminari-activerecord (= 1.2.2)
|
||||
kaminari-core (= 1.2.2)
|
||||
kaminari-actionview (1.2.2)
|
||||
actionview
|
||||
kaminari-core (= 1.2.2)
|
||||
kaminari-activerecord (1.2.2)
|
||||
activerecord
|
||||
kaminari-core (= 1.2.2)
|
||||
kaminari-core (1.2.2)
|
||||
logger (1.7.0)
|
||||
loofah (2.25.1)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.12.0)
|
||||
mail (2.9.0)
|
||||
logger
|
||||
mini_mime (>= 0.1.1)
|
||||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
marcel (1.1.0)
|
||||
mini_mime (1.1.5)
|
||||
minitest (6.0.6)
|
||||
drb (~> 2.0)
|
||||
prism (~> 1.5)
|
||||
msgpack (1.8.0)
|
||||
mysql2 (0.5.7)
|
||||
bigdecimal
|
||||
net-imap (0.6.4)
|
||||
date
|
||||
net-protocol
|
||||
net-pop (0.1.2)
|
||||
net-protocol
|
||||
net-protocol (0.2.2)
|
||||
timeout
|
||||
net-smtp (0.5.1)
|
||||
net-protocol
|
||||
nio4r (2.7.5)
|
||||
nokogiri (1.19.3-x86_64-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
orm_adapter (0.5.0)
|
||||
pp (0.6.3)
|
||||
prettyprint
|
||||
prettyprint (0.2.0)
|
||||
prism (1.9.0)
|
||||
psych (5.3.1)
|
||||
date
|
||||
stringio
|
||||
puma (8.0.1)
|
||||
nio4r (~> 2.0)
|
||||
racc (1.8.1)
|
||||
rack (3.2.6)
|
||||
rack-session (2.1.2)
|
||||
base64 (>= 0.1.0)
|
||||
rack (>= 3.0.0)
|
||||
rack-test (2.2.0)
|
||||
rack (>= 1.3)
|
||||
rackup (2.3.1)
|
||||
rack (>= 3)
|
||||
rails (8.1.3)
|
||||
actioncable (= 8.1.3)
|
||||
actionmailbox (= 8.1.3)
|
||||
actionmailer (= 8.1.3)
|
||||
actionpack (= 8.1.3)
|
||||
actiontext (= 8.1.3)
|
||||
actionview (= 8.1.3)
|
||||
activejob (= 8.1.3)
|
||||
activemodel (= 8.1.3)
|
||||
activerecord (= 8.1.3)
|
||||
activestorage (= 8.1.3)
|
||||
activesupport (= 8.1.3)
|
||||
bundler (>= 1.15.0)
|
||||
railties (= 8.1.3)
|
||||
rails-dom-testing (2.3.0)
|
||||
activesupport (>= 5.0.0)
|
||||
minitest
|
||||
nokogiri (>= 1.6)
|
||||
rails-html-sanitizer (1.7.0)
|
||||
loofah (~> 2.25)
|
||||
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
|
||||
railties (8.1.3)
|
||||
actionpack (= 8.1.3)
|
||||
activesupport (= 8.1.3)
|
||||
irb (~> 1.13)
|
||||
rackup (>= 1.0.0)
|
||||
rake (>= 12.2)
|
||||
thor (~> 1.0, >= 1.2.2)
|
||||
tsort (>= 0.2)
|
||||
zeitwerk (~> 2.6)
|
||||
rake (13.4.2)
|
||||
ransack (4.4.1)
|
||||
activerecord (>= 7.2)
|
||||
activesupport (>= 7.2)
|
||||
i18n
|
||||
rdoc (7.2.0)
|
||||
erb
|
||||
psych (>= 4.0.0)
|
||||
tsort
|
||||
reline (0.6.3)
|
||||
io-console (~> 0.5)
|
||||
responders (3.2.0)
|
||||
actionpack (>= 7.0)
|
||||
railties (>= 7.0)
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyzip (2.4.1)
|
||||
sassc (2.4.0)
|
||||
ffi (~> 1.9)
|
||||
sassc-rails (2.1.2)
|
||||
railties (>= 4.0.0)
|
||||
sassc (>= 2.0)
|
||||
sprockets (> 3.0)
|
||||
sprockets-rails
|
||||
tilt
|
||||
securerandom (0.4.1)
|
||||
sprockets (4.2.2)
|
||||
concurrent-ruby (~> 1.0)
|
||||
logger
|
||||
rack (>= 2.2.4, < 4)
|
||||
sprockets-rails (3.5.2)
|
||||
actionpack (>= 6.1)
|
||||
activesupport (>= 6.1)
|
||||
sprockets (>= 3.0.0)
|
||||
stringio (3.2.0)
|
||||
thor (1.5.0)
|
||||
tilt (2.7.0)
|
||||
timeout (0.6.1)
|
||||
tsort (0.2.0)
|
||||
tzinfo (2.0.6)
|
||||
concurrent-ruby (~> 1.0)
|
||||
uri (1.1.1)
|
||||
useragent (0.16.11)
|
||||
warden (1.2.9)
|
||||
rack (>= 2.0.9)
|
||||
websocket-driver (0.8.0)
|
||||
base64
|
||||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.5)
|
||||
zeitwerk (2.7.5)
|
||||
|
||||
PLATFORMS
|
||||
x86_64-linux-gnu
|
||||
|
||||
DEPENDENCIES
|
||||
activeadmin
|
||||
bootsnap
|
||||
debug
|
||||
devise
|
||||
mysql2 (~> 0.5)
|
||||
puma (>= 5.0)
|
||||
rails (~> 8.0)
|
||||
rubyzip (~> 2.3)
|
||||
sassc-rails
|
||||
sprockets-rails
|
||||
|
||||
BUNDLED WITH
|
||||
2.5.22
|
||||
@@ -0,0 +1,2 @@
|
||||
require_relative "config/application"
|
||||
Rails.application.load_tasks
|
||||
@@ -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
|
||||
Executable
+81
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env ruby
|
||||
# frozen_string_literal: true
|
||||
|
||||
#
|
||||
# This file was generated by Bundler.
|
||||
#
|
||||
# The application's executable is in bin/bundle.
|
||||
#
|
||||
|
||||
require "rubygems"
|
||||
|
||||
m = Module.new do
|
||||
module_function
|
||||
|
||||
def invoked_as_script?
|
||||
File.expand_path($0) == File.expand_path(__FILE__)
|
||||
end
|
||||
|
||||
def env_var_version
|
||||
ENV["BUNDLER_VERSION"]
|
||||
end
|
||||
|
||||
def cli_arg_version
|
||||
return unless invoked_as_script?
|
||||
return unless "--version" == ARGV.first
|
||||
bundler_version = nil
|
||||
update_index = nil
|
||||
ARGV.each_with_index do |a, i|
|
||||
bundler_version = a if update_index && update_index.succ == i
|
||||
next unless a =~ /\A--bundler\z/
|
||||
update_index = i
|
||||
end
|
||||
bundler_version
|
||||
end
|
||||
|
||||
def gemfile
|
||||
gemfile = ENV["BUNDLE_GEMFILE"]
|
||||
return gemfile if gemfile && !gemfile.empty?
|
||||
File.expand_path("../Gemfile", __dir__)
|
||||
end
|
||||
|
||||
def lockfile
|
||||
lockfile = "#{gemfile}.lock"
|
||||
return lockfile if File.exist?(lockfile)
|
||||
nil
|
||||
end
|
||||
|
||||
def bundler_version
|
||||
@bundler_version ||=
|
||||
env_var_version || cli_arg_version ||
|
||||
lockfile_version
|
||||
end
|
||||
|
||||
def lockfile_version
|
||||
return unless lockfile
|
||||
lockfile_contents = File.read(lockfile)
|
||||
return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s*(#{Gem::Version::VERSION_PATTERN})\n/
|
||||
Regexp.last_match(1)
|
||||
end
|
||||
|
||||
def load_bundler!
|
||||
$LOAD_PATH.unshift(bundler_spec.lib_dirs_glob) if bundler_spec
|
||||
require "bundler"
|
||||
unless bundler_spec
|
||||
Gem::GemRunner.new.run(["install", "bundler", "--version", bundler_version]) if bundler_version
|
||||
require "bundler"
|
||||
end
|
||||
end
|
||||
|
||||
def bundler_spec
|
||||
@bundler_spec ||= Gem::Specification.find_by_name("bundler", bundler_version)
|
||||
rescue Gem::MissingSpecVersionError, Gem::MissingSpecError
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
m.load_bundler!
|
||||
|
||||
if m.invoked_as_script?
|
||||
load Gem.bin_path("bundler", "bundle")
|
||||
end
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env ruby
|
||||
APP_PATH = File.expand_path("../config/application", __dir__)
|
||||
require_relative "../config/boot"
|
||||
require "rails/commands"
|
||||
@@ -0,0 +1,3 @@
|
||||
require_relative "config/environment"
|
||||
run Rails.application
|
||||
Rails.application.load_server
|
||||
@@ -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"
|
||||
require "sprockets/railtie"
|
||||
|
||||
Bundler.require(*Rails.groups)
|
||||
|
||||
module Api
|
||||
class Application < Rails::Application
|
||||
config.load_defaults 8.0
|
||||
|
||||
config.time_zone = "UTC"
|
||||
config.active_record.default_timezone = :utc
|
||||
|
||||
config.assets.prefix = "/admin/assets"
|
||||
|
||||
config.autoload_lib(ignore: %w[assets tasks])
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,4 @@
|
||||
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
|
||||
|
||||
require 'bundler/setup'
|
||||
require 'bootsnap/setup'
|
||||
@@ -0,0 +1,16 @@
|
||||
default: &default
|
||||
adapter: mysql2
|
||||
encoding: utf8mb4
|
||||
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
|
||||
username: <%= ENV.fetch("DB_USER", "root") %>
|
||||
password: <%= ENV.fetch("DB_PASSWORD", "") %>
|
||||
host: <%= ENV.fetch("DB_HOST", "mysql") %>
|
||||
port: <%= ENV.fetch("DB_PORT", "3306") %>
|
||||
|
||||
development:
|
||||
<<: *default
|
||||
database: <%= ENV.fetch("DB_NAME", "softwares") %>
|
||||
|
||||
production:
|
||||
<<: *default
|
||||
database: <%= ENV.fetch("DB_NAME", "softwares") %>
|
||||
@@ -0,0 +1,3 @@
|
||||
require_relative "application"
|
||||
|
||||
Rails.application.initialize!
|
||||
@@ -0,0 +1,14 @@
|
||||
Rails.application.configure do
|
||||
config.eager_load = false
|
||||
config.consider_all_requests_local = true
|
||||
config.server_timing = true
|
||||
|
||||
config.cache_classes = false
|
||||
config.action_controller.perform_caching = false
|
||||
|
||||
config.active_record.migration_error = :page_load
|
||||
config.active_record.verbose_query_logs = true
|
||||
|
||||
config.log_level = :debug
|
||||
config.log_tags = [:request_id]
|
||||
end
|
||||
@@ -0,0 +1,7 @@
|
||||
Rails.application.configure do
|
||||
config.eager_load = true
|
||||
config.consider_all_requests_local = false
|
||||
config.log_level = :info
|
||||
config.log_tags = [:request_id]
|
||||
config.active_record.verbose_query_logs = false
|
||||
end
|
||||
@@ -0,0 +1,14 @@
|
||||
ActiveAdmin.setup do |config|
|
||||
config.site_title = "Teletype Games Admin"
|
||||
|
||||
config.authentication_method = :authenticate_admin_user!
|
||||
config.current_user_method = :current_admin_user
|
||||
config.logout_link_path = :destroy_admin_user_session_path
|
||||
config.logout_link_method = :delete
|
||||
|
||||
config.comments = false
|
||||
config.batch_actions = true
|
||||
config.filter_attributes = [:password, :password_confirmation, :current_password, :encrypted_password]
|
||||
|
||||
config.localize_format = :long
|
||||
end
|
||||
@@ -0,0 +1,16 @@
|
||||
Devise.setup do |config|
|
||||
config.mailer_sender = "noreply@teletype.hu"
|
||||
|
||||
require "devise/orm/active_record"
|
||||
|
||||
config.case_insensitive_keys = [:email]
|
||||
config.strip_whitespace_keys = [:email]
|
||||
config.skip_session_storage = [:http_auth]
|
||||
config.stretches = Rails.env.test? ? 1 : 12
|
||||
config.reconfirmable = false
|
||||
config.expire_all_remember_me_on_sign_out = true
|
||||
config.password_length = 6..128
|
||||
config.email_regexp = /\A[^@\s]+@[^@\s]+\z/
|
||||
config.reset_password_within = 6.hours
|
||||
config.sign_out_via = :delete
|
||||
end
|
||||
@@ -0,0 +1,5 @@
|
||||
Rails.application.config.action_dispatch.default_headers = {
|
||||
"Cross-Origin-Opener-Policy" => "same-origin",
|
||||
"Cross-Origin-Embedder-Policy" => "require-corp",
|
||||
"Cross-Origin-Resource-Policy" => "same-origin"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
max_threads_count = ENV.fetch("RAILS_MAX_THREADS", 5)
|
||||
min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count }
|
||||
threads min_threads_count, max_threads_count
|
||||
|
||||
port ENV.fetch("PORT", 3000)
|
||||
environment ENV.fetch("RAILS_ENV", "development")
|
||||
pidfile ENV.fetch("PIDFILE", "tmp/pids/server.pid")
|
||||
plugin :tmp_restart
|
||||
@@ -0,0 +1,12 @@
|
||||
Rails.application.routes.draw do
|
||||
devise_for :admin_users, ActiveAdmin::Devise.config
|
||||
ActiveAdmin.routes(self)
|
||||
|
||||
namespace :api do
|
||||
get "software", to: "software#index"
|
||||
get "software/highlighted", to: "software_highlighted#index"
|
||||
end
|
||||
|
||||
get "update", to: "update#update"
|
||||
get "file/*path", to: "files#show", format: false
|
||||
end
|
||||
@@ -0,0 +1,24 @@
|
||||
class CreateSoftwares < ActiveRecord::Migration[8.1]
|
||||
def change
|
||||
create_table :softwares, id: { type: :bigint, unsigned: true },
|
||||
charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci",
|
||||
if_not_exists: true 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
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,23 @@
|
||||
class CreateReleases < ActiveRecord::Migration[8.1]
|
||||
def change
|
||||
create_table :releases, id: { type: :bigint, unsigned: true },
|
||||
charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci",
|
||||
if_not_exists: true do |t|
|
||||
t.string :cartridge_path
|
||||
t.datetime :created_at, precision: 3
|
||||
t.datetime :deleted_at, precision: 3
|
||||
t.string :docs_folder_path
|
||||
t.string :html_folder_path
|
||||
t.bigint :software_id, unsigned: true
|
||||
t.string :source_path
|
||||
t.datetime :updated_at, precision: 3
|
||||
t.string :version, limit: 64
|
||||
t.boolean :web_playable, default: false
|
||||
|
||||
t.index :deleted_at, name: "idx_releases_deleted_at"
|
||||
t.index :software_id, name: "idx_releases_software_id"
|
||||
end
|
||||
|
||||
add_foreign_key :releases, :softwares, name: "fk_softwares_releases" unless foreign_key_exists?(:releases, :softwares)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,19 @@
|
||||
class CreateExternalLinks < ActiveRecord::Migration[8.1]
|
||||
def change
|
||||
create_table :external_links, id: { type: :bigint, unsigned: true },
|
||||
charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci",
|
||||
if_not_exists: true 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
|
||||
|
||||
add_foreign_key :external_links, :softwares, name: "fk_softwares_external_links" unless foreign_key_exists?(:external_links, :softwares)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,17 @@
|
||||
class CreateAdminUsers < ActiveRecord::Migration[8.1]
|
||||
def change
|
||||
create_table :admin_users, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci",
|
||||
if_not_exists: true do |t|
|
||||
t.string :email, null: false, default: ""
|
||||
t.string :encrypted_password, null: false, default: ""
|
||||
t.string :reset_password_token
|
||||
t.datetime :reset_password_sent_at
|
||||
t.datetime :remember_created_at
|
||||
t.datetime :created_at, null: false
|
||||
t.datetime :updated_at, null: false
|
||||
|
||||
t.index :email, name: "index_admin_users_on_email", unique: true
|
||||
t.index :reset_password_token, name: "index_admin_users_on_reset_password_token", unique: true
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,173 @@
|
||||
# This file is auto-generated from the current state of the database. Instead
|
||||
# of editing this file, please use the migrations feature of Active Record to
|
||||
# incrementally modify your database, and then regenerate this schema definition.
|
||||
#
|
||||
# This file is the source Rails uses to define your schema when running `bin/rails
|
||||
# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
|
||||
# be faster and is potentially less error prone than running all of your
|
||||
# migrations from scratch. Old migrations may fail to apply correctly if those
|
||||
# migrations use external dependencies or application code.
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[8.1].define(version: 2026_05_05_000004) do
|
||||
create_table "admin_users", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.datetime "created_at", null: false
|
||||
t.string "email", default: "", null: false
|
||||
t.string "encrypted_password", default: "", null: false
|
||||
t.datetime "remember_created_at"
|
||||
t.datetime "reset_password_sent_at"
|
||||
t.string "reset_password_token"
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["email"], name: "index_admin_users_on_email", unique: true
|
||||
t.index ["reset_password_token"], name: "index_admin_users_on_reset_password_token", unique: true
|
||||
end
|
||||
|
||||
create_table "articles", id: :integer, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.text "content"
|
||||
t.timestamp "created_at", default: -> { "CURRENT_TIMESTAMP" }
|
||||
t.string "slug"
|
||||
t.string "title"
|
||||
t.timestamp "updated_at", default: -> { "CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" }
|
||||
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 "goadmin_menu", id: { type: :integer, unsigned: true }, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.timestamp "created_at", default: -> { "CURRENT_TIMESTAMP" }
|
||||
t.string "header", limit: 150
|
||||
t.string "icon", limit: 50, null: false
|
||||
t.integer "order", default: 0, null: false
|
||||
t.integer "parent_id", default: 0, null: false
|
||||
t.string "plugin_name", limit: 150, default: "", null: false
|
||||
t.string "title", limit: 50, null: false
|
||||
t.integer "type", default: 0, null: false
|
||||
t.timestamp "updated_at", default: -> { "CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" }
|
||||
t.string "uri", null: false
|
||||
t.string "uuid", limit: 150
|
||||
end
|
||||
|
||||
create_table "goadmin_operation_log", id: { type: :integer, unsigned: true }, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.timestamp "created_at", default: -> { "CURRENT_TIMESTAMP" }
|
||||
t.text "input", null: false
|
||||
t.string "ip", limit: 15, null: false
|
||||
t.string "method", limit: 10, null: false
|
||||
t.string "path", null: false
|
||||
t.timestamp "updated_at", default: -> { "CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" }
|
||||
t.integer "user_id", null: false
|
||||
end
|
||||
|
||||
create_table "goadmin_permissions", id: { type: :integer, unsigned: true }, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.timestamp "created_at", default: -> { "CURRENT_TIMESTAMP" }
|
||||
t.string "http_method"
|
||||
t.text "http_path"
|
||||
t.string "name", limit: 50, null: false
|
||||
t.string "slug", limit: 50, null: false
|
||||
t.timestamp "updated_at", default: -> { "CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" }
|
||||
t.index ["name"], name: "permissions_name_unique", unique: true
|
||||
t.index ["slug"], name: "permissions_slug_unique", unique: true
|
||||
end
|
||||
|
||||
create_table "goadmin_role_menu", id: false, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.timestamp "created_at", default: -> { "CURRENT_TIMESTAMP" }
|
||||
t.integer "menu_id", null: false
|
||||
t.integer "role_id", null: false
|
||||
t.timestamp "updated_at", default: -> { "CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" }
|
||||
end
|
||||
|
||||
create_table "goadmin_role_permissions", id: false, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.timestamp "created_at", default: -> { "CURRENT_TIMESTAMP" }
|
||||
t.integer "permission_id", null: false
|
||||
t.integer "role_id", null: false
|
||||
t.timestamp "updated_at", default: -> { "CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" }
|
||||
end
|
||||
|
||||
create_table "goadmin_role_users", id: false, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.timestamp "created_at", default: -> { "CURRENT_TIMESTAMP" }
|
||||
t.integer "role_id", null: false
|
||||
t.timestamp "updated_at", default: -> { "CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" }
|
||||
t.integer "user_id", null: false
|
||||
end
|
||||
|
||||
create_table "goadmin_roles", id: { type: :integer, unsigned: true }, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.timestamp "created_at", default: -> { "CURRENT_TIMESTAMP" }
|
||||
t.string "name", limit: 100, null: false
|
||||
t.string "slug", limit: 100, null: false
|
||||
t.timestamp "updated_at", default: -> { "CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" }
|
||||
t.index ["name"], name: "roles_name_unique", unique: true
|
||||
t.index ["slug"], name: "roles_slug_unique", unique: true
|
||||
end
|
||||
|
||||
create_table "goadmin_site", id: { type: :integer, unsigned: true }, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.timestamp "created_at", default: -> { "CURRENT_TIMESTAMP" }
|
||||
t.string "description", limit: 3000, null: false
|
||||
t.string "key_", limit: 100, null: false
|
||||
t.integer "state", limit: 1, default: 0, null: false, unsigned: true
|
||||
t.timestamp "updated_at", default: -> { "CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" }
|
||||
t.text "value", null: false
|
||||
t.index ["key_"], name: "site_key_unique", unique: true
|
||||
end
|
||||
|
||||
create_table "goadmin_user_permissions", id: false, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.timestamp "created_at", default: -> { "CURRENT_TIMESTAMP" }
|
||||
t.integer "permission_id", null: false
|
||||
t.timestamp "updated_at", default: -> { "CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" }
|
||||
t.integer "user_id", null: false
|
||||
end
|
||||
|
||||
create_table "goadmin_users", id: { type: :integer, unsigned: true }, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.string "avatar"
|
||||
t.timestamp "created_at", default: -> { "CURRENT_TIMESTAMP" }
|
||||
t.string "name", limit: 100, null: false
|
||||
t.string "password", limit: 100, null: false
|
||||
t.string "remember_token", limit: 100
|
||||
t.timestamp "updated_at", default: -> { "CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" }
|
||||
t.string "username", limit: 100, null: false
|
||||
t.index ["username"], name: "users_username_unique", unique: true
|
||||
end
|
||||
|
||||
create_table "releases", id: { type: :bigint, unsigned: true }, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.string "cartridge_path"
|
||||
t.datetime "created_at", precision: 3
|
||||
t.datetime "deleted_at", precision: 3
|
||||
t.string "docs_folder_path"
|
||||
t.string "html_folder_path"
|
||||
t.bigint "software_id", unsigned: true
|
||||
t.string "source_path"
|
||||
t.datetime "updated_at", precision: 3
|
||||
t.string "version", limit: 64
|
||||
t.boolean "web_playable", default: false
|
||||
t.index ["deleted_at"], name: "idx_releases_deleted_at"
|
||||
t.index ["software_id"], name: "idx_releases_software_id"
|
||||
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 "external_links", "softwares", name: "fk_softwares_external_links"
|
||||
add_foreign_key "releases", "softwares", name: "fk_softwares_releases"
|
||||
end
|
||||
@@ -0,0 +1,4 @@
|
||||
AdminUser.find_or_create_by!(email: ENV.fetch("ADMIN_EMAIL", "admin@teletype.hu")) do |user|
|
||||
user.password = ENV.fetch("ADMIN_PASSWORD", "password123")
|
||||
user.password_confirmation = ENV.fetch("ADMIN_PASSWORD", "password123")
|
||||
end
|
||||
@@ -0,0 +1,12 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
mkdir -p tmp/pids tmp/cache tmp/sockets log
|
||||
rm -f tmp/pids/server.pid
|
||||
|
||||
bundle check || bundle install
|
||||
|
||||
bundle exec rails db:migrate
|
||||
bundle exec rails db:seed
|
||||
|
||||
bundle exec rails server -b 0.0.0.0 -p 3000
|
||||
@@ -1,39 +0,0 @@
|
||||
root = "."
|
||||
testdata_dir = "testdata"
|
||||
tmp_dir = "tmp"
|
||||
|
||||
[build]
|
||||
args_bin = []
|
||||
bin = "./tmp/teletype_softwares"
|
||||
cmd = "go build -o ./tmp/teletype_softwares ./main.go"
|
||||
delay = 1000
|
||||
exclude_dir = [
|
||||
"tmp",
|
||||
]
|
||||
exclude_file = []
|
||||
exclude_regex = ["_test.go", "data"]
|
||||
exclude_unchanged = false
|
||||
follow_symlink = false
|
||||
full_bin = ""
|
||||
include_dir = []
|
||||
include_ext = ["go", "tpl", "tmpl", "html"]
|
||||
kill_delay = "0s"
|
||||
log = "build-errors.log"
|
||||
send_interrupt = false
|
||||
stop_on_error = true
|
||||
|
||||
[color]
|
||||
app = ""
|
||||
build = "yellow"
|
||||
main = "magenta"
|
||||
runner = "green"
|
||||
watcher = "cyan"
|
||||
|
||||
[log]
|
||||
time = false
|
||||
|
||||
[misc]
|
||||
clean_on_exit = false
|
||||
|
||||
[screen]
|
||||
clear_on_rebuild = false
|
||||
@@ -1,21 +0,0 @@
|
||||
# stage 1: building application binary file
|
||||
FROM --platform=linux/amd64 golang:1.19-alpine as build
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN go mod tidy
|
||||
RUN go build -o main ./main.go
|
||||
|
||||
# stage 2: copy only the application binary file and necessary files to the alpine container
|
||||
FROM --platform=linux/amd64 alpine:latest
|
||||
RUN apk --update add ca-certificates
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=build /app/main .
|
||||
COPY --from=build /app/http/openapi.yml .
|
||||
|
||||
# run the service on container startup.
|
||||
CMD ["/app/main"]
|
||||
@@ -1,14 +0,0 @@
|
||||
FROM --platform=linux/amd64 golang:1.23-alpine
|
||||
|
||||
RUN apk add curl git unzip
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN chmod +x ./entrypoint.sh
|
||||
|
||||
RUN curl -fLo install.sh https://raw.githubusercontent.com/cosmtrek/air/master/install.sh \
|
||||
&& chmod +x install.sh && sh install.sh && cp ./bin/air /bin/air
|
||||
|
||||
CMD ["./entrypoint.sh"]
|
||||
@@ -1,66 +0,0 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"teletype_softwares/lib/mysql_utils"
|
||||
)
|
||||
|
||||
type Domain struct {
|
||||
SoftwareRepository SoftwareRepositoryInterface
|
||||
ReleaseRepository ReleaseRepositoryInterface
|
||||
FileRepository FileRepositoryInterface
|
||||
ExternalLinkRepository ExternalLinkRepositoryInterface
|
||||
DownloadService DownloadServiceInterface
|
||||
SoftwareUpdaterService SoftwareUpdaterServiceInterface
|
||||
SoftwareService SoftwareServiceInterface
|
||||
FileService FileServiceInterface
|
||||
}
|
||||
|
||||
func NewDomain() Domain {
|
||||
DB := mysql_utils.Init()
|
||||
MigrateGoDatabase(DB)
|
||||
|
||||
software_repository := &SoftwareRepository{db: DB}
|
||||
release_repository := &ReleaseRepository{db: DB}
|
||||
file_repository := NewFileRepository()
|
||||
external_link_repository := &ExternalLinkRepository{db: DB}
|
||||
|
||||
software_service := NewSoftwareService(software_repository, release_repository)
|
||||
|
||||
tic80_updater := NewSoftwareUpdaterTIC80Service(
|
||||
software_repository,
|
||||
release_repository,
|
||||
file_repository,
|
||||
external_link_repository,
|
||||
)
|
||||
|
||||
ebitengine_updater := NewSoftwareUpdaterEbitengineService(
|
||||
software_repository,
|
||||
release_repository,
|
||||
file_repository,
|
||||
external_link_repository,
|
||||
)
|
||||
|
||||
love_updater := NewSoftwareUpdaterLoveService(
|
||||
software_repository,
|
||||
release_repository,
|
||||
file_repository,
|
||||
external_link_repository,
|
||||
)
|
||||
|
||||
software_updater_service := NewSoftwareUpdaterService(tic80_updater, ebitengine_updater, love_updater)
|
||||
|
||||
download_service := NewDownloadService(software_repository, release_repository)
|
||||
|
||||
file_service := NewFileService(file_repository)
|
||||
|
||||
return Domain{
|
||||
SoftwareRepository: software_repository,
|
||||
ReleaseRepository: release_repository,
|
||||
FileRepository: file_repository,
|
||||
ExternalLinkRepository: external_link_repository,
|
||||
DownloadService: download_service,
|
||||
SoftwareUpdaterService: software_updater_service,
|
||||
SoftwareService: software_service,
|
||||
FileService: file_service,
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
package domain
|
||||
|
||||
import "gorm.io/gorm"
|
||||
|
||||
type ExternalLink struct {
|
||||
gorm.Model
|
||||
SoftwareID uint `gorm:"index" json:"softwareId"`
|
||||
Label string `gorm:"size:128" json:"label"`
|
||||
URL string `gorm:"size:255" json:"url"`
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func MigrateGoDatabase(db *gorm.DB) {
|
||||
db.AutoMigrate(
|
||||
Software{},
|
||||
Release{},
|
||||
ExternalLink{},
|
||||
)
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package domain
|
||||
|
||||
import "gorm.io/gorm"
|
||||
|
||||
type Release struct {
|
||||
gorm.Model
|
||||
SoftwareID uint `gorm:"index" json:"softwareId"`
|
||||
Version string `gorm:"size:64" json:"version"`
|
||||
CartridgePath string `gorm:"size:255" json:"cartridgePath"`
|
||||
SourcePath string `gorm:"size:255" json:"sourcePath"`
|
||||
HTMLFolderPath string `gorm:"size:255" json:"htmlFolderPath"`
|
||||
DocsFolderPath string `gorm:"size:255" json:"docsFolderPath"`
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package domain
|
||||
|
||||
import "gorm.io/gorm"
|
||||
|
||||
type Software struct {
|
||||
gorm.Model
|
||||
Name string `gorm:"uniqueIndex;size:128" json:"name"`
|
||||
Title string `gorm:"size:255" json:"title"`
|
||||
Author string `gorm:"size:255" json:"author"`
|
||||
Desc string `gorm:"type:text" json:"desc"`
|
||||
Story string `gorm:"type:text" json:"story"`
|
||||
License string `gorm:"size:128" json:"license"`
|
||||
Platform string `gorm:"size:128" json:"platform"`
|
||||
Status string `gorm:"size:20;default:development" json:"status"`
|
||||
Highlighted bool `gorm:"default:false" json:"highlighted"`
|
||||
Releases []Release `json:"-"`
|
||||
ExternalLinks []ExternalLink `json:"externalLinks"`
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package domain
|
||||
|
||||
import "gorm.io/gorm"
|
||||
|
||||
type ExternalLinkRepositoryInterface interface {
|
||||
UpsertByLabel(softwareID uint, label, url string) error
|
||||
}
|
||||
|
||||
type ExternalLinkRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func (r *ExternalLinkRepository) UpsertByLabel(softwareID uint, label, url string) error {
|
||||
var link ExternalLink
|
||||
err := r.db.Where("software_id = ? AND label = ?", softwareID, label).First(&link).Error
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return r.db.Create(&ExternalLink{SoftwareID: softwareID, Label: label, URL: url}).Error
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return r.db.Model(&link).Update("url", url).Error
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type FileRepositoryInterface interface {
|
||||
GetPath(path string) string
|
||||
FileExists(path string) bool
|
||||
CreateDir(path string) error
|
||||
DeleteDir(path string) error
|
||||
DeleteFile(path string) error
|
||||
MoveFile(src_path, dest_path string) error
|
||||
UnzipFile(path, dest_path string) error
|
||||
}
|
||||
|
||||
type FileRepository struct {
|
||||
fileContainerPath string
|
||||
}
|
||||
|
||||
func NewFileRepository() *FileRepository {
|
||||
file_container_path, _ := os.LookupEnv("FILE_CONTAINER_PATH")
|
||||
return &FileRepository{
|
||||
fileContainerPath: file_container_path,
|
||||
}
|
||||
}
|
||||
|
||||
func (fr *FileRepository) GetPath(path string) string {
|
||||
return filepath.Join(fr.fileContainerPath, path)
|
||||
}
|
||||
|
||||
func (fr *FileRepository) FileExists(path string) bool {
|
||||
full_path := fr.GetPath(path)
|
||||
_, err := os.Stat(full_path)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (fr *FileRepository) CreateDir(path string) error {
|
||||
full_path := fr.GetPath(path)
|
||||
return os.MkdirAll(full_path, 0755)
|
||||
}
|
||||
|
||||
func (fr *FileRepository) DeleteDir(path string) error {
|
||||
full_path := fr.GetPath(path)
|
||||
return os.RemoveAll(full_path)
|
||||
}
|
||||
|
||||
func (fr *FileRepository) DeleteFile(path string) error {
|
||||
full_path := fr.GetPath(path)
|
||||
return os.RemoveAll(full_path)
|
||||
}
|
||||
|
||||
func (fr *FileRepository) MoveFile(src_path, dest_path string) error {
|
||||
full_src_path := fr.GetPath(src_path)
|
||||
full_dest_path := fr.GetPath(dest_path)
|
||||
|
||||
dest_dir := filepath.Dir(full_dest_path)
|
||||
if err := os.MkdirAll(dest_dir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create destination directory: %w", err)
|
||||
}
|
||||
|
||||
return os.Rename(full_src_path, full_dest_path)
|
||||
}
|
||||
|
||||
func (fr *FileRepository) UnzipFile(zipfile, desc string) error {
|
||||
zip_path := fr.GetPath(zipfile)
|
||||
desc_path := fr.GetPath(desc)
|
||||
|
||||
if err := os.MkdirAll(desc_path, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd := exec.Command("unzip", "-q", zip_path, "-d", desc_path)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.Remove(zip_path)
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package domain
|
||||
|
||||
import "gorm.io/gorm"
|
||||
|
||||
type ReleaseRepositoryInterface interface {
|
||||
Create(release *Release) error
|
||||
CreateIfNotExist(release *Release) error
|
||||
FindLatestBySoftwareID(software_id uint) (*Release, error)
|
||||
FindBySoftwareIDAndVersion(software_id uint, version string) (*Release, error)
|
||||
ListBySoftwareID(software_id uint) []Release
|
||||
}
|
||||
|
||||
type ReleaseRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func (r *ReleaseRepository) Create(release *Release) error {
|
||||
return r.db.Create(release).Error
|
||||
}
|
||||
|
||||
func (r *ReleaseRepository) CreateIfNotExist(release *Release) error {
|
||||
var existing Release
|
||||
err := r.db.Where("software_id = ? AND version = ?", release.SoftwareID, release.Version).First(&existing).Error
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return r.db.Create(release).Error
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ReleaseRepository) FindLatestBySoftwareID(software_id uint) (*Release, error) {
|
||||
var release Release
|
||||
err := r.db.Where("software_id = ?", software_id).Order("created_at desc").First(&release).Error
|
||||
return &release, err
|
||||
}
|
||||
|
||||
func (r *ReleaseRepository) FindBySoftwareIDAndVersion(software_id uint, version string) (*Release, error) {
|
||||
var release Release
|
||||
err := r.db.Where("software_id = ? AND version = ?", software_id, version).First(&release).Error
|
||||
return &release, err
|
||||
}
|
||||
|
||||
func (r *ReleaseRepository) ListBySoftwareID(software_id uint) []Release {
|
||||
var releases []Release
|
||||
r.db.Where("software_id = ?", software_id).Find(&releases)
|
||||
return releases
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
package domain
|
||||
|
||||
import "gorm.io/gorm"
|
||||
|
||||
type SoftwareRepositoryInterface interface {
|
||||
List() ([]Software, error)
|
||||
GetByName(name string) (*Software, error)
|
||||
UpdateOrCreate(software *Software) error
|
||||
GetHighlighted() (*Software, error)
|
||||
}
|
||||
|
||||
type SoftwareRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func (r *SoftwareRepository) GetHighlighted() (*Software, error) {
|
||||
var software Software
|
||||
if err := r.db.Preload("Releases").Preload("ExternalLinks").Where("highlighted = 1").Order("id DESC").First(&software).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &software, nil
|
||||
}
|
||||
|
||||
|
||||
func (r *SoftwareRepository) GetByName(name string) (*Software, error) {
|
||||
var software Software
|
||||
if err := r.db.Preload("Releases").Preload("ExternalLinks").Where("name = ?", name).First(&software).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &software, nil
|
||||
}
|
||||
|
||||
func (r *SoftwareRepository) List() ([]Software, error) {
|
||||
var softwares []Software
|
||||
if err := r.db.Preload("Releases").Preload("ExternalLinks").Find(&softwares).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return softwares, nil
|
||||
}
|
||||
|
||||
func (r *SoftwareRepository) UpdateOrCreate(software *Software) error {
|
||||
var existing Software
|
||||
if err := r.db.Where("name = ?", software.Name).First(&existing).Error; err == nil {
|
||||
software.ID = existing.ID
|
||||
return r.db.Model(&existing).Updates(software).Error
|
||||
} else {
|
||||
return r.db.Create(software).Error
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrSoftwareNotFound = errors.New("software not found")
|
||||
ErrNoReleasesFound = errors.New("no releases found for software")
|
||||
ErrReleaseNotFound = errors.New("release not found for software and version")
|
||||
)
|
||||
|
||||
type DownloadServiceInterface interface {
|
||||
GetLatestRelease(software_name string) (*Release, error)
|
||||
GetSpecificRelease(software_name string, version string) (*Release, error)
|
||||
}
|
||||
|
||||
type DownloadService struct {
|
||||
softwareRepository SoftwareRepositoryInterface
|
||||
releaseRepository ReleaseRepositoryInterface
|
||||
}
|
||||
|
||||
func NewDownloadService(software_repository SoftwareRepositoryInterface, release_repository ReleaseRepositoryInterface) *DownloadService {
|
||||
return &DownloadService{
|
||||
softwareRepository: software_repository,
|
||||
releaseRepository: release_repository,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *DownloadService) GetLatestRelease(software_name string) (*Release, error) {
|
||||
software, err := s.softwareRepository.GetByName(software_name)
|
||||
if err != nil {
|
||||
return nil, ErrSoftwareNotFound
|
||||
}
|
||||
|
||||
release, err := s.releaseRepository.FindLatestBySoftwareID(software.ID)
|
||||
if err != nil {
|
||||
return nil, ErrNoReleasesFound
|
||||
}
|
||||
|
||||
return release, nil
|
||||
}
|
||||
|
||||
func (s *DownloadService) GetSpecificRelease(software_name string, version string) (*Release, error) {
|
||||
software, err := s.softwareRepository.GetByName(software_name)
|
||||
if err != nil {
|
||||
return nil, ErrSoftwareNotFound
|
||||
}
|
||||
|
||||
release, err := s.releaseRepository.FindBySoftwareIDAndVersion(software.ID, version)
|
||||
if err != nil {
|
||||
return nil, ErrReleaseNotFound
|
||||
}
|
||||
|
||||
return release, nil
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package domain
|
||||
|
||||
type FileServiceInterface interface {
|
||||
GetPath(path string) string
|
||||
}
|
||||
|
||||
type FileService struct {
|
||||
FileRepository FileRepositoryInterface
|
||||
}
|
||||
|
||||
func NewFileService(file_repository FileRepositoryInterface) *FileService {
|
||||
return &FileService{
|
||||
FileRepository: file_repository,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *FileService) GetPath(path string) string {
|
||||
return s.FileRepository.GetPath(path)
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
)
|
||||
|
||||
type SoftwareListDTO struct {
|
||||
Software
|
||||
LatestRelease *Release
|
||||
}
|
||||
|
||||
type SoftwareDetaildListDTO struct {
|
||||
Softwares []SoftwareShowData `json:"softwares"`
|
||||
}
|
||||
|
||||
type SoftwareShowData struct {
|
||||
Software *Software `json:"software"`
|
||||
Releases []Release `json:"releases"`
|
||||
LatestRelease *Release `json:"latestRelease"`
|
||||
WebPlayableRelease *Release `json:"webPlayableRelease"`
|
||||
}
|
||||
|
||||
type SoftwareServiceInterface interface {
|
||||
List() ([]SoftwareListDTO, error)
|
||||
DetailedList() (*SoftwareDetaildListDTO, error)
|
||||
GetByName(name string) (*Software, error)
|
||||
GetLatestRelease(software_id string) (*Release, error)
|
||||
GetForShowByName(name string) (*SoftwareShowData, error)
|
||||
GetHighlighted() (*SoftwareShowData, error)
|
||||
}
|
||||
|
||||
type SoftwareService struct {
|
||||
softeare_repository SoftwareRepositoryInterface
|
||||
release_repository ReleaseRepositoryInterface
|
||||
}
|
||||
|
||||
func (s *SoftwareService) GetHighlighted() (*SoftwareShowData, error) {
|
||||
software, err := s.softeare_repository.GetHighlighted()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if software == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return s.GetForShowByName(software.Name)
|
||||
}
|
||||
|
||||
func NewSoftwareService(
|
||||
softeare_repository SoftwareRepositoryInterface,
|
||||
release_repository ReleaseRepositoryInterface,
|
||||
) *SoftwareService {
|
||||
return &SoftwareService{
|
||||
softeare_repository: softeare_repository,
|
||||
release_repository: release_repository,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SoftwareService) List() ([]SoftwareListDTO, error) {
|
||||
softwares, err := s.softeare_repository.List()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dtos := make([]SoftwareListDTO, 0, len(softwares))
|
||||
for _, sw := range softwares {
|
||||
dto := SoftwareListDTO{Software: sw}
|
||||
if len(sw.Releases) > 0 {
|
||||
sort.Slice(sw.Releases, func(i, j int) bool {
|
||||
return sw.Releases[i].CreatedAt.After(sw.Releases[j].CreatedAt)
|
||||
})
|
||||
dto.LatestRelease = &sw.Releases[0]
|
||||
}
|
||||
dtos = append(dtos, dto)
|
||||
}
|
||||
|
||||
return dtos, nil
|
||||
}
|
||||
|
||||
func (s *SoftwareService) GetByName(name string) (*Software, error) {
|
||||
return s.softeare_repository.GetByName(name)
|
||||
}
|
||||
|
||||
func (s *SoftwareService) GetForShowByName(name string) (*SoftwareShowData, error) {
|
||||
software, err := s.softeare_repository.GetByName(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if software == nil {
|
||||
return nil, fmt.Errorf("software not found")
|
||||
}
|
||||
|
||||
releases := s.release_repository.ListBySoftwareID(software.ID)
|
||||
|
||||
var latest_release *Release
|
||||
if len(releases) > 0 {
|
||||
sort.Slice(releases, func(i, j int) bool {
|
||||
return releases[i].CreatedAt.After(releases[j].CreatedAt)
|
||||
})
|
||||
latest_release = &releases[0]
|
||||
}
|
||||
|
||||
var web_playable_release *Release
|
||||
for i := range releases {
|
||||
if releases[i].HTMLFolderPath != "" {
|
||||
web_playable_release = &releases[i] // fix: slice elem címe, nem a loop változóé
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return &SoftwareShowData{
|
||||
Software: software,
|
||||
Releases: releases,
|
||||
LatestRelease: latest_release,
|
||||
WebPlayableRelease: web_playable_release,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *SoftwareService) GetLatestRelease(software_id string) (*Release, error) {
|
||||
software, err := s.softeare_repository.GetByName(software_id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if software == nil {
|
||||
return nil, nil
|
||||
}
|
||||
if len(software.Releases) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
sort.Slice(software.Releases, func(i, j int) bool {
|
||||
return software.Releases[i].CreatedAt.After(software.Releases[j].CreatedAt)
|
||||
})
|
||||
return &software.Releases[0], nil
|
||||
}
|
||||
|
||||
func (s *SoftwareService) DetailedList() (*SoftwareDetaildListDTO, error) {
|
||||
softwares, err := s.softeare_repository.List()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dtos := make([]SoftwareShowData, 0, len(softwares))
|
||||
for _, software := range softwares {
|
||||
showData, err := s.GetForShowByName(software.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dtos = append(dtos, *showData)
|
||||
}
|
||||
|
||||
return &SoftwareDetaildListDTO{Softwares: dtos}, nil
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type SoftwareUpdaterServiceInterface interface {
|
||||
Update(platform, name, version string) error
|
||||
}
|
||||
|
||||
type SoftwareUpdaterService struct {
|
||||
tic80Updater SoftwareUpdaterTIC80ServiceInterface
|
||||
ebitengineUpdater SoftwareUpdaterEbitengineServiceInterface
|
||||
loveUpdater SoftwareUpdaterLoveServiceInterface
|
||||
}
|
||||
|
||||
func NewSoftwareUpdaterService(tic80_updater SoftwareUpdaterTIC80ServiceInterface, ebitengine_updater SoftwareUpdaterEbitengineServiceInterface, love_updater SoftwareUpdaterLoveServiceInterface) *SoftwareUpdaterService {
|
||||
return &SoftwareUpdaterService{
|
||||
tic80Updater: tic80_updater,
|
||||
ebitengineUpdater: ebitengine_updater,
|
||||
loveUpdater: love_updater,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SoftwareUpdaterService) Update(platform, name, version string) error {
|
||||
if platform == "tic80" {
|
||||
return s.tic80Updater.Update(name, version)
|
||||
}
|
||||
if platform == "ebitengine" {
|
||||
return s.ebitengineUpdater.Update(name, version)
|
||||
}
|
||||
if platform == "love" {
|
||||
return s.loveUpdater.Update(name, version)
|
||||
}
|
||||
return fmt.Errorf("unsupported platform: %s", platform)
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
type SoftwareUpdaterEbitengineMetadata struct {
|
||||
Name string `json:"name"`
|
||||
Title string `json:"title"`
|
||||
Author string `json:"author"`
|
||||
Desc string `json:"desc"`
|
||||
Site string `json:"site"`
|
||||
License string `json:"license"`
|
||||
}
|
||||
|
||||
type SoftwareUpdaterEbitengineServiceInterface interface {
|
||||
Update(name, version string) error
|
||||
}
|
||||
|
||||
type SoftwareUpdaterEbitengineService struct {
|
||||
softwareRepository SoftwareRepositoryInterface
|
||||
releaseRepository ReleaseRepositoryInterface
|
||||
fileRepository FileRepositoryInterface
|
||||
externalLinkRepository ExternalLinkRepositoryInterface
|
||||
}
|
||||
|
||||
func NewSoftwareUpdaterEbitengineService(
|
||||
software_repository SoftwareRepositoryInterface,
|
||||
release_repository ReleaseRepositoryInterface,
|
||||
file_repository FileRepositoryInterface,
|
||||
external_link_repository ExternalLinkRepositoryInterface,
|
||||
) *SoftwareUpdaterEbitengineService {
|
||||
return &SoftwareUpdaterEbitengineService{
|
||||
softwareRepository: software_repository,
|
||||
releaseRepository: release_repository,
|
||||
fileRepository: file_repository,
|
||||
externalLinkRepository: external_link_repository,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SoftwareUpdaterEbitengineService) Update(name, version string) error {
|
||||
fmt.Printf("Ebitengine Updater: Starting update for name: %s, version: %s\n", name, version)
|
||||
|
||||
versioned_name := name + "-" + version
|
||||
|
||||
zip_filename := versioned_name + ".html.zip"
|
||||
metadata_filename := versioned_name + ".metadata.json"
|
||||
html_dirname := versioned_name
|
||||
|
||||
fmt.Printf("Ebitengine Updater: Processing zip file: %s\n", zip_filename)
|
||||
fmt.Printf("Ebitengine Updater: Processing metadata file: %s\n", metadata_filename)
|
||||
fmt.Printf("Ebitengine Updater: Processing HTML directory: %s\n", html_dirname)
|
||||
|
||||
if s.fileRepository.FileExists(html_dirname) {
|
||||
fmt.Printf("Ebitengine Updater: Removing existing HTML directory: %s\n", html_dirname)
|
||||
s.fileRepository.DeleteDir(html_dirname)
|
||||
}
|
||||
|
||||
fmt.Printf("Ebitengine Updater: Creating HTML directory: %s\n", html_dirname)
|
||||
s.fileRepository.CreateDir(html_dirname)
|
||||
fmt.Printf("Ebitengine Updater: Unzipping file: %s to directory: %s\n", zip_filename, html_dirname)
|
||||
s.fileRepository.UnzipFile(zip_filename, html_dirname)
|
||||
|
||||
html_dir_path := s.fileRepository.GetPath(html_dirname)
|
||||
metadata_path := s.fileRepository.GetPath(metadata_filename)
|
||||
|
||||
var err error
|
||||
|
||||
fmt.Printf("Ebitengine Updater: Extracting metadata from metadata file: %s\n", metadata_path)
|
||||
meta_data, err := s.GetMetadata(metadata_path)
|
||||
if err != nil {
|
||||
fmt.Printf("Ebitengine Updater: Error extracting metadata: %s\n", err.Error())
|
||||
return err
|
||||
}
|
||||
fmt.Printf("Ebitengine Updater: Extracted metadata: %+v\n", meta_data)
|
||||
siteURL := meta_data.Site
|
||||
software := s.BuildSoftware(meta_data)
|
||||
|
||||
err = s.softwareRepository.UpdateOrCreate(software)
|
||||
if err != nil {
|
||||
fmt.Printf("Ebitengine Updater: Error updating or creating software: %s\n", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
if siteURL != "" {
|
||||
if linkErr := s.externalLinkRepository.UpsertByLabel(software.ID, "Source Code", siteURL); linkErr != nil {
|
||||
fmt.Printf("Ebitengine Updater: Error upserting external link: %s\n", linkErr.Error())
|
||||
}
|
||||
}
|
||||
|
||||
release := Release{
|
||||
SoftwareID: software.ID,
|
||||
Version: version,
|
||||
CartridgePath: "",
|
||||
SourcePath: "",
|
||||
HTMLFolderPath: html_dir_path,
|
||||
}
|
||||
|
||||
fmt.Printf("Ebitengine Updater: Creating release for software ID: %d, version: %s\n", software.ID, version)
|
||||
s.releaseRepository.CreateIfNotExist(&release)
|
||||
|
||||
fmt.Printf("Ebitengine Updater: Successfully processed Lua cartridge: %s\n", name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SoftwareUpdaterEbitengineService) BuildSoftware(metadata SoftwareUpdaterEbitengineMetadata) *Software {
|
||||
software := &Software{
|
||||
Name: metadata.Name,
|
||||
Title: metadata.Title,
|
||||
Author: metadata.Author,
|
||||
Desc: metadata.Desc,
|
||||
License: metadata.License,
|
||||
Platform: "ebitengine",
|
||||
}
|
||||
|
||||
return software
|
||||
}
|
||||
|
||||
func (s *SoftwareUpdaterEbitengineService) GetMetadata(metadata_path string) (SoftwareUpdaterEbitengineMetadata, error) {
|
||||
var metadata SoftwareUpdaterEbitengineMetadata
|
||||
|
||||
data, err := os.ReadFile(metadata_path)
|
||||
if err != nil {
|
||||
return metadata, err
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, &metadata); err != nil {
|
||||
return metadata, err
|
||||
}
|
||||
|
||||
return metadata, nil
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
type SoftwareUpdaterLoveMetadata struct {
|
||||
Name string `json:"name"`
|
||||
Title string `json:"title"`
|
||||
Author string `json:"author"`
|
||||
Desc string `json:"desc"`
|
||||
Site string `json:"site"`
|
||||
License string `json:"license"`
|
||||
}
|
||||
|
||||
type SoftwareUpdaterLoveServiceInterface interface {
|
||||
Update(name, version string) error
|
||||
}
|
||||
|
||||
type SoftwareUpdaterLoveService struct {
|
||||
softwareRepository SoftwareRepositoryInterface
|
||||
releaseRepository ReleaseRepositoryInterface
|
||||
fileRepository FileRepositoryInterface
|
||||
externalLinkRepository ExternalLinkRepositoryInterface
|
||||
}
|
||||
|
||||
func NewSoftwareUpdaterLoveService(
|
||||
software_repository SoftwareRepositoryInterface,
|
||||
release_repository ReleaseRepositoryInterface,
|
||||
file_repository FileRepositoryInterface,
|
||||
external_link_repository ExternalLinkRepositoryInterface,
|
||||
) *SoftwareUpdaterLoveService {
|
||||
return &SoftwareUpdaterLoveService{
|
||||
softwareRepository: software_repository,
|
||||
releaseRepository: release_repository,
|
||||
fileRepository: file_repository,
|
||||
externalLinkRepository: external_link_repository,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SoftwareUpdaterLoveService) Update(name, version string) error {
|
||||
fmt.Printf("Love Updater: Starting update for name: %s, version: %s\n", name, version)
|
||||
|
||||
versioned_name := name + "-" + version
|
||||
|
||||
zip_filename := versioned_name + ".html.zip"
|
||||
metadata_filename := versioned_name + ".metadata.json"
|
||||
html_dirname := versioned_name
|
||||
|
||||
fmt.Printf("Love Updater: Processing zip file: %s\n", zip_filename)
|
||||
fmt.Printf("Love Updater: Processing metadata file: %s\n", metadata_filename)
|
||||
fmt.Printf("Love Updater: Processing HTML directory: %s\n", html_dirname)
|
||||
|
||||
if s.fileRepository.FileExists(html_dirname) {
|
||||
fmt.Printf("Love Updater: Removing existing HTML directory: %s\n", html_dirname)
|
||||
s.fileRepository.DeleteDir(html_dirname)
|
||||
}
|
||||
|
||||
fmt.Printf("Love Updater: Creating HTML directory: %s\n", html_dirname)
|
||||
s.fileRepository.CreateDir(html_dirname)
|
||||
fmt.Printf("Love Updater: Unzipping file: %s to directory: %s\n", zip_filename, html_dirname)
|
||||
s.fileRepository.UnzipFile(zip_filename, html_dirname)
|
||||
|
||||
html_dir_path := s.fileRepository.GetPath(html_dirname)
|
||||
metadata_path := s.fileRepository.GetPath(metadata_filename)
|
||||
|
||||
var err error
|
||||
|
||||
fmt.Printf("Love Updater: Extracting metadata from metadata file: %s\n", metadata_path)
|
||||
meta_data, err := s.GetMetadata(metadata_path)
|
||||
if err != nil {
|
||||
fmt.Printf("Love Updater: Error extracting metadata: %s\n", err.Error())
|
||||
return err
|
||||
}
|
||||
fmt.Printf("Love Updater: Extracted metadata: %+v\n", meta_data)
|
||||
siteURL := meta_data.Site
|
||||
software := s.BuildSoftware(meta_data)
|
||||
|
||||
err = s.softwareRepository.UpdateOrCreate(software)
|
||||
if err != nil {
|
||||
fmt.Printf("Love Updater: Error updating or creating software: %s\n", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
if siteURL != "" {
|
||||
if linkErr := s.externalLinkRepository.UpsertByLabel(software.ID, "Source Code", siteURL); linkErr != nil {
|
||||
fmt.Printf("Love Updater: Error upserting external link: %s\n", linkErr.Error())
|
||||
}
|
||||
}
|
||||
|
||||
release := Release{
|
||||
SoftwareID: software.ID,
|
||||
Version: version,
|
||||
CartridgePath: "",
|
||||
SourcePath: "",
|
||||
HTMLFolderPath: html_dir_path,
|
||||
}
|
||||
|
||||
fmt.Printf("Love Updater: Creating release for software ID: %d, version: %s\n", software.ID, version)
|
||||
s.releaseRepository.CreateIfNotExist(&release)
|
||||
|
||||
fmt.Printf("Love Updater: Successfully processed Lua cartridge: %s\n", name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SoftwareUpdaterLoveService) BuildSoftware(metadata SoftwareUpdaterLoveMetadata) *Software {
|
||||
software := &Software{
|
||||
Name: metadata.Name,
|
||||
Title: metadata.Title,
|
||||
Author: metadata.Author,
|
||||
Desc: metadata.Desc,
|
||||
License: metadata.License,
|
||||
Platform: "love",
|
||||
}
|
||||
|
||||
return software
|
||||
}
|
||||
|
||||
func (s *SoftwareUpdaterLoveService) GetMetadata(metadata_path string) (SoftwareUpdaterLoveMetadata, error) {
|
||||
var metadata SoftwareUpdaterLoveMetadata
|
||||
|
||||
data, err := os.ReadFile(metadata_path)
|
||||
if err != nil {
|
||||
return metadata, err
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, &metadata); err != nil {
|
||||
return metadata, err
|
||||
}
|
||||
|
||||
return metadata, nil
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type SoftwareUpdaterTIC80ServiceInterface interface {
|
||||
Update(name, version string) error
|
||||
}
|
||||
|
||||
type SoftwareUpdaterTIC80Service struct {
|
||||
softwareRepository SoftwareRepositoryInterface
|
||||
releaseRepository ReleaseRepositoryInterface
|
||||
fileRepository FileRepositoryInterface
|
||||
externalLinkRepository ExternalLinkRepositoryInterface
|
||||
}
|
||||
|
||||
func NewSoftwareUpdaterTIC80Service(
|
||||
software_repository SoftwareRepositoryInterface,
|
||||
release_repository ReleaseRepositoryInterface,
|
||||
file_repository FileRepositoryInterface,
|
||||
external_link_repository ExternalLinkRepositoryInterface,
|
||||
) *SoftwareUpdaterTIC80Service {
|
||||
return &SoftwareUpdaterTIC80Service{
|
||||
softwareRepository: software_repository,
|
||||
releaseRepository: release_repository,
|
||||
fileRepository: file_repository,
|
||||
externalLinkRepository: external_link_repository,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SoftwareUpdaterTIC80Service) Update(name, version string) error {
|
||||
fmt.Printf("TIC80 Updater: Starting update for name: %s, version: %s\n", name, version)
|
||||
|
||||
versioned_name := name + "-" + version
|
||||
|
||||
zip_filename := versioned_name + ".html.zip"
|
||||
docs_zip_filename := versioned_name + "-docs.zip"
|
||||
cartridge_filename := versioned_name + ".tic"
|
||||
source_filename := versioned_name + ".lua"
|
||||
html_dirname := versioned_name
|
||||
docs_dirname := versioned_name + "-docs"
|
||||
|
||||
fmt.Printf("TIC80 Updater: Processing zip file: %s\n", zip_filename)
|
||||
fmt.Printf("TIC80 Updater: Processing docs zip file: %s\n", docs_zip_filename)
|
||||
fmt.Printf("TIC80 Updater: Processing cartridge file: %s\n", cartridge_filename)
|
||||
fmt.Printf("TIC80 Updater: Processing source file: %s\n", source_filename)
|
||||
fmt.Printf("TIC80 Updater: Processing HTML directory: %s\n", html_dirname)
|
||||
fmt.Printf("TIC80 Updater: Processing docs directory: %s\n", docs_dirname)
|
||||
|
||||
if s.fileRepository.FileExists(html_dirname) {
|
||||
fmt.Printf("TIC80 Updater: Removing existing HTML directory: %s\n", html_dirname)
|
||||
s.fileRepository.DeleteDir(html_dirname)
|
||||
}
|
||||
|
||||
if s.fileRepository.FileExists(docs_dirname) {
|
||||
fmt.Printf("TIC80 Updater: Removing existing docs directory: %s\n", docs_dirname)
|
||||
s.fileRepository.DeleteDir(docs_dirname)
|
||||
}
|
||||
|
||||
s.fileRepository.CreateDir(html_dirname)
|
||||
fmt.Printf("TIC80 Updater: Unzipping file: %s to directory: %s\n", zip_filename, html_dirname)
|
||||
s.fileRepository.UnzipFile(zip_filename, html_dirname)
|
||||
|
||||
s.fileRepository.CreateDir(docs_dirname)
|
||||
fmt.Printf("TIC80 Updater: Unzipping docs file: %s to directory: %s\n", docs_zip_filename, docs_dirname)
|
||||
s.fileRepository.UnzipFile(docs_zip_filename, docs_dirname)
|
||||
|
||||
cartridge_path := s.fileRepository.GetPath((cartridge_filename))
|
||||
source_path := s.fileRepository.GetPath(source_filename)
|
||||
html_dir_path := s.fileRepository.GetPath(html_dirname)
|
||||
docs_dir_path := s.fileRepository.GetPath(docs_dirname)
|
||||
|
||||
var err error
|
||||
fmt.Printf("TIC80 Updater: Extracting metadata from source file: %s\n", source_path)
|
||||
meta_data, err := s.GetMetadata(source_path)
|
||||
if err != nil {
|
||||
fmt.Printf("TIC80 Updater: Error extracting metadata: %s\n", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
siteURL := meta_data["site"]
|
||||
software := s.BuildSoftware(meta_data)
|
||||
err = s.softwareRepository.UpdateOrCreate(software)
|
||||
if err != nil {
|
||||
fmt.Printf("TIC80 Updater: Error updating or creating software: %s\n", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
if siteURL != "" {
|
||||
if linkErr := s.externalLinkRepository.UpsertByLabel(software.ID, "Source Code", siteURL); linkErr != nil {
|
||||
fmt.Printf("TIC80 Updater: Error upserting external link: %s\n", linkErr.Error())
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("TIC80 Updater: Creating release for software ID: %d, version: %s\n", software.ID, version)
|
||||
release := Release{
|
||||
SoftwareID: software.ID,
|
||||
Version: version,
|
||||
CartridgePath: cartridge_path,
|
||||
SourcePath: source_path,
|
||||
HTMLFolderPath: html_dir_path,
|
||||
DocsFolderPath: docs_dir_path,
|
||||
}
|
||||
|
||||
s.releaseRepository.CreateIfNotExist(&release)
|
||||
|
||||
fmt.Printf("TIC80 Updater: Successfully processed Lua cartridge: %s\n", name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SoftwareUpdaterTIC80Service) BuildSoftware(metadata map[string]string) *Software {
|
||||
software := &Software{
|
||||
Name: metadata["name"],
|
||||
Title: metadata["title"],
|
||||
Author: metadata["author"],
|
||||
Desc: metadata["desc"],
|
||||
License: metadata["license"],
|
||||
Platform: "tic80",
|
||||
}
|
||||
|
||||
return software
|
||||
}
|
||||
|
||||
func (s *SoftwareUpdaterTIC80Service) GetMetadata(source_path string) (map[string]string, error) {
|
||||
|
||||
file, err := os.Open(source_path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
meta_data := make(map[string]string)
|
||||
scanner := bufio.NewScanner(file)
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if strings.HasPrefix(line, "--") {
|
||||
parts := strings.SplitN(strings.TrimPrefix(line, "--"), ":", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
key := strings.TrimSpace(parts[0])
|
||||
val := strings.TrimSpace(parts[1])
|
||||
meta_data[strings.ToLower(key)] = val
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return meta_data, nil
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
#!/bin/sh
|
||||
go mod download && air -c .air.toml
|
||||
@@ -1,17 +0,0 @@
|
||||
module teletype_softwares
|
||||
|
||||
go 1.21.5
|
||||
|
||||
require (
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.1
|
||||
github.com/go-chi/chi/v5 v5.0.10
|
||||
github.com/google/uuid v1.4.0
|
||||
gorm.io/driver/mysql v1.5.2
|
||||
gorm.io/gorm v1.25.5
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/go-sql-driver/mysql v1.7.0 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
)
|
||||
@@ -1,18 +0,0 @@
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.1 h1:FK6RCIUSfmbnI/imIICmboyQBkOckutaa6R5YYlLZyo=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.1/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
|
||||
github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk=
|
||||
github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
|
||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
|
||||
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
|
||||
gorm.io/driver/mysql v1.5.2 h1:QC2HRskSE75wBuOxe0+iCkyJZ+RqpudsQtqkp+IMuXs=
|
||||
gorm.io/driver/mysql v1.5.2/go.mod h1:pQLhh1Ut/WUAySdTHwBpBv6+JKcj+ua4ZFx1QQTBzb8=
|
||||
gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
|
||||
gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
|
||||
gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||
@@ -1,48 +0,0 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"teletype_softwares/domain"
|
||||
)
|
||||
|
||||
type APISoftwareController struct {
|
||||
softwareService domain.SoftwareServiceInterface
|
||||
}
|
||||
|
||||
func NewAPISoftwareController(software_service domain.SoftwareServiceInterface) *APISoftwareController {
|
||||
return &APISoftwareController{softwareService: software_service}
|
||||
}
|
||||
|
||||
func replaceReleasePaths(release *domain.Release) {
|
||||
release.CartridgePath = strings.ReplaceAll(release.CartridgePath, "/softwares/", "/file/")
|
||||
release.SourcePath = strings.ReplaceAll(release.SourcePath, "/softwares/", "/file/")
|
||||
release.HTMLFolderPath = strings.ReplaceAll(release.HTMLFolderPath, "/softwares/", "/file/")
|
||||
release.DocsFolderPath = strings.ReplaceAll(release.DocsFolderPath, "/softwares/", "/file/")
|
||||
}
|
||||
|
||||
func (c *APISoftwareController) Index(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
softwares, _ := c.softwareService.DetailedList()
|
||||
|
||||
for idx := range softwares.Softwares {
|
||||
softwareShowData := &softwares.Softwares[idx]
|
||||
|
||||
for i := range softwareShowData.Releases {
|
||||
replaceReleasePaths(&softwareShowData.Releases[i])
|
||||
}
|
||||
|
||||
// LatestRelease és WebPlayableRelease a Releases slice elemeire mutat,
|
||||
// ezért ezeket külön is frissíteni kell, mivel értékmásolat kerülhet bele
|
||||
if softwareShowData.LatestRelease != nil {
|
||||
replaceReleasePaths(softwareShowData.LatestRelease)
|
||||
}
|
||||
if softwareShowData.WebPlayableRelease != nil {
|
||||
replaceReleasePaths(softwareShowData.WebPlayableRelease)
|
||||
}
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(softwares)
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"teletype_softwares/domain"
|
||||
)
|
||||
|
||||
type APISoftwareHighlightedController struct {
|
||||
softwareService domain.SoftwareServiceInterface
|
||||
}
|
||||
|
||||
func NewAPISoftwareHighlightedController(software_service domain.SoftwareServiceInterface) *APISoftwareHighlightedController {
|
||||
return &APISoftwareHighlightedController{softwareService: software_service}
|
||||
}
|
||||
|
||||
func (c *APISoftwareHighlightedController) Index(w http.ResponseWriter, r *http.Request) {
|
||||
softwareShowData, err := c.softwareService.GetHighlighted()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if softwareShowData == nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": "no highlighted software found"})
|
||||
return
|
||||
}
|
||||
|
||||
for i := range softwareShowData.Releases {
|
||||
replaceReleasePaths(&softwareShowData.Releases[i])
|
||||
}
|
||||
|
||||
if softwareShowData.LatestRelease != nil {
|
||||
replaceReleasePaths(softwareShowData.LatestRelease)
|
||||
}
|
||||
if softwareShowData.WebPlayableRelease != nil {
|
||||
replaceReleasePaths(softwareShowData.WebPlayableRelease)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(softwareShowData)
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"teletype_softwares/domain"
|
||||
)
|
||||
|
||||
type SoftwareUpdaterController struct {
|
||||
softwareUpdaterService domain.SoftwareUpdaterServiceInterface
|
||||
softwareService domain.SoftwareServiceInterface
|
||||
}
|
||||
|
||||
func NewSoftwareUpdaterController(
|
||||
softwareUpdaterService domain.SoftwareUpdaterServiceInterface,
|
||||
softwareService domain.SoftwareServiceInterface,
|
||||
) *SoftwareUpdaterController {
|
||||
return &SoftwareUpdaterController{
|
||||
softwareUpdaterService: softwareUpdaterService,
|
||||
softwareService: softwareService,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *SoftwareUpdaterController) Update(w http.ResponseWriter, r *http.Request) {
|
||||
secret := r.URL.Query().Get("secret")
|
||||
if secret != os.Getenv("UPDATE_SECRET") {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
platform := r.URL.Query().Get("platform")
|
||||
name := r.URL.Query().Get("name")
|
||||
version := r.URL.Query().Get("version")
|
||||
|
||||
if version == "" {
|
||||
http.Error(w, "Version not provided", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.softwareUpdaterService.Update(platform, name, version); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Write([]byte("Updated"))
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"teletype_softwares/domain"
|
||||
"teletype_softwares/lib/http_utils"
|
||||
)
|
||||
|
||||
func StartHttpServer(domain_instance domain.Domain) {
|
||||
router := NewRouter(
|
||||
NewAPISoftwareController(domain_instance.SoftwareService),
|
||||
NewAPISoftwareHighlightedController(domain_instance.SoftwareService),
|
||||
NewSoftwareUpdaterController(domain_instance.SoftwareUpdaterService, domain_instance.SoftwareService),
|
||||
).Init()
|
||||
|
||||
http_utils.StartGenericHTTPServer(http_utils.StartGenericHTTPServerContext{
|
||||
Router: router,
|
||||
})
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package http
|
||||
|
||||
import "net/http"
|
||||
|
||||
func CORSMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Cross-Origin-Opener-Policy", "same-origin")
|
||||
w.Header().Set("Cross-Origin-Embedder-Policy", "require-corp")
|
||||
w.Header().Set("Cross-Origin-Resource-Policy", "same-origin")
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
type Router struct {
|
||||
apiSoftwareController *APISoftwareController
|
||||
apiSoftwareHighlightedController *APISoftwareHighlightedController
|
||||
softwareUpdaterController *SoftwareUpdaterController
|
||||
}
|
||||
|
||||
func NewRouter(
|
||||
api_software_controller *APISoftwareController,
|
||||
api_software_highlighted_controller *APISoftwareHighlightedController,
|
||||
software_updater_controller *SoftwareUpdaterController,
|
||||
) *Router {
|
||||
return &Router{
|
||||
apiSoftwareController: api_software_controller,
|
||||
apiSoftwareHighlightedController: api_software_highlighted_controller,
|
||||
softwareUpdaterController: software_updater_controller,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Router) Init() *chi.Mux {
|
||||
router := chi.NewRouter()
|
||||
router.Use(CORSMiddleware)
|
||||
|
||||
router.Get("/api/software", r.apiSoftwareController.Index)
|
||||
router.Get("/api/software/highlighted", r.apiSoftwareHighlightedController.Index)
|
||||
router.Get("/update", r.softwareUpdaterController.Update)
|
||||
|
||||
fs_file := http.FileServer(http.Dir("/softwares"))
|
||||
router.Handle("/file/*", http.StripPrefix("/file/", fs_file))
|
||||
|
||||
return router
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
package http_utils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func RespondWithJSON(w http.ResponseWriter, code int, payload interface{}) {
|
||||
response, _ := json.Marshal(payload)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(code)
|
||||
w.Write(response)
|
||||
}
|
||||
|
||||
func GetParam(r *http.Request, name string) string {
|
||||
return chi.URLParam(r, name)
|
||||
}
|
||||
|
||||
func GetParamUUID(r *http.Request, name string) (uuid.UUID, error) {
|
||||
return uuid.Parse(GetParam(r, name))
|
||||
}
|
||||
|
||||
type StartGenericHTTPServerContext struct {
|
||||
Router *chi.Mux
|
||||
}
|
||||
|
||||
func StartGenericHTTPServer(ctx StartGenericHTTPServerContext) {
|
||||
r := chi.NewRouter()
|
||||
r.Use(LoggingMiddleware)
|
||||
|
||||
r.Mount("/", ctx.Router)
|
||||
http.Handle("/", r)
|
||||
log.Fatal(http.ListenAndServe(":80", r))
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package http_utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func LoggingMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Printf("%s %s\n", r.Method, r.RequestURI)
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package mysql_utils
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type DBMockSuite struct {
|
||||
DB *gorm.DB
|
||||
Mock sqlmock.Sqlmock
|
||||
}
|
||||
|
||||
func NewDBMockSuite() *DBMockSuite {
|
||||
var (
|
||||
db *sql.DB
|
||||
gormdb *gorm.DB
|
||||
mock sqlmock.Sqlmock
|
||||
err error
|
||||
)
|
||||
|
||||
db, mock, err = sqlmock.New()
|
||||
|
||||
if err != nil {
|
||||
panic("SQL Mock error")
|
||||
}
|
||||
|
||||
mock.ExpectQuery("SELECT VERSION()").WithArgs().WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(true))
|
||||
|
||||
gormdb, err = gorm.Open(mysql.New(mysql.Config{
|
||||
DSN: "sqlmock_db_0",
|
||||
DriverName: "mysql",
|
||||
Conn: db,
|
||||
}), &gorm.Config{})
|
||||
|
||||
if err != nil {
|
||||
panic("GORM open error")
|
||||
}
|
||||
|
||||
return &DBMockSuite{
|
||||
DB: gormdb,
|
||||
Mock: mock,
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
package mysql_utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type DbConnectData struct {
|
||||
User string
|
||||
Password string
|
||||
Host string
|
||||
Port string
|
||||
Database string
|
||||
}
|
||||
|
||||
func getDbConnectData() DbConnectData {
|
||||
return DbConnectData{
|
||||
User: os.Getenv("DB_USER"),
|
||||
Password: os.Getenv("DB_PASSWORD"),
|
||||
Host: os.Getenv("DB_HOST"),
|
||||
Port: os.Getenv("DB_PORT"),
|
||||
Database: os.Getenv("DB_NAME"),
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func Init() *gorm.DB {
|
||||
c := getDbConnectData()
|
||||
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", c.User, c.Password, c.Host, c.Port, c.Database)
|
||||
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return db
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
package template_utils
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type TemplateCache struct {
|
||||
cache map[string]*template.Template
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
var globalCache *TemplateCache
|
||||
|
||||
func init() {
|
||||
globalCache = &TemplateCache{
|
||||
cache: make(map[string]*template.Template),
|
||||
}
|
||||
}
|
||||
|
||||
func GetTemplate(name string, files ...string) (*template.Template, error) {
|
||||
globalCache.mu.RLock()
|
||||
if tmpl, exists := globalCache.cache[name]; exists {
|
||||
globalCache.mu.RUnlock()
|
||||
return tmpl, nil
|
||||
}
|
||||
globalCache.mu.RUnlock()
|
||||
|
||||
tmpl, err := template.ParseFiles(files...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
globalCache.mu.Lock()
|
||||
globalCache.cache[name] = tmpl
|
||||
globalCache.mu.Unlock()
|
||||
|
||||
return tmpl, nil
|
||||
}
|
||||
|
||||
func ClearCache() {
|
||||
globalCache.mu.Lock()
|
||||
globalCache.cache = make(map[string]*template.Template)
|
||||
globalCache.mu.Unlock()
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"teletype_softwares/domain"
|
||||
"teletype_softwares/http"
|
||||
)
|
||||
|
||||
func main() {
|
||||
domain := domain.NewDomain()
|
||||
http.StartHttpServer(domain)
|
||||
}
|
||||
+14
-8
@@ -138,12 +138,13 @@ services:
|
||||
- proxy
|
||||
- interstack
|
||||
|
||||
catalog:
|
||||
api:
|
||||
build:
|
||||
context: ./apps/catalog
|
||||
context: ./apps/api
|
||||
dockerfile: Dockerfile.development
|
||||
container_name: catalog
|
||||
container_name: api
|
||||
environment:
|
||||
- RAILS_ENV=development
|
||||
- DB_USER=root
|
||||
- DB_PASSWORD=${DB_PASSWORD}
|
||||
- DB_HOST=mysql
|
||||
@@ -151,18 +152,22 @@ services:
|
||||
- DB_NAME=softwares
|
||||
- UPDATE_SECRET=${UPDATE_SECRET}
|
||||
- FILE_CONTAINER_PATH=/softwares
|
||||
- SECRET_KEY_BASE=${SECRET_KEY_BASE:-changeme_in_production_use_long_random_string}
|
||||
- ADMIN_EMAIL=${ADMIN_EMAIL:-admin@teletype.hu}
|
||||
- ADMIN_PASSWORD=${ADMIN_PASSWORD:-password123}
|
||||
depends_on:
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- ./data/softwares:/softwares
|
||||
- ./apps/catalog:/app
|
||||
- ./apps/api:/app
|
||||
- api_bundle:/usr/local/bundle
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.catalog.rule=Host(`${WEBAPP_DOMAIN}`) && (PathPrefix(`/api`) || PathPrefix(`/file`) || PathPrefix(`/update`))"
|
||||
- "traefik.http.routers.catalog.entrypoints=web"
|
||||
- "traefik.http.routers.catalog.priority=10"
|
||||
- "traefik.http.services.catalog.loadbalancer.server.port=80"
|
||||
- "traefik.http.routers.api.rule=Host(`${WEBAPP_DOMAIN}`) && (PathPrefix(`/api`) || PathPrefix(`/file`) || PathPrefix(`/update`) || PathPrefix(`/admin`))"
|
||||
- "traefik.http.routers.api.entrypoints=web"
|
||||
- "traefik.http.routers.api.priority=10"
|
||||
- "traefik.http.services.api.loadbalancer.server.port=3000"
|
||||
- "traefik.docker.network=proxy"
|
||||
networks:
|
||||
- proxy
|
||||
@@ -192,6 +197,7 @@ volumes:
|
||||
woodpecker:
|
||||
mysql-data:
|
||||
softwares:
|
||||
api_bundle:
|
||||
|
||||
networks:
|
||||
proxy:
|
||||
|
||||
+6
-1
@@ -25,4 +25,9 @@ DB_PASSWORD=
|
||||
UPDATE_SECRET=
|
||||
DROP_PASSWORD=
|
||||
WEBAPP_WIKIJS_TOKEN=
|
||||
DISCORD_INVITE_LINK=
|
||||
DISCORD_INVITE_LINK=
|
||||
|
||||
# Rails API (apps/api)
|
||||
ADMIN_EMAIL=
|
||||
ADMIN_PASSWORD=
|
||||
SECRET_KEY_BASE=
|
||||
Reference in New Issue
Block a user