diff --git a/assets/js/autocomplete/__tests__/context.ts b/assets/js/autocomplete/__tests__/context.ts index e66a8d429..95da41a9b 100644 --- a/assets/js/autocomplete/__tests__/context.ts +++ b/assets/js/autocomplete/__tests__/context.ts @@ -34,6 +34,7 @@ export class TestContext { constructor(fakeAutocompleteResponse: Response) { this.fakeAutocompleteResponse = fakeAutocompleteResponse; + window.booru.autocompleteFileUrl = '/autocomplete/compiled'; vi.useFakeTimers().setSystemTime(0); fetchMock.enableMocks(); @@ -179,7 +180,7 @@ export class TestContext { await vi.runAllTimersAsync(); } - snapPequests() { + snapRequests() { const snapshot = vi.mocked(fetch).mock.calls.map(([input]) => { const request = input as unknown as Request; const meta: Record = {}; @@ -283,10 +284,10 @@ export const autocompleteTest = test.extend<{ ctx: TestContext }>({ // Initialize the lazy autocomplete index cache await ctx.focusInput(); - expect(ctx.snapPequests()).toMatchInlineSnapshot(` + expect(ctx.snapRequests()).toMatchInlineSnapshot(` [ { - "dest": "GET http://localhost:3000/autocomplete/compiled?vsn=2&key=1970-0-1", + "dest": "GET http://localhost:3000/autocomplete/compiled", "meta": { "cache": "force-cache", "credentials": "omit", diff --git a/assets/js/autocomplete/client.ts b/assets/js/autocomplete/client.ts index 43fea48c8..3bb99eca1 100644 --- a/assets/js/autocomplete/client.ts +++ b/assets/js/autocomplete/client.ts @@ -59,11 +59,7 @@ export class AutocompleteClient { * Issues a GET request to fetch the compiled autocomplete index. */ async getCompiledAutocomplete(): Promise { - const now = new Date(); - const key = `${now.getUTCFullYear()}-${now.getUTCMonth()}-${now.getUTCDate()}`; - - const response = await this.http.fetch(`/autocomplete/compiled`, { - query: { vsn: '2', key }, + const response = await this.http.fetch(window.booru.autocompleteFileUrl, { credentials: 'omit', cache: 'force-cache', }); diff --git a/assets/js/booru.ts b/assets/js/booru.ts index ed0e9828b..94ec140b8 100644 --- a/assets/js/booru.ts +++ b/assets/js/booru.ts @@ -81,6 +81,10 @@ export interface BooruObject { * Indicates whether sensitive staff-only info should be hidden or not. */ hideStaffTools: boolean; + /** + * URL to the local autocomplete binary + */ + autocompleteFileUrl: string; /** * List of image IDs in the current gallery. */ diff --git a/config/runtime.exs b/config/runtime.exs index c53ab698f..ed6ec0eee 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -11,6 +11,8 @@ config :bcrypt_elixir, log_rounds: String.to_integer(System.get_env("BCRYPT_ROUNDS", "12")) config :philomena, + autocomplete_file_root: System.fetch_env!("AUTOCOMPLETE_FILE_ROOT"), + autocomplete_url_root: System.fetch_env!("AUTOCOMPLETE_URL_ROOT"), anonymous_name_salt: System.fetch_env!("ANONYMOUS_NAME_SALT"), hcaptcha_secret_key: System.fetch_env!("HCAPTCHA_SECRET_KEY"), hcaptcha_site_key: System.fetch_env!("HCAPTCHA_SITE_KEY"), diff --git a/docker-compose.yml b/docker-compose.yml index 620036ec0..088c06eff 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -31,11 +31,13 @@ services: - PASSWORD_PEPPER=dn2e0EpZrvBLoxUM3gfQveBhjf0bG/6/bYhrOyq3L3hV9hdo/bimJ+irbDWsuXLP - TUMBLR_API_KEY=fuiKNFp9vQFvjLNvx4sUwti4Yb5yGutBN4Xh10LXZhhRKjWlV4 - OTP_SECRET_KEY=Wn7O/8DD+qxL0X4X7bvT90wOkVGcA90bIHww4twR03Ci//zq7PnMw8ypqyyT/b/C + - AUTOCOMPLETE_FILE_ROOT=autocomplete - ADVERT_FILE_ROOT=adverts - AVATAR_FILE_ROOT=avatars - BADGE_FILE_ROOT=badges - IMAGE_FILE_ROOT=images - TAG_FILE_ROOT=tags + - AUTOCOMPLETE_URL_ROOT=/autocomplete-bin - AVATAR_URL_ROOT=/avatars - ADVERT_URL_ROOT=/spns - IMAGE_URL_ROOT=/img diff --git a/docker/app/run-development b/docker/app/run-development index 6e2fe2a0e..8122f3e7e 100755 --- a/docker/app/run-development +++ b/docker/app/run-development @@ -17,7 +17,7 @@ background() { step mix run -e 'Philomena.Release.verify_artist_links()' > /dev/null step mix run -e 'Philomena.Release.update_stats()' > /dev/null step mix run -e 'Philomena.Release.clean_moderation_logs()' > /dev/null - step mix run -e 'Philomena.Release.generate_autocomplete()' > /dev/null + step mix run -e 'Philomena.Release.generate_and_prune_autocomplete()' > /dev/null step mix run -e 'Philomena.Release.clean_tags()' > /dev/null sleep 300 diff --git a/docker/production/.env-example b/docker/production/.env-example index e5c8cf250..8d0922223 100644 --- a/docker/production/.env-example +++ b/docker/production/.env-example @@ -6,6 +6,7 @@ TUMBLR_API_KEY=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa PROXY_HOST=http://tinyproxy:3128 DATABASE_URL=ecto://philomena@postgres/philomena IMAGE_URL_ROOT=https://philomena-cdn.example/img +AUTOCOMPLETE_URL_ROOT=https://philomena-cdn.example/autocomplete AVATAR_URL_ROOT=https://philomena-cdn.example/avatars ADVERT_URL_ROOT=https://philomena-cdn.example/spns BADGE_URL_ROOT=https://philomena-cdn.example/badges @@ -33,6 +34,7 @@ REDIS_HOST=valkey OPENSEARCH_URL=http://opensearch:9200 MEDIAPROC_ADDR=mediaproc:1500 +AUTOCOMPLETE_FILE_ROOT=autocomplete ADVERT_FILE_ROOT=adverts AVATAR_FILE_ROOT=avatars BADGE_FILE_ROOT=badges diff --git a/docker/production/run-cron-daily b/docker/production/run-cron-daily index 618d2bf9a..9f2808ade 100755 --- a/docker/production/run-cron-daily +++ b/docker/production/run-cron-daily @@ -3,5 +3,5 @@ set -e export RELEASE_NODE=philomena_daily -philomena eval 'Philomena.Release.generate_autocomplete()' +philomena eval 'Philomena.Release.generate_and_prune_autocomplete()' philomena eval 'Philomena.Release.clean_tags()' diff --git a/docker/production/web/Caddyfile b/docker/production/web/Caddyfile index 7ee7f21c8..cb74b7e3d 100644 --- a/docker/production/web/Caddyfile +++ b/docker/production/web/Caddyfile @@ -146,6 +146,7 @@ @imgview path_regexp ^/img/view/(.+)/([0-9]+).*\.([A-Za-z0-9]+)$ @img path_regexp ^/img/(.+)$ @spns path_regexp ^/spns/(.+)$ + @autocomplete path_regexp ^/autocomplete/(.+)$ @avatars path_regexp ^/avatars/(.+)$ @badges path_regexp ^/badges/(.+)$ @tags path_regexp ^/tags/(.+)$ @@ -155,6 +156,7 @@ import s3path @imgdownload /images/{re.imgdownload.1}/{re.imgdownload.2}/full.{re.imgdownload.3} import s3path @imgview /images/{re.imgview.1}/{re.imgview.2}/full.{re.imgview.3} import s3path @img /images/{re.img.1} + import s3path @autocomplete /autocomplete/{re.autocomplete.1} import s3path @avatars /avatars/{re.avatars.1} import s3path @spns /adverts/{re.spns.1} import s3path @badges /badges/{re.badges.1} diff --git a/docker/web/config/Caddyfile b/docker/web/config/Caddyfile index 3cd19e332..8e9dabbd3 100644 --- a/docker/web/config/Caddyfile +++ b/docker/web/config/Caddyfile @@ -23,6 +23,7 @@ @imgview path_regexp ^/img/view/(.+)/([0-9]+).*\.([A-Za-z0-9]+)$ @img path_regexp ^/img/(.+)$ @spns path_regexp ^/spns/(.+)$ + @autocomplete path_regexp ^/autocomplete-bin/(.+)$ @avatars path_regexp ^/avatars/(.+)$ @badges path_regexp ^/badge-img/(.+)$ @tags path_regexp ^/tag-img/(.+)$ @@ -30,6 +31,7 @@ import s3path @imgdownload /images/{re.imgdownload.1}/{re.imgdownload.2}/full.{re.imgdownload.3} import s3path @imgview /images/{re.imgview.1}/{re.imgview.2}/full.{re.imgview.3} import s3path @img /images/{re.img.1} + import s3path @autocomplete /autocomplete/{re.autocomplete.1} import s3path @avatars /avatars/{re.avatars.1} import s3path @spns /adverts/{re.spns.1} import s3path @badges /badges/{re.badges.1} diff --git a/lib/philomena/autocomplete.ex b/lib/philomena/autocomplete.ex index e496ffc49..07a55bb37 100644 --- a/lib/philomena/autocomplete.ex +++ b/lib/philomena/autocomplete.ex @@ -12,9 +12,10 @@ defmodule Philomena.Autocomplete do alias Philomena.Autocomplete.Autocomplete alias Philomena.Autocomplete.Generator + alias Philomena.Autocomplete.Uploader @doc """ - Gets the current local autocompletion binary. + Gets the current local autocompletion information. Returns nil if the binary is not currently generated. @@ -35,20 +36,52 @@ defmodule Philomena.Autocomplete do end @doc """ - Creates a new local autocompletion binary, replacing any which currently exist. + Creates a new local autocompletion binary and prunes existing binaries. + """ + def generate_and_prune_autocomplete! do + generate_autocomplete!() + prune_autocomplete!() + end + + @doc """ + Creates a new local autocompletion binary. """ def generate_autocomplete! do - ac_file = Generator.generate() + path = generate_autocomplete_file!() - # Insert the autocomplete binary - new_ac = - %Autocomplete{} - |> Autocomplete.changeset(%{content: ac_file}) - |> Repo.insert!() + %Autocomplete{} + |> Uploader.prepare_upload(path) + |> Repo.insert!() + |> Uploader.persist_upload() + end - # Remove anything older + @doc """ + Removes old autocomplete binaries. + """ + def prune_autocomplete! do Autocomplete - |> where([ac], ac.created_at < ^new_ac.created_at) - |> Repo.delete_all() + |> where([ac], ac.created_at < ago(1, "week")) + |> Repo.all() + |> Enum.each(&delete_autocomplete/1) + end + + defp delete_autocomplete(%Autocomplete{} = autocomplete) do + if autocomplete.file do + Uploader.unpersist_upload(autocomplete) + + Autocomplete + |> where([ac], ac.file == ^autocomplete.file) + |> Repo.delete_all() + end + end + + # sobelow_skip ["Traversal.FileModule"] + defp generate_autocomplete_file! do + content = Generator.generate() + file = Briefly.create!() + + File.write!(file, content) + + file end end diff --git a/lib/philomena/autocomplete/autocomplete.ex b/lib/philomena/autocomplete/autocomplete.ex index 6be4dcf2c..715bdbb93 100644 --- a/lib/philomena/autocomplete/autocomplete.ex +++ b/lib/philomena/autocomplete/autocomplete.ex @@ -6,14 +6,16 @@ defmodule Philomena.Autocomplete.Autocomplete do @primary_key false schema "autocomplete" do - field :content, :binary + field :file, :string + field :uploaded_file, :string, virtual: true + field :removed_file, :string, virtual: true timestamps(inserted_at: :created_at, updated_at: false, type: :utc_datetime) end @doc false def changeset(autocomplete, attrs) do autocomplete - |> cast(attrs, [:content]) - |> validate_required([:content]) + |> cast(attrs, [:file, :uploaded_file, :removed_file]) + |> validate_required([:file]) end end diff --git a/lib/philomena/autocomplete/uploader.ex b/lib/philomena/autocomplete/uploader.ex new file mode 100644 index 000000000..7cd2b2291 --- /dev/null +++ b/lib/philomena/autocomplete/uploader.ex @@ -0,0 +1,37 @@ +defmodule Philomena.Autocomplete.Uploader do + @moduledoc """ + Upload callback logic for Autocomplete. + """ + + alias Philomena.Autocomplete.Autocomplete + alias PhilomenaMedia.Filename + alias PhilomenaMedia.Uploader + + @field_name "file" + + def prepare_upload(autocomplete, path) do + storage_key = Filename.build("bin") + + Uploader.prepare_upload( + autocomplete, + @field_name, + storage_key, + path, + &Autocomplete.changeset/2 + ) + end + + def persist_upload(autocomplete) do + Uploader.persist_upload(autocomplete, autocomplete_file_root(), @field_name) + end + + def unpersist_upload(autocomplete) do + autocomplete + |> Map.put(:removed_file, autocomplete.file) + |> Uploader.unpersist_old_upload(autocomplete_file_root(), @field_name) + end + + defp autocomplete_file_root do + Application.get_env(:philomena, :autocomplete_file_root) + end +end diff --git a/lib/philomena/release.ex b/lib/philomena/release.ex index fccf7ae64..7daa06cd1 100644 --- a/lib/philomena/release.ex +++ b/lib/philomena/release.ex @@ -44,9 +44,9 @@ defmodule Philomena.Release do Philomena.ModerationLogs.cleanup!() end - def generate_autocomplete do + def generate_and_prune_autocomplete do start_app() - Philomena.Autocomplete.generate_autocomplete!() + Philomena.Autocomplete.generate_and_prune_autocomplete!() end def clean_tags do diff --git a/lib/philomena_media/uploader.ex b/lib/philomena_media/uploader.ex index 7248d92c6..92dd92017 100644 --- a/lib/philomena_media/uploader.ex +++ b/lib/philomena_media/uploader.ex @@ -1,6 +1,6 @@ defmodule PhilomenaMedia.Uploader do @moduledoc """ - Upload and processing callback logic for media files. + Upload and processing callback logic for files. To use the uploader, the target schema must be modified to add at least the following fields, assuming the name of the field to write to the database is `foo`: @@ -114,12 +114,57 @@ defmodule PhilomenaMedia.Uploader do @type schema :: struct() @type schema_or_changeset :: struct() | Ecto.Changeset.t() + @type storage_key :: String.t() @type field_name :: String.t() @type file_root :: String.t() @doc """ - Performs analysis of the specified `m:Plug.Upload`, and invokes a changeset callback on the schema - or changeset passed in. + Prepares the specified file path for persistence to the storage layer, and invokes a + changeset callback on the schema or changeset passed in with the properties of the upload. + + The storage key (location to write the persisted file) is set by the assignment to the + schema's `field_name`, and passed with the `field_name` attribute into the changeset callback. + + Additional attributes to be passed to the changeset callback may be passed in `extra_attrs`. + + This function is used to handle direct file uploads without validation for generated files. + Use `analyze_upload/4` for handling user-controlled media uploads. + + This function does not persist an upload to storage. + """ + @spec prepare_upload( + schema_or_changeset(), + field_name(), + storage_key(), + Path.t(), + (schema_or_changeset(), map() -> Ecto.Changeset.t()), + map() + ) :: Ecto.Changeset.t() + def prepare_upload( + schema_or_changeset, + field_name, + storage_key, + upload_file_path, + changeset_fn, + extra_attrs \\ %{} + ) do + removed_storage_key = + schema_or_changeset + |> change() + |> get_changeset_field(field_name) + + attributes = + extra_attrs + |> Map.put(field_name, storage_key) + |> Map.put(uploaded_field_name(field_name), upload_file_path) + |> Map.put(removed_field_name(field_name), removed_storage_key) + + changeset_fn.(schema_or_changeset, attributes) + end + + @doc """ + Performs analysis of the specified image in the `m:Plug.Upload`, and invokes a changeset callback on the + schema or changeset passed in with the properties of the upload. The file name which will be written to is set by the assignment to the schema's `field_name`, and the below attributes are prefixed by the `field_name`. @@ -134,7 +179,7 @@ defmodule PhilomenaMedia.Uploader do * `format` (String) - the file extension, one of `~w(gif jpg png svg webm)`, determined by reading the file * `mime_type` (String) - the file's sniffed MIME type, determined by reading the file * `duration` (float) - the duration of the media file - * `aspect_ratio` (float) - width divided by height. + * `aspect_ratio` (float) - width divided by height * `orig_sha512_hash` (String) - the SHA-512 hash of the file * `sha512_hash` (String) - the SHA-512 hash of the file * `is_animated` (boolean) - whether the file contains animation @@ -185,13 +230,12 @@ defmodule PhilomenaMedia.Uploader do |> validate_inclusion(:image_width, 699..729) end - The key (location to write the persisted file) is passed with the `field_name` attribute into the - changeset callback. The key is calculated using the current date, a UUID, and the computed - extension. A file uploaded may therefore be given a key such as + The storage key is calculated using the current date, a UUID, and the computed extension. + A file uploaded may therefore be given a key such as `2024/1/1/0bce8eea-17e0-11ef-b7d4-0242ac120006.png`. See `PhilomenaMedia.Filename.build/1` for the actual construction. - This function does not persist an upload to storage. + Attributes are passed to the changeset function as in `prepare_upload/5`. See the module documentation for a complete example. @@ -212,11 +256,6 @@ defmodule PhilomenaMedia.Uploader do def analyze_upload(schema_or_changeset, field_name, upload_parameter, changeset_fn) do with {:ok, analysis} <- Analyzers.analyze_upload(upload_parameter), analysis <- extra_attributes(analysis, upload_parameter) do - removed = - schema_or_changeset - |> change() - |> get_field(field(field_name)) - attributes = %{ "name" => analysis.name, @@ -233,11 +272,15 @@ defmodule PhilomenaMedia.Uploader do "is_animated" => analysis.animated? } |> prefix_attributes(field_name) - |> Map.put(field_name, analysis.new_name) - |> Map.put(upload_key(field_name), upload_parameter.path) - |> Map.put(remove_key(field_name), removed) - changeset_fn.(schema_or_changeset, attributes) + prepare_upload( + schema_or_changeset, + field_name, + analysis.storage_key, + upload_parameter.path, + changeset_fn, + attributes + ) else {:unsupported_mime, mime} -> attributes = prefix_attributes(%{"mime_type" => mime}, field_name) @@ -252,7 +295,7 @@ defmodule PhilomenaMedia.Uploader do Writes the file to permanent storage. This should be the second-to-last step before completing a file operation. - The key (location to write the persisted file) is fetched from the schema by `field_name`. + The storage key (location to write the persisted file) is fetched from the schema by `field_name`. This is then prefixed with the `file_root` specified by the caller. Finally, the file is written to storage. @@ -268,15 +311,15 @@ defmodule PhilomenaMedia.Uploader do """ @spec persist_upload(schema(), file_root(), field_name()) :: :ok def persist_upload(schema, file_root, field_name) do - source = Map.get(schema, field(upload_key(field_name))) - dest = Map.get(schema, field(field_name)) + source = get_schema_field(schema, uploaded_field_name(field_name)) + dest = get_schema_field(schema, field_name) target = Path.join(file_root, dest) persist_file(target, source) end @doc """ - Persist an arbitrary file to storage with the given key. + Persist an arbitrary file to storage with the given storage key. > #### Warning {: .warning} > @@ -284,7 +327,7 @@ defmodule PhilomenaMedia.Uploader do > to allow overriding the key. If you do not need to override the key, use > `persist_upload/3` instead. - The key (location to write the persisted file) and the file path to upload are passed through + The storage key and the file path to upload are passed through to `PhilomenaMedia.Objects.upload/2` without modification. See the definition of that function for additional details. @@ -303,9 +346,8 @@ defmodule PhilomenaMedia.Uploader do Removes the old file from permanent storage. This should be the last step in completing a file operation. - The key (location to write the persisted file) is fetched from the schema by `field_name`. - This is then prefixed with the `file_root` specified by the caller. Finally, the file is - purged from storage. + The storage key is fetched from the schema by `field_name`. This is then prefixed with + the `file_root` specified by the caller. Finally, the file is purged from storage. See the module documentation for a complete example. @@ -320,7 +362,7 @@ defmodule PhilomenaMedia.Uploader do @spec unpersist_old_upload(schema(), file_root(), field_name()) :: :ok def unpersist_old_upload(schema, file_root, field_name) do schema - |> Map.get(field(remove_key(field_name))) + |> get_schema_field(removed_field_name(field_name)) |> try_remove(file_root) end @@ -330,7 +372,7 @@ defmodule PhilomenaMedia.Uploader do stat = File.stat!(path) sha512 = Sha512.file(path) - new_name = Filename.build(analysis.extension) + storage_key = Filename.build(analysis.extension) analysis |> Map.put(:size, stat.size) @@ -338,7 +380,7 @@ defmodule PhilomenaMedia.Uploader do |> Map.put(:width, width) |> Map.put(:height, height) |> Map.put(:sha512, sha512) - |> Map.put(:new_name, new_name) + |> Map.put(:storage_key, storage_key) |> Map.put(:aspect_ratio, aspect_ratio) end @@ -355,9 +397,13 @@ defmodule PhilomenaMedia.Uploader do defp prefix_attributes(map, prefix), do: Map.new(map, fn {key, value} -> {"#{prefix}_#{key}", value} end) - defp upload_key(field_name), do: "uploaded_#{field_name}" + defp uploaded_field_name(field_name), do: "uploaded_#{field_name}" + + defp removed_field_name(field_name), do: "removed_#{field_name}" - defp remove_key(field_name), do: "removed_#{field_name}" + defp get_schema_field(schema, field_name), + do: Map.get(schema, String.to_existing_atom(field_name)) - defp field(field_name), do: String.to_existing_atom(field_name) + defp get_changeset_field(changeset, field_name), + do: get_field(changeset, String.to_existing_atom(field_name)) end diff --git a/lib/philomena_web/controllers/autocomplete/compiled_controller.ex b/lib/philomena_web/controllers/autocomplete/compiled_controller.ex deleted file mode 100644 index 7571e03e6..000000000 --- a/lib/philomena_web/controllers/autocomplete/compiled_controller.ex +++ /dev/null @@ -1,23 +0,0 @@ -defmodule PhilomenaWeb.Autocomplete.CompiledController do - use PhilomenaWeb, :controller - - alias Philomena.Autocomplete - - def show(conn, _params) do - autocomplete = Autocomplete.get_autocomplete() - - case autocomplete do - nil -> - conn - |> put_status(:not_found) - |> configure_session(drop: true) - |> text("") - - %{content: content} -> - conn - |> put_resp_header("cache-control", "public, max-age=86400") - |> configure_session(drop: true) - |> resp(200, content) - end - end -end diff --git a/lib/philomena_web/plugs/content_security_policy_plug.ex b/lib/philomena_web/plugs/content_security_policy_plug.ex index 32ff15e47..a2dc278d3 100644 --- a/lib/philomena_web/plugs/content_security_policy_plug.ex +++ b/lib/philomena_web/plugs/content_security_policy_plug.ex @@ -26,7 +26,7 @@ defmodule PhilomenaWeb.ContentSecurityPolicyPlug do csp_config = [ {:default_src, ["'self'"]}, {:script_src, [default_script_src(conn.host) | script_src]}, - {:connect_src, [default_connect_src(conn.host)]}, + {:connect_src, [default_connect_src(conn.host), cdn_uri]}, {:style_src, [default_style_src() | style_src]}, {:object_src, ["'none'"]}, {:frame_ancestors, ["'none'"]}, diff --git a/lib/philomena_web/router.ex b/lib/philomena_web/router.ex index 15678a26c..c4e94b9b3 100644 --- a/lib/philomena_web/router.ex +++ b/lib/philomena_web/router.ex @@ -482,7 +482,6 @@ defmodule PhilomenaWeb.Router do scope "/autocomplete", Autocomplete, as: :autocomplete do resources "/tags", TagController, only: [:show], singleton: true - resources "/compiled", CompiledController, only: [:show], singleton: true end scope "/fetch", Fetch, as: :fetch do diff --git a/lib/philomena_web/views/layout_view.ex b/lib/philomena_web/views/layout_view.ex index 57e8cea8f..6b5d51cc4 100644 --- a/lib/philomena_web/views/layout_view.ex +++ b/lib/philomena_web/views/layout_view.ex @@ -5,6 +5,7 @@ defmodule PhilomenaWeb.LayoutView do alias PhilomenaWeb.ImageView alias Philomena.Config alias Philomena.Users.User + alias Philomena.Autocomplete alias Plug.Conn @themes User.themes() @@ -52,6 +53,7 @@ defmodule PhilomenaWeb.LayoutView do def clientside_data(conn) do conn = Conn.fetch_cookies(conn) + autocomplete = Autocomplete.get_autocomplete() extra = Map.get(conn.assigns, :clientside_data, []) interactions = Map.get(conn.assigns, :interactions, []) user = conn.assigns.current_user @@ -75,7 +77,8 @@ defmodule PhilomenaWeb.LayoutView do fancy_tag_upload: if(user, do: user.fancy_tag_field_on_upload, else: "true") |> to_string(), interactions: JSON.encode!(interactions), ignored_tag_list: JSON.encode!(ignored_tag_list(conn.assigns[:tags])), - hide_staff_tools: conn.cookies["hide_staff_tools"] |> to_string() + hide_staff_tools: conn.cookies["hide_staff_tools"] |> to_string(), + autocomplete_file_url: if(autocomplete, do: autocomplete_file_url(autocomplete), else: nil) ] data = Keyword.merge(data, extra) @@ -167,4 +170,12 @@ defmodule PhilomenaWeb.LayoutView do _ -> "" end end + + def autocomplete_file_url(autocomplete) do + "#{autocomplete_url_root()}/#{autocomplete.file}" + end + + def autocomplete_url_root do + Application.get_env(:philomena, :autocomplete_url_root) + end end diff --git a/priv/repo/migrations/20260523031354_convert_autocomplete_to_uploaded.exs b/priv/repo/migrations/20260523031354_convert_autocomplete_to_uploaded.exs new file mode 100644 index 000000000..27322f8e4 --- /dev/null +++ b/priv/repo/migrations/20260523031354_convert_autocomplete_to_uploaded.exs @@ -0,0 +1,10 @@ +defmodule Philomena.Repo.Migrations.ConvertAutocompleteToUploaded do + use Ecto.Migration + + def change do + alter table(:autocomplete) do + remove :content, :binary + add :file, :string + end + end +end diff --git a/priv/repo/structure.sql b/priv/repo/structure.sql index da2095507..f0072f9ff 100644 --- a/priv/repo/structure.sql +++ b/priv/repo/structure.sql @@ -2,10 +2,10 @@ -- PostgreSQL database dump -- -\restrict hgf9CfMeD1rU4CvxcBOzalhPAIJmwKqP0y1eMNujQOmt5EMX2Gnh4Z2aHUUnVGK +\restrict nOqN7gS2zjoE8aNiKCTVfnfcMKFhyhuGAdVRERGqnwhJ1yxd6Iy7PJxFva9Su1s --- Dumped from database version 18.0 --- Dumped by pg_dump version 18.0 +-- Dumped from database version 18.3 +-- Dumped by pg_dump version 18.3 SET statement_timeout = 0; SET lock_timeout = 0; @@ -122,8 +122,8 @@ ALTER SEQUENCE public.artist_links_id_seq OWNED BY public.artist_links.id; -- CREATE TABLE public.autocomplete ( - content bytea NOT NULL, - created_at timestamp without time zone NOT NULL + created_at timestamp without time zone NOT NULL, + file character varying(255) ); @@ -5565,7 +5565,7 @@ ALTER TABLE ONLY public.users -- PostgreSQL database dump complete -- -\unrestrict hgf9CfMeD1rU4CvxcBOzalhPAIJmwKqP0y1eMNujQOmt5EMX2Gnh4Z2aHUUnVGK +\unrestrict nOqN7gS2zjoE8aNiKCTVfnfcMKFhyhuGAdVRERGqnwhJ1yxd6Iy7PJxFva9Su1s INSERT INTO public."schema_migrations" (version) VALUES (20200503002523); INSERT INTO public."schema_migrations" (version) VALUES (20200607000511); @@ -5592,11 +5592,12 @@ INSERT INTO public."schema_migrations" (version) VALUES (20240728191353); INSERT INTO public."schema_migrations" (version) VALUES (20240818182358); INSERT INTO public."schema_migrations" (version) VALUES (20241216165826); INSERT INTO public."schema_migrations" (version) VALUES (20250407021536); +INSERT INTO public."schema_migrations" (version) VALUES (20250430092058); +INSERT INTO public."schema_migrations" (version) VALUES (20250501023533); INSERT INTO public."schema_migrations" (version) VALUES (20250501174007); INSERT INTO public."schema_migrations" (version) VALUES (20250502110018); INSERT INTO public."schema_migrations" (version) VALUES (20250507183410); INSERT INTO public."schema_migrations" (version) VALUES (20250617121030); INSERT INTO public."schema_migrations" (version) VALUES (20250617122513); INSERT INTO public."schema_migrations" (version) VALUES (20251103173014); -INSERT INTO public."schema_migrations" (version) VALUES (20250430092058); -INSERT INTO public."schema_migrations" (version) VALUES (20250501023533); +INSERT INTO public."schema_migrations" (version) VALUES (20260523031354);