tweaks
This commit is contained in:
@@ -21,9 +21,7 @@ class Api::DownloadsController < ApiController
|
||||
)
|
||||
|
||||
if full_path
|
||||
ext = File.extname(full_path.to_s).delete_prefix(".")
|
||||
mime = Mime::Type.lookup_by_extension(ext) || "application/octet-stream"
|
||||
send_file full_path, disposition: "attachment", type: mime
|
||||
send_file full_path, disposition: "attachment", type: resolve_mime(full_path)
|
||||
else
|
||||
head :not_found
|
||||
end
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
class Api::ImagesController < ApplicationController
|
||||
class Api::ImagesController < ApiController
|
||||
resource_description do
|
||||
short "Images"
|
||||
formats [ "binary" ]
|
||||
@@ -11,9 +11,7 @@ class Api::ImagesController < ApplicationController
|
||||
def show
|
||||
image = ImageService.new.show(ImageShowInputDto.new(id: params[:id]))
|
||||
send_file image.file_path, type: image.content_type, disposition: "inline"
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
render plain: "Not Found", status: :not_found
|
||||
rescue Errno::ENOENT
|
||||
render plain: "Not Found", status: :not_found
|
||||
head :not_found
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,42 +2,6 @@ class Api::SwaggerController < ActionController::Base
|
||||
layout false
|
||||
|
||||
def index
|
||||
render html: swagger_html.html_safe
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def swagger_html
|
||||
<<~HTML
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Teletype Games API</title>
|
||||
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css">
|
||||
<style>
|
||||
body { margin: 0; background: #fafafa; }
|
||||
.swagger-ui .topbar { display: none; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="swagger-ui"></div>
|
||||
<script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
|
||||
<script>
|
||||
SwaggerUIBundle({
|
||||
url: '/api/docs.json?type=swagger',
|
||||
dom_id: '#swagger-ui',
|
||||
deepLinking: true,
|
||||
presets: [
|
||||
SwaggerUIBundle.presets.apis,
|
||||
SwaggerUIBundle.SwaggerUIStandalonePreset
|
||||
],
|
||||
layout: 'BaseLayout'
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
HTML
|
||||
render template: "api/swagger/index", formats: [:html]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,4 +3,20 @@ class ApiController < ActionController::API
|
||||
api_version "1.0"
|
||||
formats [ "json" ]
|
||||
end
|
||||
|
||||
rescue_from StandardError do |e|
|
||||
Rails.logger.error("[#{self.class.name}] #{e.class}: #{e.message}")
|
||||
render json: { error: "Internal server error" }, status: :internal_server_error
|
||||
end
|
||||
|
||||
rescue_from ActiveRecord::RecordNotFound do |e|
|
||||
render json: { error: "Not found" }, status: :not_found
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def resolve_mime(path)
|
||||
ext = File.extname(path.to_s).delete_prefix(".")
|
||||
Mime::Type.lookup_by_extension(ext) || "application/octet-stream"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
class FilesController < ApplicationController
|
||||
skip_forgery_protection
|
||||
|
||||
class FilesController < ApiController
|
||||
resource_description do
|
||||
short "Static files"
|
||||
formats [ "binary" ]
|
||||
@@ -15,10 +13,7 @@ class FilesController < ApplicationController
|
||||
result = FileService.new.show(FileShowInputDto.new(path: params[:path]))
|
||||
case result.type
|
||||
when :redirect then redirect_to result.url, status: :moved_permanently
|
||||
when :file
|
||||
ext = File.extname(result.path.to_s).delete_prefix(".")
|
||||
mime = Mime::Type.lookup_by_extension(ext) || "application/octet-stream"
|
||||
send_file result.path, disposition: "inline", type: mime
|
||||
when :file then send_file result.path, disposition: "inline", type: resolve_mime(result.path)
|
||||
when :not_found then head :not_found
|
||||
end
|
||||
end
|
||||
|
||||
@@ -29,12 +29,13 @@ class UpdateController < ApiController
|
||||
render plain: e.message, status: :bad_request
|
||||
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
|
||||
render plain: "Internal server error", status: :internal_server_error
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def authorized?
|
||||
params[:secret] == ENV.fetch("UPDATE_SECRET", "")
|
||||
secret = request.headers["X-Update-Secret"].presence || params[:secret]
|
||||
secret == ENV.fetch("UPDATE_SECRET", "")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
class Event < ApplicationRecord
|
||||
validates :name, presence: true
|
||||
validates :date, presence: true
|
||||
|
||||
default_scope { where(deleted_at: nil) }
|
||||
|
||||
scope :upcoming, -> { where("date > ?", Time.current).order(:date) }
|
||||
|
||||
@@ -3,6 +3,9 @@ class ExternalLink < ApplicationRecord
|
||||
|
||||
belongs_to :software
|
||||
|
||||
validates :label, presence: true
|
||||
validates :url, presence: true
|
||||
|
||||
default_scope { where(deleted_at: nil) }
|
||||
|
||||
def self.ransackable_attributes(auth_object = nil)
|
||||
|
||||
@@ -2,6 +2,8 @@ class Member < ApplicationRecord
|
||||
belongs_to :image, optional: true
|
||||
has_one :admin_user
|
||||
|
||||
validates :nick, presence: true, uniqueness: true
|
||||
|
||||
def self.ransackable_attributes(auth_object = nil)
|
||||
%w[avatar_filename created_at id image_id motto nick real_nick updated_at]
|
||||
end
|
||||
|
||||
@@ -4,6 +4,9 @@ class Release < ApplicationRecord
|
||||
belongs_to :software
|
||||
has_many :downloads
|
||||
|
||||
validates :version, presence: true
|
||||
validates :version, uniqueness: { scope: :software_id }
|
||||
|
||||
default_scope { where(deleted_at: nil) }
|
||||
|
||||
validate :c64_cannot_be_web_playable
|
||||
|
||||
@@ -11,6 +11,10 @@ class Software < ApplicationRecord
|
||||
accepts_nested_attributes_for :external_links, allow_destroy: true
|
||||
accepts_nested_attributes_for :releases, allow_destroy: true
|
||||
|
||||
validates :name, presence: true, uniqueness: true
|
||||
validates :title, presence: true
|
||||
validates :platform, presence: true
|
||||
|
||||
default_scope { where(deleted_at: nil) }
|
||||
|
||||
def self.ransackable_attributes(auth_object = nil)
|
||||
|
||||
@@ -14,5 +14,5 @@ class ReleaseSerializer < Blueprinter::Base
|
||||
field(:sourcePath) { |r| r.source_path.blank? ? "" : r.source_path.gsub(FILE_PATH_FROM, FILE_PATH_TO) }
|
||||
field(:htmlFolderPath) { |r| r.html_folder_path.blank? ? "" : r.html_folder_path.gsub(FILE_PATH_FROM, FILE_PATH_TO) }
|
||||
field(:docsFolderPath) { |r| r.docs_folder_path.blank? ? "" : r.docs_folder_path.gsub(FILE_PATH_FROM, FILE_PATH_TO) }
|
||||
field(:downloadCount) { |r| r.downloads.size }
|
||||
field(:downloadCount) { |r| r.association(:downloads).loaded? ? r.downloads.size : r.downloads.count }
|
||||
end
|
||||
|
||||
@@ -3,5 +3,5 @@ class SoftwareDetailSerializer < Blueprinter::Base
|
||||
field(:releases) { |_, opts| ReleaseSerializer.render_as_hash(opts[:releases]) }
|
||||
field(:latestRelease) { |_, opts| opts[:latest] ? ReleaseSerializer.render_as_hash(opts[:latest]) : nil }
|
||||
field(:webPlayableRelease) { |_, opts| opts[:web_playable] ? ReleaseSerializer.render_as_hash(opts[:web_playable]) : nil }
|
||||
field(:totalDownloads) { |sw, _| Download.where(release: sw.releases).count }
|
||||
field(:totalDownloads) { |_, opts| opts[:total_downloads] || 0 }
|
||||
end
|
||||
|
||||
@@ -2,14 +2,17 @@ class DownloadService
|
||||
BASE_PATH = Pathname.new(ENV.fetch("FILE_CONTAINER_PATH", "/softwares")).realpath
|
||||
|
||||
def call(path:, ip:, user_agent:, referer:)
|
||||
full_path = BASE_PATH.join(path.to_s)
|
||||
sanitized = path.to_s
|
||||
full_path = BASE_PATH.join(sanitized).realpath
|
||||
return nil unless full_path.to_s.start_with?(BASE_PATH.to_s)
|
||||
return nil unless File.file?(full_path)
|
||||
|
||||
escaped = sanitized.gsub("%", "\\%").gsub("_", "\\_")
|
||||
release = Release.find_by("cartridge_path LIKE ? OR source_path LIKE ?",
|
||||
"%#{path}%", "%#{path}%")
|
||||
"%#{escaped}%", "%#{escaped}%")
|
||||
|
||||
Download.create!(
|
||||
file_path: path,
|
||||
file_path: sanitized,
|
||||
release: release,
|
||||
ip_address: ip,
|
||||
user_agent: user_agent&.truncate(500),
|
||||
|
||||
@@ -3,6 +3,7 @@ class FileService
|
||||
|
||||
def show(input)
|
||||
full_path = BASE_PATH.join(input.path.to_s)
|
||||
return FileResultDto.not_found unless safe_path?(full_path)
|
||||
|
||||
if File.directory?(full_path)
|
||||
index_path = full_path.join("index.html")
|
||||
@@ -16,4 +17,10 @@ class FileService
|
||||
FileResultDto.not_found
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def safe_path?(path)
|
||||
File.exist?(path) && Pathname.new(path).realpath.to_s.start_with?(BASE_PATH.to_s)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,11 +5,13 @@ module SoftwareResponseBuilder
|
||||
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? }
|
||||
total_downloads = releases.sum { |r| r.association(:downloads).loaded? ? r.downloads.size : 0 }
|
||||
|
||||
SoftwareDetailSerializer.render_as_hash(software,
|
||||
releases: sorted,
|
||||
latest: latest,
|
||||
web_playable: web_playable
|
||||
releases: sorted,
|
||||
latest: latest,
|
||||
web_playable: web_playable,
|
||||
total_downloads: total_downloads
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Teletype Games API</title>
|
||||
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css">
|
||||
<style>
|
||||
body { margin: 0; background: #fafafa; }
|
||||
.swagger-ui .topbar { display: none; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="swagger-ui"></div>
|
||||
<script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
|
||||
<script>
|
||||
SwaggerUIBundle({
|
||||
url: '/api/docs.json?type=swagger',
|
||||
dom_id: '#swagger-ui',
|
||||
deepLinking: true,
|
||||
presets: [
|
||||
SwaggerUIBundle.presets.apis,
|
||||
SwaggerUIBundle.SwaggerUIStandalonePreset
|
||||
],
|
||||
layout: 'BaseLayout'
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user