FIX: automatically bust cache for share ai assets (#942)

* FIX: automatically bust cache for share ai assets

CDNs can be configured to strip query params in Discourse
hosting. This is generally safe, but in this case we had
no way of busting the cache using the path.

New design properly caches and properly breaks busts the
cache if asset changes so we don't need to worry about versions

* one day I will set up conditional lint on save :)
This commit is contained in:
Sam 2024-11-22 11:23:15 +11:00 committed by GitHub
parent 2c78961bed
commit 86cf4ccba7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 79 additions and 9 deletions

View File

@ -7,7 +7,8 @@ module DiscourseAi
requires_login only: %i[create update destroy]
before_action :require_site_settings!
skip_before_action :preload_json, :check_xhr, only: %i[show]
skip_before_action :preload_json, :check_xhr, only: %i[show asset]
skip_before_action :verify_authenticity_token, only: ["asset"]
def create
ensure_allowed_create!
@ -50,6 +51,30 @@ module DiscourseAi
end
end
def asset
no_cookies
name = params[:name]
path, content_type =
if name == "share"
%w[share.css text/css]
elsif name == "highlight"
%w[highlight.min.js application/javascript]
else
raise Discourse::NotFound
end
content = File.read(DiscourseAi.public_asset_path("ai-share/#{path}"))
# note, path contains a ":version" which automatically busts the cache
# based on file content, so this is safe
response.headers["Last-Modified"] = 10.years.ago.httpdate
response.headers["Content-Length"] = content.bytesize.to_s
immutable_for 1.year
render plain: content, disposition: :nil, content_type: content_type
end
def preview
ensure_allowed_preview!
data = SharedAiConversation.build_conversation_data(@topic, include_usernames: true)

View File

@ -2,13 +2,29 @@
module DiscourseAi
module AiBot
module SharedAiConversationsHelper
# bump up version when assets change
# long term we may want to change this cause it is hard to remember
# to bump versions, but for now this does the job
VERSION = "1"
# keeping it here for caching
def self.share_asset_url(asset_name)
if !%w[share.css highlight.js].include?(asset_name)
raise StandardError, "unknown asset type #{asset_name}"
end
def share_asset_url(short_path)
::UrlHelper.absolute("/plugins/discourse-ai/ai-share/#{short_path}?#{VERSION}")
@urls ||= {}
url = @urls[asset_name]
return url if url
path = asset_name
path = "highlight.min.js" if asset_name == "highlight.js"
content = File.read(DiscourseAi.public_asset_path("ai-share/#{path}"))
sha1 = Digest::SHA1.hexdigest(content)
url = "/discourse-ai/ai-bot/shared-ai-conversations/asset/#{sha1}/#{asset_name}"
@urls[asset_name] = GlobalPath.cdn_path(url)
end
def share_asset_url(asset_name)
DiscourseAi::AiBot::SharedAiConversationsHelper.share_asset_url(asset_name)
end
end
end

View File

@ -10,7 +10,7 @@
<meta name="twitter:title" content="<%= I18n.t("discourse_ai.share_ai.title", title: @shared_conversation.title, site_name: SiteSetting.title) %>">
<meta name="twitter:description" content="<%= @shared_conversation.formatted_excerpt %>">
<meta name="viewport" content="width=device-width">
<link rel="stylesheet" href="<%= ::UrlHelper.absolute("/plugins/discourse-ai/ai-share/share.css?v=1") %>">
<link rel="stylesheet" href="<%= share_asset_url("share.css") %>">
</head>
<body>
<header class="site-header">
@ -54,7 +54,7 @@
</article>
<% end %>
</section>
<script src="<%= share_asset_url("highlight.min.js") %>" nonce="<%= csp_nonce_placeholder %>" ></script>
<script src="<%= share_asset_url("highlight.js") %>" nonce="<%= csp_nonce_placeholder %>" ></script>
<script nonce="<%= csp_nonce_placeholder %>">
document.querySelectorAll('pre code').forEach((el) => {
hljs.highlightElement(el);

View File

@ -30,6 +30,7 @@ DiscourseAi::Engine.routes.draw do
post "/" => "shared_ai_conversations#create"
delete "/:share_key" => "shared_ai_conversations#destroy"
get "/:share_key" => "shared_ai_conversations#show"
get "/asset/:version/:name" => "shared_ai_conversations#asset"
get "/preview/:topic_id" => "shared_ai_conversations#preview"
end

View File

@ -43,6 +43,10 @@ register_asset "stylesheets/modules/ai-bot/common/ai-artifact.scss"
module ::DiscourseAi
PLUGIN_NAME = "discourse-ai"
def self.public_asset_path(name)
File.expand_path(File.join(__dir__, "public", name))
end
end
Rails.autoloaders.main.push_dir(File.join(__dir__, "lib"), namespace: ::DiscourseAi)

View File

@ -294,6 +294,30 @@ RSpec.describe DiscourseAi::AiBot::SharedAiConversationsController do
end
end
describe "GET asset" do
let(:helper) { Class.new { extend DiscourseAi::AiBot::SharedAiConversationsHelper } }
it "renders highlight js correctly" do
get helper.share_asset_url("highlight.js")
expect(response).to be_successful
expect(response.headers["Content-Type"]).to eq("application/javascript; charset=utf-8")
js = File.read(DiscourseAi.public_asset_path("ai-share/highlight.min.js"))
expect(response.body).to eq(js)
end
it "renders css correctly" do
get helper.share_asset_url("share.css")
expect(response).to be_successful
expect(response.headers["Content-Type"]).to eq("text/css; charset=utf-8")
css = File.read(DiscourseAi.public_asset_path("ai-share/share.css"))
expect(response.body).to eq(css)
end
end
describe "GET preview" do
it "denies preview from logged out users" do
get "#{path}/preview/#{user_pm_share.id}.json"