From 75fe720f21a2a4753d9d03bea73580a7e7b98986 Mon Sep 17 00:00:00 2001 From: trisua Date: Thu, 21 Aug 2025 00:30:29 -0400 Subject: [PATCH] add: move upload server to buckets --- Cargo.lock | 85 +++++++- README.md | 4 +- crates/app/src/public/css/style.css | 1 + crates/app/src/public/html/components.lisp | 12 +- crates/app/src/public/html/economy/ad.lisp | 2 +- crates/app/src/public/html/economy/edit.lisp | 2 +- .../app/src/public/html/economy/product.lisp | 2 +- .../src/public/html/littleweb/browser.lisp | 4 +- crates/app/src/public/html/macros.lisp | 2 +- .../app/src/public/html/profile/settings.lisp | 6 +- crates/app/src/public/js/atto.js | 2 +- crates/app/src/routes/api/v1/ads.rs | 17 +- .../src/routes/api/v1/communities/emojis.rs | 23 ++- .../src/routes/api/v1/communities/posts.rs | 25 ++- crates/app/src/routes/api/v1/mod.rs | 2 - crates/app/src/routes/api/v1/products.rs | 42 ++-- crates/app/src/routes/api/v1/uploads.rs | 98 +++------ crates/app/src/routes/pages/profile.rs | 11 +- crates/core/Cargo.toml | 4 +- crates/core/src/config.rs | 31 ++- crates/core/src/database/ads.rs | 4 +- crates/core/src/database/auth.rs | 9 +- crates/core/src/database/common.rs | 2 +- crates/core/src/database/drivers/common.rs | 1 - crates/core/src/database/drivers/mod.rs | 20 +- .../database/drivers/sql/create_uploads.sql | 7 - crates/core/src/database/emojis.rs | 4 +- crates/core/src/database/mod.rs | 1 - crates/core/src/database/posts.rs | 8 +- crates/core/src/database/products.rs | 8 +- crates/core/src/database/questions.rs | 36 +++- crates/core/src/database/uploads.rs | 194 ------------------ crates/core/src/model/mod.rs | 1 - crates/core/src/model/uploads.rs | 89 +------- crates/l10n/Cargo.toml | 1 + crates/shared/Cargo.toml | 1 + example/tetratto.toml | 5 +- .../apps_api_key.sql | 0 .../apps_data_used.sql | 0 .../apps_scopes.sql | 0 .../browser_session.sql | 0 .../channels_last_message.sql | 0 .../channels_members.sql | 0 .../channels_title.sql | 0 .../communities_is_forge.sql | 0 .../communities_post_count.sql | 0 .../journals_dirs.sql | 0 .../messages_reactions.sql | 0 .../notes_dir_tags.sql | 0 .../notes_is_global.sql | 0 .../notifications_tag.sql | 0 .../posts_is_deleted.sql | 0 .../posts_is_open.sql | 0 .../posts_poll_id.sql | 0 .../posts_stack.sql | 0 .../posts_title.sql | 0 .../posts_tscvector_content.sql | 0 .../posts_uploads.sql | 0 .../questions_context.sql | 0 .../questions_drawings.sql | 0 .../questions_ip.sql | 0 .../questions_likes.sql | 0 .../reactions_unique.sql | 0 .../requests_pkey.sql | 0 .../requests_pkeys.sql | 0 .../services_revision.sql | 0 .../stacks_mode_sort.sql | 0 manual_migrations/uploads.js | 43 ++++ .../uploads_alt.sql | 0 .../users_achievements.sql | 0 .../users_associated.sql | 0 .../users_awaiting_purchase.sql | 0 .../users_ban_reason.sql | 0 .../users_connections.sql | 0 .../users_grants.sql | 0 .../users_invite_code.sql | 0 .../users_layouts.sql | 0 .../users_post_count.sql | 0 .../users_request_count.sql | 0 .../users_secondary_permissions.sql | 0 .../users_seller_data.sql | 0 .../users_stripe_id.sql | 0 .../users_totp.sql | 0 83 files changed, 351 insertions(+), 458 deletions(-) delete mode 100644 crates/core/src/database/drivers/sql/create_uploads.sql delete mode 100644 crates/core/src/database/uploads.rs rename {sql_changes => manual_migrations}/apps_api_key.sql (100%) rename {sql_changes => manual_migrations}/apps_data_used.sql (100%) rename {sql_changes => manual_migrations}/apps_scopes.sql (100%) rename {sql_changes => manual_migrations}/browser_session.sql (100%) rename {sql_changes => manual_migrations}/channels_last_message.sql (100%) rename {sql_changes => manual_migrations}/channels_members.sql (100%) rename {sql_changes => manual_migrations}/channels_title.sql (100%) rename {sql_changes => manual_migrations}/communities_is_forge.sql (100%) rename {sql_changes => manual_migrations}/communities_post_count.sql (100%) rename {sql_changes => manual_migrations}/journals_dirs.sql (100%) rename {sql_changes => manual_migrations}/messages_reactions.sql (100%) rename {sql_changes => manual_migrations}/notes_dir_tags.sql (100%) rename {sql_changes => manual_migrations}/notes_is_global.sql (100%) rename {sql_changes => manual_migrations}/notifications_tag.sql (100%) rename {sql_changes => manual_migrations}/posts_is_deleted.sql (100%) rename {sql_changes => manual_migrations}/posts_is_open.sql (100%) rename {sql_changes => manual_migrations}/posts_poll_id.sql (100%) rename {sql_changes => manual_migrations}/posts_stack.sql (100%) rename {sql_changes => manual_migrations}/posts_title.sql (100%) rename {sql_changes => manual_migrations}/posts_tscvector_content.sql (100%) rename {sql_changes => manual_migrations}/posts_uploads.sql (100%) rename {sql_changes => manual_migrations}/questions_context.sql (100%) rename {sql_changes => manual_migrations}/questions_drawings.sql (100%) rename {sql_changes => manual_migrations}/questions_ip.sql (100%) rename {sql_changes => manual_migrations}/questions_likes.sql (100%) rename {sql_changes => manual_migrations}/reactions_unique.sql (100%) rename {sql_changes => manual_migrations}/requests_pkey.sql (100%) rename {sql_changes => manual_migrations}/requests_pkeys.sql (100%) rename {sql_changes => manual_migrations}/services_revision.sql (100%) rename {sql_changes => manual_migrations}/stacks_mode_sort.sql (100%) create mode 100644 manual_migrations/uploads.js rename {sql_changes => manual_migrations}/uploads_alt.sql (100%) rename {sql_changes => manual_migrations}/users_achievements.sql (100%) rename {sql_changes => manual_migrations}/users_associated.sql (100%) rename {sql_changes => manual_migrations}/users_awaiting_purchase.sql (100%) rename {sql_changes => manual_migrations}/users_ban_reason.sql (100%) rename {sql_changes => manual_migrations}/users_connections.sql (100%) rename {sql_changes => manual_migrations}/users_grants.sql (100%) rename {sql_changes => manual_migrations}/users_invite_code.sql (100%) rename {sql_changes => manual_migrations}/users_layouts.sql (100%) rename {sql_changes => manual_migrations}/users_post_count.sql (100%) rename {sql_changes => manual_migrations}/users_request_count.sql (100%) rename {sql_changes => manual_migrations}/users_secondary_permissions.sql (100%) rename {sql_changes => manual_migrations}/users_seller_data.sql (100%) rename {sql_changes => manual_migrations}/users_stripe_id.sql (100%) rename {sql_changes => manual_migrations}/users_totp.sql (100%) diff --git a/Cargo.lock b/Cargo.lock index 9bfe6cb..27be837 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -383,6 +383,21 @@ dependencies = [ "serde", ] +[[package]] +name = "buckets-core" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "536e476a5181a9f8a12d65be91615f036a000a1b1a2eaacde1be78be866940fd" +dependencies = [ + "oiseau", + "pathbufd", + "serde", + "serde_json", + "tetratto-core 15.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "tetratto-shared 12.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "toml 0.9.5", +] + [[package]] name = "built" version = "0.7.7" @@ -1372,7 +1387,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.5.10", + "socket2 0.6.0", "system-configuration", "tokio", "tower-service", @@ -3341,9 +3356,9 @@ dependencies = [ "serde", "serde_json", "tera", - "tetratto-core", - "tetratto-l10n", - "tetratto-shared", + "tetratto-core 15.0.2", + "tetratto-l10n 12.0.0", + "tetratto-shared 12.0.6", "tokio", "tower-http", "tracing", @@ -3353,7 +3368,34 @@ dependencies = [ [[package]] name = "tetratto-core" -version = "15.0.1" +version = "15.0.2" +dependencies = [ + "async-recursion", + "base16ct", + "base64 0.22.1", + "bitflags 2.9.2", + "buckets-core", + "emojis", + "md-5", + "oiseau", + "paste", + "pathbufd", + "regex", + "reqwest", + "serde", + "serde_json", + "tetratto-l10n 12.0.0", + "tetratto-shared 12.0.6", + "tokio", + "toml 0.9.5", + "totp-rs", +] + +[[package]] +name = "tetratto-core" +version = "15.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "605c03fac71468f57f9c47d9246300640f3f65ec9f19fb86799e10f632d3ea68" dependencies = [ "async-recursion", "base16ct", @@ -3368,8 +3410,8 @@ dependencies = [ "reqwest", "serde", "serde_json", - "tetratto-l10n", - "tetratto-shared", + "tetratto-l10n 12.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tetratto-shared 12.0.6 (registry+https://github.com/rust-lang/crates.io-index)", "tokio", "toml 0.9.5", "totp-rs", @@ -3384,6 +3426,17 @@ dependencies = [ "toml 0.9.5", ] +[[package]] +name = "tetratto-l10n" +version = "12.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d96f5e41633c757e3519efb47c9b85d00d14322c1961360e126d0ecc0ea79b86" +dependencies = [ + "pathbufd", + "serde", + "toml 0.9.5", +] + [[package]] name = "tetratto-shared" version = "12.0.6" @@ -3400,6 +3453,24 @@ dependencies = [ "uuid 1.18.0", ] +[[package]] +name = "tetratto-shared" +version = "12.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "286290ad09be3c507f9a47d38e92b024e6fcde34dbb515113f5bdb6b926cbee3" +dependencies = [ + "ammonia", + "chrono", + "hex_fmt", + "pulldown-cmark", + "rand 0.9.2", + "regex", + "serde", + "sha2", + "snowflaked", + "uuid 1.18.0", +] + [[package]] name = "thiserror" version = "1.0.69" diff --git a/README.md b/README.md index d1a3d80..38eaa9e 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,9 @@ A `docs` directory will be generated in the same directory that you ran the `tet You can configure your port through the `port` key of the configuration file. You can also run the server with the `PORT` environment variable, which will override whatever is set in the configuration file. You can also use the `CACHE_BREAKER` environment variable to specify a version number to be used in static asset links in order to break cache entries. -You can launch with the `LITTLEWEB=true` environment variable to start the littleweb viewer/fake DNS server. This should be used in combination with `PORT`, as well as set as the `lw_host` in your configuration file. This secondary server is required to allow users to view their littleweb projects. +You can launch with the `LITTLEWEB=true` environment variable to start the littleweb viewer/fake DNS server. This should be used in combination with `PORT`, as well as set as the `littleweb` key in the `[service_hosts]` section of your configuration file. This secondary server is required to allow users to view their littleweb projects. + +You are also required to include the `buckets` key of the `[service_hosts]` section of your configuration file. This host should link to [upload server](https://trisua.com/t/buckets). Tetratto provides Buckets with the `media` directory you set (in the `dirs` section of your configuration file). The `uploads` sub-directory of your media directory is also used, and as such should be a symbolic link to the Buckets [directory](https://docs.rs/buckets-core/latest/buckets_core/struct.Config.html#structfield.directory). ## Usage (as a user) diff --git a/crates/app/src/public/css/style.css b/crates/app/src/public/css/style.css index 52f3464..71a9a5c 100644 --- a/crates/app/src/public/css/style.css +++ b/crates/app/src/public/css/style.css @@ -70,6 +70,7 @@ cursor: zoom-in; max-height: calc(100dvh - var(--padding)); max-width: calc(100dvh - var(--padding)); + max-width: 100%; } /* avatar/banner */ diff --git a/crates/app/src/public/html/components.lisp b/crates/app/src/public/html/components.lisp index d4433c9..1ec8828 100644 --- a/crates/app/src/public/html/components.lisp +++ b/crates/app/src/public/html/components.lisp @@ -421,7 +421,7 @@ (span (str (text "general:label.could_not_find_post")))) (text "{%- endif %} {%- endif %}")) - (text "{{ self::post_media(upload_ids=post.uploads) }} {% else %}") + (text "{{ self::post_media(upload_ids=post.uploads, bucket=\"post_media\") }} {% else %}") (details ("class" "card tiny lowered w_full") (summary @@ -448,7 +448,7 @@ (span (text "Could not find original post..."))) (text "{%- endif %} {%- endif %}")) - (text "{{ self::post_media(upload_ids=post.uploads) }}"))) + (text "{{ self::post_media(upload_ids=post.uploads, bucket=\"post_media\") }}"))) (text "{%- endif %} {%- endif %}") (text "{% if poll -%} {{ self::poll(post=post, poll=poll) }} {%- endif %}") @@ -465,15 +465,15 @@ (text "{% if community and show_community and community.id != config.town_square or question %}")) (text "{%- endif %} {%- endmacro %}") -(text "{% macro post_media(upload_ids, custom_click=false) -%} {% if upload_ids|length > 0 -%}") +(text "{% macro post_media(upload_ids, custom_click=false, bucket) -%} {% if upload_ids|length > 0 -%}") (div ("class" "media_gallery gap_2") (text "{% for upload in upload_ids %}") (img - ("src" "/api/v1/uploads/{{ upload }}") + ("src" "{{ config.service_hosts.buckets }}/{{ bucket }}/{{ upload }}") ("data-upload-id" "{{ upload }}") ("alt" "Image upload") - ("onclick" "{% if custom_click -%} {{ custom_click }} {%- else -%} trigger('ui::lightbox_open', ['/api/v1/uploads/{{ upload }}']) {%- endif %}")) + ("onclick" "{% if custom_click -%} {{ custom_click }} {%- else -%} trigger('ui::lightbox_open', ['{{ config.service_hosts.buckets }}/{{ bucket }}/{{ upload }}']) {%- endif %}")) (text "{% endfor %}")) (text "{%- endif %} {%- endmacro %} {% macro notification(notification) -%}") (div @@ -825,7 +825,7 @@ ("id" "question_content:{{ question.id }}") (text "{{ question.content|markdown|safe }}")) ; question drawings - (text "{{ self::post_media(upload_ids=question.drawings) }}") + (text "{{ self::post_media(upload_ids=question.drawings, bucket=\"drawings\") }}") ; asking about (text "{% if asking_about -%}") (text "{{ self::post(post=asking_about[1], owner=asking_about[0], secondary=not secondary, show_community=false) }}") diff --git a/crates/app/src/public/html/economy/ad.lisp b/crates/app/src/public/html/economy/ad.lisp index fe4581f..0e7bed9 100644 --- a/crates/app/src/public/html/economy/ad.lisp +++ b/crates/app/src/public/html/economy/ad.lisp @@ -41,7 +41,7 @@ display: inline; width: 100dvw; height: 100dvh; - background-image: url(\"{{ config.host|safe }}/api/v1/uploads/{{ ad.upload_id }}\"); + background-image: url(\"{{ config.service_hosts.buckets }}/ads/{{ ad.upload_id }}\"); background-position: center; background-size: contain; } diff --git a/crates/app/src/public/html/economy/edit.lisp b/crates/app/src/public/html/economy/edit.lisp index 28e5db5..1f878f5 100644 --- a/crates/app/src/public/html/economy/edit.lisp +++ b/crates/app/src/public/html/economy/edit.lisp @@ -13,7 +13,7 @@ (str (text "economy:label.thumbnails")))) (div ("class" "card flex flex_col gap_2") - (text "{{ components::post_media(upload_ids=product.uploads.thumbnails, custom_click=\"remove_thumbnail(event.target)\") }}") + (text "{{ components::post_media(upload_ids=product.uploads.thumbnails, custom_click=\"remove_thumbnail(event.target)\", bucket=\"product_imgs\") }}") (text "{% if product.uploads.thumbnails|length < 4 -%}") (button ("onclick" "add_thumbnail()") diff --git a/crates/app/src/public/html/economy/product.lisp b/crates/app/src/public/html/economy/product.lisp index b59e4fb..638878e 100644 --- a/crates/app/src/public/html/economy/product.lisp +++ b/crates/app/src/public/html/economy/product.lisp @@ -4,7 +4,7 @@ (text "{% endblock %} {% block body %} {{ macros::nav(selected=\"\") }}") (main ("class" "flex flex_col gap_2") - (text "{{ components::post_media(upload_ids=product.uploads.thumbnails) }}") + (text "{{ components::post_media(upload_ids=product.uploads.thumbnails, bucket=\"product_imgs\") }}") (div ("class" "card flex flex_col gap_2") (h3 diff --git a/crates/app/src/public/html/littleweb/browser.lisp b/crates/app/src/public/html/littleweb/browser.lisp index 5267031..e508d55 100644 --- a/crates/app/src/public/html/littleweb/browser.lisp +++ b/crates/app/src/public/html/littleweb/browser.lisp @@ -55,7 +55,7 @@ (iframe ("id" "browser_iframe") ("frameborder" "0") - ("src" "{% if path -%} {{ config.lw_host }}/api/v1/net/{{ path }}?s={{ session }} {%- endif %}")) + ("src" "{% if path -%} {{ config.service_hosts.littleweb }}/api/v1/net/{{ path }}?s={{ session }} {%- endif %}")) (style ("data-turbo-temporary" "true") @@ -124,7 +124,7 @@ // ... console.log(\"navigate\", uri); - document.getElementById(\"browser_iframe\").src = `{{ config.lw_host|safe }}/api/v1/net/${uri}?s={{ session }}`; + document.getElementById(\"browser_iframe\").src = `{{ config.service_hosts.littleweb|safe }}/api/v1/net/${uri}?s={{ session }}`; if (!uri.includes(\"atto://\")) { document.getElementById(\"uri\").setAttribute(\"true_value\", `atto://${uri}`); diff --git a/crates/app/src/public/html/macros.lisp b/crates/app/src/public/html/macros.lisp index cadeeba..dff731a 100644 --- a/crates/app/src/public/html/macros.lisp +++ b/crates/app/src/public/html/macros.lisp @@ -101,7 +101,7 @@ ("href" "/developer") (icon (text "code")) (str (text "developer:label.apps"))) - (text "{% if config.lw_host -%}") + (text "{% if config.service_hosts.littleweb -%}") (button ("onclick" "document.getElementById('littleweb').showModal()") (icon (text "globe")) diff --git a/crates/app/src/public/html/profile/settings.lisp b/crates/app/src/public/html/profile/settings.lisp index 8bda6c2..1628288 100644 --- a/crates/app/src/public/html/profile/settings.lisp +++ b/crates/app/src/public/html/profile/settings.lisp @@ -665,12 +665,12 @@ (span ("class" "date") (text "{{ upload.created }}")) - (text " ({{ upload.what }})"))) + (text " ({{ upload.metadata.what }})"))) (div ("class" "flex gap_2") (button ("class" "raised small") - ("onclick" "trigger('ui::lightbox_open', ['/api/v1/uploads/{{ upload.id }}'])") + ("onclick" "trigger('ui::lightbox_open', ['{{ config.service_hosts.buckets }}/{{ upload.bucket }}/{{ upload.id }}'])") (text "{{ icon \"view\" }}") (span (text "{{ text \"general:action.view\" }}"))) @@ -694,7 +694,7 @@ ("name" "alt") ("class" "w_full") ("placeholder" "Alternative text") - (text "{{ upload.alt|safe }}"))) + (text "{{ upload.metadata.alt|safe }}"))) (button (icon (text "check")) diff --git a/crates/app/src/public/js/atto.js b/crates/app/src/public/js/atto.js index ac6dda8..952e005 100644 --- a/crates/app/src/public/js/atto.js +++ b/crates/app/src/public/js/atto.js @@ -1152,7 +1152,7 @@ ${option.input_element_type === "textarea" ? `${option.value}` : ""} self.define("lightbox_open", async (_, src) => { document.getElementById("lightbox_img").src = src; - const data = await (await fetch(`${src}/data`)).json(); + const data = await (await fetch(`${src}/json`)).json(); document .getElementById("lightbox_img") .setAttribute("alt", data.payload.alt); diff --git a/crates/app/src/routes/api/v1/ads.rs b/crates/app/src/routes/api/v1/ads.rs index 505b49e..f2c9744 100644 --- a/crates/app/src/routes/api/v1/ads.rs +++ b/crates/app/src/routes/api/v1/ads.rs @@ -42,11 +42,16 @@ pub async fn create_request( } let upload = match data - .create_upload(MediaUpload::new(MediaType::Webp, user.id)) + .2 + .create_upload(MediaUpload::new( + MediaType::Webp, + user.id, + "ads".to_string(), + )) .await { Ok(x) => x, - Err(e) => return Json(e.into()), + Err(e) => return Json(Error::MiscError(e.to_string()).into()), }; match data @@ -55,9 +60,11 @@ pub async fn create_request( { Ok(_) => { // write image - if let Err(e) = - save_webp_buffer(&upload.path(&data.0.0).to_string(), file.to_vec(), None) - { + if let Err(e) = save_webp_buffer( + &upload.path(&data.2.0.0.directory).to_string(), + file.to_vec(), + None, + ) { return Json(Error::MiscError(e.to_string()).into()); } diff --git a/crates/app/src/routes/api/v1/communities/emojis.rs b/crates/app/src/routes/api/v1/communities/emojis.rs index 84fadc0..8417658 100644 --- a/crates/app/src/routes/api/v1/communities/emojis.rs +++ b/crates/app/src/routes/api/v1/communities/emojis.rs @@ -63,10 +63,11 @@ pub async fn get_request( }; let upload = data + .2 .get_upload_by_id(emoji.0.unwrap().upload_id) .await .unwrap(); - let path = upload.path(&data.0.0); + let path = upload.path(&data.2.0.0.directory); if !exists(&path).unwrap() { return Err(( @@ -80,7 +81,7 @@ pub async fn get_request( } Ok(( - [("Content-Type", upload.what.mime())], + [("Content-Type", upload.metadata.what.mime())], Body::from(read_image(path)), )) } @@ -119,6 +120,7 @@ pub async fn create_request( // create upload let upload = match data + .2 .create_upload(MediaUpload::new( if img.1 == "image/gif" { MediaType::Gif @@ -126,11 +128,12 @@ pub async fn create_request( MediaType::Webp }, user.id, + "emojis".to_string(), )) .await { Ok(u) => u, - Err(e) => return Json(e.into()), + Err(e) => return Json(Error::MiscError(e.to_string()).into()), }; // create emoji @@ -147,13 +150,15 @@ pub async fn create_request( { Ok(_) => { if is_animated { - if let Err(e) = upload.write(&data.0.0, &img.0) { + if let Err(e) = upload.write(&data.2.0.0.directory, &img.0) { return Json(Error::MiscError(e.to_string()).into()); } } else { - if let Err(e) = - save_webp_buffer(&upload.path(&data.0.0).to_string(), img.0.to_vec(), None) - { + if let Err(e) = save_webp_buffer( + &upload.path(&data.2.0.0.directory).to_string(), + img.0.to_vec(), + None, + ) { return Json(Error::MiscError(e.to_string()).into()); } } @@ -165,8 +170,8 @@ pub async fn create_request( }) } Err(e) => { - if let Err(e) = upload.remove(&data.0.0) { - return Json(e.into()); + if let Err(e) = upload.remove(&data.2.0.0.directory) { + return Json(Error::MiscError(e.to_string()).into()); } Json(e.into()) diff --git a/crates/app/src/routes/api/v1/communities/posts.rs b/crates/app/src/routes/api/v1/communities/posts.rs index 05751f1..02f2453 100644 --- a/crates/app/src/routes/api/v1/communities/posts.rs +++ b/crates/app/src/routes/api/v1/communities/posts.rs @@ -146,11 +146,16 @@ pub async fn create_request( for _ in 0..images.len() { props.uploads.push( match data - .create_upload(MediaUpload::new(MediaType::Webp, props.owner)) + .2 + .create_upload(MediaUpload::new( + MediaType::Webp, + props.owner, + "post_media".to_string(), + )) .await { Ok(u) => u.id, - Err(e) => return Json(e.into()), + Err(e) => return Json(Error::MiscError(e.to_string()).into()), }, ); } @@ -164,22 +169,24 @@ pub async fn create_request( let image = match images.get(i) { Some(img) => img, None => { - if let Err(e) = data.delete_upload(*upload_id).await { - return Json(e.into()); + if let Err(e) = data.2.delete_upload(*upload_id).await { + return Json(Error::MiscError(e.to_string()).into()); } continue; } }; - let upload = match data.get_upload_by_id(*upload_id).await { + let upload = match data.2.get_upload_by_id(*upload_id).await { Ok(u) => u, - Err(e) => return Json(e.into()), + Err(e) => return Json(Error::MiscError(e.to_string()).into()), }; - if let Err(e) = - save_webp_buffer(&upload.path(&data.0.0).to_string(), image.to_vec(), None) - { + if let Err(e) = save_webp_buffer( + &upload.path(&data.2.0.0.directory).to_string(), + image.to_vec(), + None, + ) { return Json(Error::MiscError(e.to_string()).into()); } } diff --git a/crates/app/src/routes/api/v1/mod.rs b/crates/app/src/routes/api/v1/mod.rs index 1f7632a..a5c92b3 100644 --- a/crates/app/src/routes/api/v1/mod.rs +++ b/crates/app/src/routes/api/v1/mod.rs @@ -706,9 +706,7 @@ pub fn routes() -> Router { delete(notes::delete_by_dir_request), ) // uploads - .route("/uploads/{id}", get(uploads::get_request)) .route("/uploads/{id}", delete(uploads::delete_request)) - .route("/uploads/{id}/data", get(uploads::get_json_request)) .route("/uploads/{id}/alt", post(uploads::update_alt_request)) // services .route("/services", get(services::list_request)) diff --git a/crates/app/src/routes/api/v1/products.rs b/crates/app/src/routes/api/v1/products.rs index 2975bbe..bc24c9d 100644 --- a/crates/app/src/routes/api/v1/products.rs +++ b/crates/app/src/routes/api/v1/products.rs @@ -368,27 +368,34 @@ pub async fn update_uploads_request( } let upload = match data - .create_upload(MediaUpload::new(MediaType::Webp, user.id)) + .2 + .create_upload(MediaUpload::new( + MediaType::Webp, + user.id, + "product_imgs".to_string(), + )) .await { Ok(x) => x, - Err(e) => return Json(e.into()), + Err(e) => return Json(Error::MiscError(e.to_string()).into()), }; product.uploads.thumbnails.push(upload.id); // write image - if let Err(e) = - save_webp_buffer(&upload.path(&data.0.0).to_string(), file.to_vec(), None) - { + if let Err(e) = save_webp_buffer( + &upload.path(&data.2.0.0.directory).to_string(), + file.to_vec(), + None, + ) { return Json(Error::MiscError(e.to_string()).into()); } } ProductUploadTarget::Reward => { // remove old if product.uploads.reward != 0 { - if let Err(e) = data.delete_upload(product.uploads.reward).await { - return Json(e.into()); + if let Err(e) = data.2.delete_upload(product.uploads.reward).await { + return Json(Error::MiscError(e.to_string()).into()); } } @@ -403,19 +410,26 @@ pub async fn update_uploads_request( } let upload = match data - .create_upload(MediaUpload::new(MediaType::Webp, user.id)) + .2 + .create_upload(MediaUpload::new( + MediaType::Webp, + user.id, + "product_imgs".to_string(), + )) .await { Ok(x) => x, - Err(e) => return Json(e.into()), + Err(e) => return Json(Error::MiscError(e.to_string()).into()), }; product.uploads.reward = upload.id; // write image - if let Err(e) = - save_webp_buffer(&upload.path(&data.0.0).to_string(), file.to_vec(), None) - { + if let Err(e) = save_webp_buffer( + &upload.path(&data.2.0.0.directory).to_string(), + file.to_vec(), + None, + ) { return Json(Error::MiscError(e.to_string()).into()); } } @@ -462,8 +476,8 @@ pub async fn remove_thumbnail_request( None => return Json(Error::GeneralNotFound("thumbnail".to_string()).into()), }; - if let Err(e) = data.delete_upload(*thumbnail).await { - return Json(e.into()); + if let Err(e) = data.2.delete_upload(*thumbnail).await { + return Json(Error::MiscError(e.to_string()).into()); } product.uploads.thumbnails.remove(req.idx); diff --git a/crates/app/src/routes/api/v1/uploads.rs b/crates/app/src/routes/api/v1/uploads.rs index 0e1d807..8ddc307 100644 --- a/crates/app/src/routes/api/v1/uploads.rs +++ b/crates/app/src/routes/api/v1/uploads.rs @@ -1,74 +1,7 @@ -use std::fs::exists; -use axum::{body::Body, extract::Path, response::IntoResponse, Extension, Json}; +use axum::{extract::Path, response::IntoResponse, Extension, Json}; use crate::cookie::CookieJar; -use pathbufd::PathBufD; use crate::{get_user_from_token, routes::api::v1::UpdateUploadAlt, State}; -use super::auth::images::read_image; -use tetratto_core::model::{carp::CarpGraph, oauth, uploads::MediaType, ApiReturn, Error}; - -pub async fn get_request( - Path(id): Path, - Extension(data): Extension, -) -> impl IntoResponse { - let data = &(data.read().await).0; - - let upload = match data.get_upload_by_id(id).await { - Ok(u) => u, - Err(_) => { - return Err(( - [("Content-Type", "image/svg+xml")], - Body::from(read_image(PathBufD::current().extend(&[ - data.0.0.dirs.media.as_str(), - "images", - "default-banner.svg", - ]))), - )); - } - }; - - let path = upload.path(&data.0.0); - - if !exists(&path).unwrap() { - return Err(( - [("Content-Type", "image/svg+xml")], - Body::from(read_image(PathBufD::current().extend(&[ - data.0.0.dirs.media.as_str(), - "images", - "default-banner.svg", - ]))), - )); - } - - let bytes = read_image(path); - - if upload.what == MediaType::Carpgraph { - // conver to svg and return - return Ok(( - [("Content-Type", "image/svg+xml".to_string())], - Body::from(CarpGraph::from_bytes(bytes).to_svg()), - )); - } - - Ok(([("Content-Type", upload.what.mime())], Body::from(bytes))) -} - -pub async fn get_json_request( - Path(id): Path, - Extension(data): Extension, -) -> impl IntoResponse { - let data = &(data.read().await).0; - - let upload = match data.get_upload_by_id(id).await { - Ok(u) => u, - Err(e) => return Json(e.into()), - }; - - Json(ApiReturn { - ok: true, - message: "Success".to_string(), - payload: Some(upload), - }) -} +use tetratto_core::model::{oauth, ApiReturn, Error}; pub async fn delete_request( jar: CookieJar, @@ -81,13 +14,22 @@ pub async fn delete_request( None => return Json(Error::NotAllowed.into()), }; - match data.delete_upload_checked(id, &user).await { + let upload = match data.2.get_upload_by_id(id).await { + Ok(x) => x, + Err(e) => return Json(Error::MiscError(e.to_string()).into()), + }; + + if user.id != upload.owner { + return Json(Error::NotAllowed.into()); + } + + match data.2.delete_upload(id).await { Ok(_) => Json(ApiReturn { ok: true, message: "Upload deleted".to_string(), payload: (), }), - Err(e) => Json(e.into()), + Err(e) => Json(Error::MiscError(e.to_string()).into()), } } @@ -103,12 +45,22 @@ pub async fn update_alt_request( None => return Json(Error::NotAllowed.into()), }; - match data.update_upload_alt(id, &user, &props.alt).await { + let mut upload = match data.2.get_upload_by_id(id).await { + Ok(x) => x, + Err(e) => return Json(Error::MiscError(e.to_string()).into()), + }; + + if user.id != upload.owner { + return Json(Error::NotAllowed.into()); + } + + upload.metadata.alt = props.alt; + match data.2.update_upload_metadata(id, upload.metadata).await { Ok(_) => Json(ApiReturn { ok: true, message: "Upload updated".to_string(), payload: (), }), - Err(e) => Json(e.into()), + Err(e) => Json(Error::MiscError(e.to_string()).into()), } } diff --git a/crates/app/src/routes/pages/profile.rs b/crates/app/src/routes/pages/profile.rs index dc6eca1..3b1df41 100644 --- a/crates/app/src/routes/pages/profile.rs +++ b/crates/app/src/routes/pages/profile.rs @@ -122,10 +122,17 @@ pub async fn settings_request( Err(e) => return Err(Html(render_error(e, &jar, &data, &None).await)), }; - let uploads = match data.0.get_uploads_by_owner(profile.id, 12, req.page).await { + let uploads = match data + .0 + .2 + .get_uploads_by_owner(profile.id, 12, req.page) + .await + { Ok(ua) => ua, Err(e) => { - return Err(Html(render_error(e, &jar, &data, &None).await)); + return Err(Html( + render_error(Error::MiscError(e.to_string()), &jar, &data, &None).await, + )); } }; diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 1a0cd88..fefadbf 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -1,8 +1,9 @@ [package] name = "tetratto-core" description = "The core behind Tetratto" -version = "15.0.1" +version = "15.0.2" edition = "2024" +readme = "../../README.md" authors.workspace = true repository.workspace = true license.workspace = true @@ -48,3 +49,4 @@ oiseau = { version = "0.1.2", default-features = false, features = [ ], optional = true } paste = { version = "1.0.15", optional = true } tokio = { version = "1.47.1", features = ["macros", "rt-multi-thread"] } +buckets-core = "1.0.1" diff --git a/crates/core/src/config.rs b/crates/core/src/config.rs index 3843f0a..5c852a3 100644 --- a/crates/core/src/config.rs +++ b/crates/core/src/config.rs @@ -257,6 +257,24 @@ impl Default for ManualsConfig { } } +#[derive(Clone, Serialize, Deserialize, Debug)] +pub struct ServiceHostsConfig { + /// Buckets host . + pub buckets: String, + /// Littleweb browser host. + #[serde(default)] + pub littleweb: String, +} + +impl Default for ServiceHostsConfig { + fn default() -> Self { + Self { + buckets: String::new(), + littleweb: String::new(), + } + } +} + #[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)] pub enum StringBan { /// An exact string. @@ -298,10 +316,9 @@ pub struct Config { /// so this host should be included in there as well. #[serde(default = "default_host")] pub host: String, - /// The main public host of the littleweb server. **Not** used to check against banned hosts, - /// so this host should be included in there as well. - #[serde(default = "default_lw_host")] - pub lw_host: String, + /// The main public host of the required microservices. + #[serde(default = "default_service_hosts")] + pub service_hosts: ServiceHostsConfig, /// Database security. #[serde(default = "default_security")] pub security: SecurityConfig, @@ -382,8 +399,8 @@ fn default_host() -> String { String::new() } -fn default_lw_host() -> String { - String::new() +fn default_service_hosts() -> ServiceHostsConfig { + ServiceHostsConfig::default() } fn default_security() -> SecurityConfig { @@ -459,7 +476,7 @@ impl Default for Config { port: default_port(), banned_hosts: default_banned_hosts(), host: default_host(), - lw_host: default_lw_host(), + service_hosts: default_service_hosts(), database: default_database(), security: default_security(), dirs: default_dirs(), diff --git a/crates/core/src/database/ads.rs b/crates/core/src/database/ads.rs index cb6463a..9660d6a 100644 --- a/crates/core/src/database/ads.rs +++ b/crates/core/src/database/ads.rs @@ -143,7 +143,9 @@ impl DataManager { } // remove upload - self.delete_upload(ad.upload_id).await?; + if let Err(e) = self.2.delete_upload(ad.upload_id).await { + return Err(Error::MiscError(e.to_string())); + } // ... let conn = match self.0.connect().await { diff --git a/crates/core/src/database/auth.rs b/crates/core/src/database/auth.rs index 2245e47..c9ad94f 100644 --- a/crates/core/src/database/auth.rs +++ b/crates/core/src/database/auth.rs @@ -648,8 +648,13 @@ impl DataManager { } // delete uploads - for upload in self.get_uploads_by_owner_all(user.id).await? { - self.delete_upload(upload.id).await?; + for upload in match self.2.get_uploads_by_owner_all(user.id).await { + Ok(x) => x, + Err(e) => return Err(Error::MiscError(e.to_string())), + } { + if let Err(e) = self.2.delete_upload(upload.id).await { + return Err(Error::MiscError(e.to_string())); + } } // delete polls diff --git a/crates/core/src/database/common.rs b/crates/core/src/database/common.rs index 5a4cf82..8df91ae 100644 --- a/crates/core/src/database/common.rs +++ b/crates/core/src/database/common.rs @@ -28,7 +28,6 @@ impl DataManager { execute!(&conn, common::CREATE_TABLE_IPBLOCKS).unwrap(); execute!(&conn, common::CREATE_TABLE_CHANNELS).unwrap(); execute!(&conn, common::CREATE_TABLE_MESSAGES).unwrap(); - execute!(&conn, common::CREATE_TABLE_UPLOADS).unwrap(); execute!(&conn, common::CREATE_TABLE_EMOJIS).unwrap(); execute!(&conn, common::CREATE_TABLE_STACKS).unwrap(); execute!(&conn, common::CREATE_TABLE_DRAFTS).unwrap(); @@ -61,6 +60,7 @@ impl DataManager { .set("atto.active_connections:chats".to_string(), "0".to_string()) .await; + self.2.init().await.expect("failed to init buckets manager"); Ok(()) } diff --git a/crates/core/src/database/drivers/common.rs b/crates/core/src/database/drivers/common.rs index ca8fa79..6a16c2d 100644 --- a/crates/core/src/database/drivers/common.rs +++ b/crates/core/src/database/drivers/common.rs @@ -16,7 +16,6 @@ pub const CREATE_TABLE_QUESTIONS: &str = include_str!("./sql/create_questions.sq pub const CREATE_TABLE_IPBLOCKS: &str = include_str!("./sql/create_ipblocks.sql"); pub const CREATE_TABLE_CHANNELS: &str = include_str!("./sql/create_channels.sql"); pub const CREATE_TABLE_MESSAGES: &str = include_str!("./sql/create_messages.sql"); -pub const CREATE_TABLE_UPLOADS: &str = include_str!("./sql/create_uploads.sql"); pub const CREATE_TABLE_EMOJIS: &str = include_str!("./sql/create_emojis.sql"); pub const CREATE_TABLE_STACKS: &str = include_str!("./sql/create_stacks.sql"); pub const CREATE_TABLE_DRAFTS: &str = include_str!("./sql/create_drafts.sql"); diff --git a/crates/core/src/database/drivers/mod.rs b/crates/core/src/database/drivers/mod.rs index 60e7435..dc16c49 100644 --- a/crates/core/src/database/drivers/mod.rs +++ b/crates/core/src/database/drivers/mod.rs @@ -4,13 +4,29 @@ use std::collections::HashMap; use tetratto_l10n::{read_langs, LangFile}; use oiseau::postgres::{DataManager as OiseauManager, Result}; use crate::config::Config; +use buckets_core::{DataManager as BucketsManager, Config as BucketsConfig}; #[derive(Clone)] -pub struct DataManager(pub OiseauManager, pub HashMap); +pub struct DataManager( + pub OiseauManager, + pub HashMap, + pub BucketsManager, +); impl DataManager { /// Create a new [`DataManager`]. pub async fn new(config: Config) -> Result { - Ok(Self(OiseauManager::new(config).await?, read_langs())) + let buckets_manager = BucketsManager::new(BucketsConfig { + directory: format!("{}/{}", config.dirs.media, "uploads"), + database: config.database.clone(), + }) + .await + .expect("failed to create buckets manager"); + + Ok(Self( + OiseauManager::new(config).await?, + read_langs(), + buckets_manager, + )) } } diff --git a/crates/core/src/database/drivers/sql/create_uploads.sql b/crates/core/src/database/drivers/sql/create_uploads.sql deleted file mode 100644 index 57d4037..0000000 --- a/crates/core/src/database/drivers/sql/create_uploads.sql +++ /dev/null @@ -1,7 +0,0 @@ -CREATE TABLE IF NOT EXISTS uploads ( - id BIGINT NOT NULL PRIMARY KEY, - created BIGINT NOT NULL, - owner BIGINT NOT NULL, - what TEXT NOT NULL, - alt TEXT NOT NULL -) diff --git a/crates/core/src/database/emojis.rs b/crates/core/src/database/emojis.rs index 4f09af7..4fb3d9d 100644 --- a/crates/core/src/database/emojis.rs +++ b/crates/core/src/database/emojis.rs @@ -194,7 +194,9 @@ impl DataManager { } // delete upload - self.delete_upload(emoji.upload_id).await?; + if let Err(e) = self.2.delete_upload(emoji.upload_id).await { + return Err(Error::MiscError(e.to_string())); + } // ... self.0.1.remove(format!("atto.emoji:{}", id)).await; diff --git a/crates/core/src/database/mod.rs b/crates/core/src/database/mod.rs index 81d0c83..1adbf88 100644 --- a/crates/core/src/database/mod.rs +++ b/crates/core/src/database/mod.rs @@ -33,7 +33,6 @@ mod services; mod stackblocks; mod stacks; mod transfers; -mod uploads; mod user_warnings; mod userblocks; mod userfollows; diff --git a/crates/core/src/database/posts.rs b/crates/core/src/database/posts.rs index 00256a4..7f6b9fe 100644 --- a/crates/core/src/database/posts.rs +++ b/crates/core/src/database/posts.rs @@ -2262,7 +2262,9 @@ impl DataManager { // delete uploads for upload in y.uploads { - self.delete_upload(upload).await?; + if let Err(e) = self.2.delete_upload(upload).await { + return Err(Error::MiscError(e.to_string())); + } } // remove poll @@ -2356,7 +2358,9 @@ impl DataManager { // delete uploads for upload in y.uploads { - self.delete_upload(upload).await?; + if let Err(e) = self.2.delete_upload(upload).await { + return Err(Error::MiscError(e.to_string())); + } } // delete question (if not global question) diff --git a/crates/core/src/database/products.rs b/crates/core/src/database/products.rs index db8fce9..f5ffe21 100644 --- a/crates/core/src/database/products.rs +++ b/crates/core/src/database/products.rs @@ -252,11 +252,15 @@ If your product is a purchase of goods or services, please be sure to fulfill th // remove uploads for upload in product.uploads.thumbnails { - self.delete_upload(upload).await?; + if let Err(e) = self.2.delete_upload(upload).await { + return Err(Error::MiscError(e.to_string())); + }; } if product.uploads.reward != 0 { - self.delete_upload(product.uploads.reward).await?; + if let Err(e) = self.2.delete_upload(product.uploads.reward).await { + return Err(Error::MiscError(e.to_string())); + } } // ... diff --git a/crates/core/src/database/questions.rs b/crates/core/src/database/questions.rs index 8372b95..30399df 100644 --- a/crates/core/src/database/questions.rs +++ b/crates/core/src/database/questions.rs @@ -4,7 +4,7 @@ use tetratto_shared::unix_epoch_timestamp; use crate::model::addr::RemoteAddr; use crate::model::communities::Post; use crate::model::communities_permissions::CommunityPermission; -use crate::model::uploads::{MediaType, MediaUpload}; +use buckets_core::model::{MediaType, MediaUpload}; use crate::model::{ Error, Result, communities::Question, @@ -463,9 +463,18 @@ impl DataManager { for _ in 0..drawings.len() { data.drawings.push( - self.create_upload(MediaUpload::new(MediaType::Carpgraph, data.owner)) - .await? - .id, + match self + .2 + .create_upload(MediaUpload::new( + MediaType::Carpgraph, + data.owner, + "drawings".to_string(), + )) + .await + { + Ok(x) => x.id, + Err(_) => continue, + }, ); } @@ -516,14 +525,23 @@ impl DataManager { let drawing = match drawings.get(i) { Some(d) => d, None => { - self.delete_upload(*drawing_id).await?; + if let Err(e) = self.2.delete_upload(*drawing_id).await { + return Err(Error::MiscError(e.to_string())); + } + continue; } }; - let upload = self.get_upload_by_id(*drawing_id).await?; + let upload = match self.2.get_upload_by_id(*drawing_id).await { + Ok(x) => x, + Err(e) => return Err(Error::MiscError(e.to_string())), + }; - if let Err(e) = std::fs::write(&upload.path(&self.0.0).to_string(), drawing.to_vec()) { + if let Err(e) = std::fs::write( + &upload.path(&self.2.0.0.directory).to_string(), + drawing.to_vec(), + ) { return Err(Error::MiscError(e.to_string())); } } @@ -595,7 +613,9 @@ impl DataManager { // delete uploads for upload in y.drawings { - self.delete_upload(upload).await?; + if let Err(e) = self.2.delete_upload(upload).await { + return Err(Error::MiscError(e.to_string())); + } } // return diff --git a/crates/core/src/database/uploads.rs b/crates/core/src/database/uploads.rs deleted file mode 100644 index f669c53..0000000 --- a/crates/core/src/database/uploads.rs +++ /dev/null @@ -1,194 +0,0 @@ -use oiseau::cache::Cache; -use crate::model::auth::User; -use crate::model::permissions::FinePermission; -use crate::model::{Error, Result, uploads::MediaUpload}; -use crate::{auto_method, DataManager}; - -use oiseau::PostgresRow; - -use oiseau::{execute, get, query_rows, params}; - -impl DataManager { - /// Get a [`MediaUpload`] from an SQL row. - pub(crate) fn get_upload_from_row(x: &PostgresRow) -> MediaUpload { - MediaUpload { - id: get!(x->0(i64)) as usize, - created: get!(x->1(i64)) as usize, - owner: get!(x->2(i64)) as usize, - what: serde_json::from_str(&get!(x->3(String))).unwrap(), - alt: get!(x->4(String)), - } - } - - auto_method!(get_upload_by_id(usize as i64)@get_upload_from_row -> "SELECT * FROM uploads WHERE id = $1" --name="upload" --returns=MediaUpload --cache-key-tmpl="atto.upload:{}"); - - /// Get all uploads (paginated). - /// - /// # Arguments - /// * `batch` - the limit of items in each page - /// * `page` - the page number - pub async fn get_uploads(&self, batch: usize, page: usize) -> Result> { - let conn = match self.0.connect().await { - Ok(c) => c, - Err(e) => return Err(Error::DatabaseConnection(e.to_string())), - }; - - let res = query_rows!( - &conn, - "SELECT * FROM uploads ORDER BY created DESC LIMIT $1 OFFSET $2", - &[&(batch as i64), &((page * batch) as i64)], - |x| { Self::get_upload_from_row(x) } - ); - - if res.is_err() { - return Err(Error::GeneralNotFound("upload".to_string())); - } - - Ok(res.unwrap()) - } - - /// Get all uploads by their owner (paginated). - /// - /// # Arguments - /// * `owner` - the ID of the owner of the upload - /// * `batch` - the limit of items in each page - /// * `page` - the page number - pub async fn get_uploads_by_owner( - &self, - owner: usize, - batch: usize, - page: usize, - ) -> Result> { - let conn = match self.0.connect().await { - Ok(c) => c, - Err(e) => return Err(Error::DatabaseConnection(e.to_string())), - }; - - let res = query_rows!( - &conn, - "SELECT * FROM uploads WHERE owner = $1 ORDER BY created DESC LIMIT $2 OFFSET $3", - &[&(owner as i64), &(batch as i64), &((page * batch) as i64)], - |x| { Self::get_upload_from_row(x) } - ); - - if res.is_err() { - return Err(Error::GeneralNotFound("upload".to_string())); - } - - Ok(res.unwrap()) - } - - /// Get all uploads by their owner. - /// - /// # Arguments - /// * `owner` - the ID of the owner of the upload - pub async fn get_uploads_by_owner_all(&self, owner: usize) -> Result> { - let conn = match self.0.connect().await { - Ok(c) => c, - Err(e) => return Err(Error::DatabaseConnection(e.to_string())), - }; - - let res = query_rows!( - &conn, - "SELECT * FROM uploads WHERE owner = $1 ORDER BY created DESC", - &[&(owner as i64)], - |x| { Self::get_upload_from_row(x) } - ); - - if res.is_err() { - return Err(Error::GeneralNotFound("upload".to_string())); - } - - Ok(res.unwrap()) - } - - /// Create a new upload in the database. - /// - /// # Arguments - /// * `data` - a mock [`MediaUpload`] object to insert - pub async fn create_upload(&self, data: MediaUpload) -> Result { - let conn = match self.0.connect().await { - Ok(c) => c, - Err(e) => return Err(Error::DatabaseConnection(e.to_string())), - }; - - let res = execute!( - &conn, - "INSERT INTO uploads VALUES ($1, $2, $3, $4, $5)", - params![ - &(data.id as i64), - &(data.created as i64), - &(data.owner as i64), - &serde_json::to_string(&data.what).unwrap().as_str(), - &data.alt, - ] - ); - - if let Err(e) = res { - return Err(Error::DatabaseError(e.to_string())); - } - - // return - Ok(data) - } - - pub async fn delete_upload(&self, id: usize) -> Result<()> { - // if !user.permissions.check(FinePermission::MANAGE_UPLOADS) { - // return Err(Error::NotAllowed); - // } - - // delete file - // it's most important that the file gets off the file system first, even - // if there's an issue in the database - // - // the actual file takes up much more space than the database entry. - let upload = self.get_upload_by_id(id).await?; - upload.remove(&self.0.0)?; - - // delete from database - let conn = match self.0.connect().await { - Ok(c) => c, - Err(e) => return Err(Error::DatabaseConnection(e.to_string())), - }; - - let res = execute!(&conn, "DELETE FROM uploads WHERE id = $1", &[&(id as i64)]); - - if let Err(e) = res { - return Err(Error::DatabaseError(e.to_string())); - } - - self.0.1.remove(format!("atto.upload:{}", id)).await; - - // return - Ok(()) - } - - pub async fn delete_upload_checked(&self, id: usize, user: &User) -> Result<()> { - let upload = self.get_upload_by_id(id).await?; - - // check user permission - if user.id != upload.owner && !user.permissions.check(FinePermission::MANAGE_UPLOADS) { - return Err(Error::NotAllowed); - } - - // delete file - upload.remove(&self.0.0)?; - - // ... - let conn = match self.0.connect().await { - Ok(c) => c, - Err(e) => return Err(Error::DatabaseConnection(e.to_string())), - }; - - let res = execute!(&conn, "DELETE FROM uploads WHERE id = $1", &[&(id as i64)]); - - if let Err(e) = res { - return Err(Error::DatabaseError(e.to_string())); - } - - self.0.1.remove(format!("atto.upload:{}", id)).await; - Ok(()) - } - - auto_method!(update_upload_alt(&str)@get_upload_by_id:FinePermission::MANAGE_UPLOADS; -> "UPDATE uploads SET alt = $1 WHERE id = $2" --cache-key-tmpl="atto.upload:{}"); -} diff --git a/crates/core/src/model/mod.rs b/crates/core/src/model/mod.rs index 9b46412..75a133f 100644 --- a/crates/core/src/model/mod.rs +++ b/crates/core/src/model/mod.rs @@ -19,7 +19,6 @@ pub mod stacks; pub mod uploads; use std::fmt::Display; - use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize)] diff --git a/crates/core/src/model/uploads.rs b/crates/core/src/model/uploads.rs index bed6dad..2a878c1 100644 --- a/crates/core/src/model/uploads.rs +++ b/crates/core/src/model/uploads.rs @@ -1,93 +1,6 @@ -use pathbufd::PathBufD; use serde::{Serialize, Deserialize}; use tetratto_shared::{snow::Snowflake, unix_epoch_timestamp}; -use crate::config::Config; -use std::fs::{write, exists, remove_file}; -use super::{Error, Result}; - -#[derive(Serialize, Deserialize, PartialEq, Eq)] -pub enum MediaType { - #[serde(alias = "image/webp")] - Webp, - #[serde(alias = "image/avif")] - Avif, - #[serde(alias = "image/png")] - Png, - #[serde(alias = "image/jpg")] - Jpg, - #[serde(alias = "image/gif")] - Gif, - #[serde(alias = "image/carpgraph")] - Carpgraph, -} - -impl MediaType { - pub fn extension(&self) -> &str { - match self { - Self::Webp => "webp", - Self::Avif => "avif", - Self::Png => "png", - Self::Jpg => "jpg", - Self::Gif => "gif", - Self::Carpgraph => "carpgraph", - } - } - - pub fn mime(&self) -> String { - format!("image/{}", self.extension()) - } -} - -#[derive(Serialize, Deserialize)] -pub struct MediaUpload { - pub id: usize, - pub created: usize, - pub owner: usize, - pub what: MediaType, - pub alt: String, -} - -impl MediaUpload { - /// Create a new [`MediaUpload`]. - pub fn new(what: MediaType, owner: usize) -> Self { - Self { - id: Snowflake::new().to_string().parse::().unwrap(), - created: unix_epoch_timestamp(), - owner, - what, - alt: String::new(), - } - } - - /// Get the path to the fs file for this upload. - pub fn path(&self, config: &Config) -> PathBufD { - PathBufD::current() - .extend(&[config.dirs.media.as_str(), "uploads"]) - .join(format!("{}.{}", self.id, self.what.extension())) - } - - /// Write to this upload in the file system. - pub fn write(&self, config: &Config, bytes: &[u8]) -> Result<()> { - match write(self.path(config), bytes) { - Ok(_) => Ok(()), - Err(e) => Err(Error::MiscError(e.to_string())), - } - } - - /// Delete this upload in the file system. - pub fn remove(&self, config: &Config) -> Result<()> { - let path = self.path(config); - - if !exists(&path).unwrap() { - return Ok(()); - } - - match remove_file(path) { - Ok(_) => Ok(()), - Err(e) => Err(Error::MiscError(e.to_string())), - } - } -} +pub use buckets_core::model::*; #[derive(Serialize, Deserialize)] pub struct CustomEmoji { diff --git a/crates/l10n/Cargo.toml b/crates/l10n/Cargo.toml index 05d53f5..4fcf312 100644 --- a/crates/l10n/Cargo.toml +++ b/crates/l10n/Cargo.toml @@ -3,6 +3,7 @@ name = "tetratto-l10n" description = "Localization for Tetratto" version = "12.0.0" edition = "2024" +readme = "../../README.md" authors.workspace = true repository.workspace = true license.workspace = true diff --git a/crates/shared/Cargo.toml b/crates/shared/Cargo.toml index 4b72694..5446ba7 100644 --- a/crates/shared/Cargo.toml +++ b/crates/shared/Cargo.toml @@ -3,6 +3,7 @@ name = "tetratto-shared" description = "Shared stuff for Tetratto" version = "12.0.6" edition = "2024" +readme = "../../README.md" authors.workspace = true repository.workspace = true license.workspace = true diff --git a/example/tetratto.toml b/example/tetratto.toml index a7c45e5..21b2932 100644 --- a/example/tetratto.toml +++ b/example/tetratto.toml @@ -4,7 +4,6 @@ color = "#c9b1bc" port = 4118 banned_hosts = [] host = "http://localhost:4118" -lw_host = "http://localhost:4119" no_track = [] banned_usernames = [ "admin", @@ -22,6 +21,10 @@ town_square = 166340372315581657 html_footer_path = "public/footer.html" system_user = 211903918383300608 +[service_hosts] +buckets = "http://localhost:8020" +littleweb = "http://localhost:4119" + [security] registration_enabled = true real_ip_header = "CF-Connecting-IP" diff --git a/sql_changes/apps_api_key.sql b/manual_migrations/apps_api_key.sql similarity index 100% rename from sql_changes/apps_api_key.sql rename to manual_migrations/apps_api_key.sql diff --git a/sql_changes/apps_data_used.sql b/manual_migrations/apps_data_used.sql similarity index 100% rename from sql_changes/apps_data_used.sql rename to manual_migrations/apps_data_used.sql diff --git a/sql_changes/apps_scopes.sql b/manual_migrations/apps_scopes.sql similarity index 100% rename from sql_changes/apps_scopes.sql rename to manual_migrations/apps_scopes.sql diff --git a/sql_changes/browser_session.sql b/manual_migrations/browser_session.sql similarity index 100% rename from sql_changes/browser_session.sql rename to manual_migrations/browser_session.sql diff --git a/sql_changes/channels_last_message.sql b/manual_migrations/channels_last_message.sql similarity index 100% rename from sql_changes/channels_last_message.sql rename to manual_migrations/channels_last_message.sql diff --git a/sql_changes/channels_members.sql b/manual_migrations/channels_members.sql similarity index 100% rename from sql_changes/channels_members.sql rename to manual_migrations/channels_members.sql diff --git a/sql_changes/channels_title.sql b/manual_migrations/channels_title.sql similarity index 100% rename from sql_changes/channels_title.sql rename to manual_migrations/channels_title.sql diff --git a/sql_changes/communities_is_forge.sql b/manual_migrations/communities_is_forge.sql similarity index 100% rename from sql_changes/communities_is_forge.sql rename to manual_migrations/communities_is_forge.sql diff --git a/sql_changes/communities_post_count.sql b/manual_migrations/communities_post_count.sql similarity index 100% rename from sql_changes/communities_post_count.sql rename to manual_migrations/communities_post_count.sql diff --git a/sql_changes/journals_dirs.sql b/manual_migrations/journals_dirs.sql similarity index 100% rename from sql_changes/journals_dirs.sql rename to manual_migrations/journals_dirs.sql diff --git a/sql_changes/messages_reactions.sql b/manual_migrations/messages_reactions.sql similarity index 100% rename from sql_changes/messages_reactions.sql rename to manual_migrations/messages_reactions.sql diff --git a/sql_changes/notes_dir_tags.sql b/manual_migrations/notes_dir_tags.sql similarity index 100% rename from sql_changes/notes_dir_tags.sql rename to manual_migrations/notes_dir_tags.sql diff --git a/sql_changes/notes_is_global.sql b/manual_migrations/notes_is_global.sql similarity index 100% rename from sql_changes/notes_is_global.sql rename to manual_migrations/notes_is_global.sql diff --git a/sql_changes/notifications_tag.sql b/manual_migrations/notifications_tag.sql similarity index 100% rename from sql_changes/notifications_tag.sql rename to manual_migrations/notifications_tag.sql diff --git a/sql_changes/posts_is_deleted.sql b/manual_migrations/posts_is_deleted.sql similarity index 100% rename from sql_changes/posts_is_deleted.sql rename to manual_migrations/posts_is_deleted.sql diff --git a/sql_changes/posts_is_open.sql b/manual_migrations/posts_is_open.sql similarity index 100% rename from sql_changes/posts_is_open.sql rename to manual_migrations/posts_is_open.sql diff --git a/sql_changes/posts_poll_id.sql b/manual_migrations/posts_poll_id.sql similarity index 100% rename from sql_changes/posts_poll_id.sql rename to manual_migrations/posts_poll_id.sql diff --git a/sql_changes/posts_stack.sql b/manual_migrations/posts_stack.sql similarity index 100% rename from sql_changes/posts_stack.sql rename to manual_migrations/posts_stack.sql diff --git a/sql_changes/posts_title.sql b/manual_migrations/posts_title.sql similarity index 100% rename from sql_changes/posts_title.sql rename to manual_migrations/posts_title.sql diff --git a/sql_changes/posts_tscvector_content.sql b/manual_migrations/posts_tscvector_content.sql similarity index 100% rename from sql_changes/posts_tscvector_content.sql rename to manual_migrations/posts_tscvector_content.sql diff --git a/sql_changes/posts_uploads.sql b/manual_migrations/posts_uploads.sql similarity index 100% rename from sql_changes/posts_uploads.sql rename to manual_migrations/posts_uploads.sql diff --git a/sql_changes/questions_context.sql b/manual_migrations/questions_context.sql similarity index 100% rename from sql_changes/questions_context.sql rename to manual_migrations/questions_context.sql diff --git a/sql_changes/questions_drawings.sql b/manual_migrations/questions_drawings.sql similarity index 100% rename from sql_changes/questions_drawings.sql rename to manual_migrations/questions_drawings.sql diff --git a/sql_changes/questions_ip.sql b/manual_migrations/questions_ip.sql similarity index 100% rename from sql_changes/questions_ip.sql rename to manual_migrations/questions_ip.sql diff --git a/sql_changes/questions_likes.sql b/manual_migrations/questions_likes.sql similarity index 100% rename from sql_changes/questions_likes.sql rename to manual_migrations/questions_likes.sql diff --git a/sql_changes/reactions_unique.sql b/manual_migrations/reactions_unique.sql similarity index 100% rename from sql_changes/reactions_unique.sql rename to manual_migrations/reactions_unique.sql diff --git a/sql_changes/requests_pkey.sql b/manual_migrations/requests_pkey.sql similarity index 100% rename from sql_changes/requests_pkey.sql rename to manual_migrations/requests_pkey.sql diff --git a/sql_changes/requests_pkeys.sql b/manual_migrations/requests_pkeys.sql similarity index 100% rename from sql_changes/requests_pkeys.sql rename to manual_migrations/requests_pkeys.sql diff --git a/sql_changes/services_revision.sql b/manual_migrations/services_revision.sql similarity index 100% rename from sql_changes/services_revision.sql rename to manual_migrations/services_revision.sql diff --git a/sql_changes/stacks_mode_sort.sql b/manual_migrations/stacks_mode_sort.sql similarity index 100% rename from sql_changes/stacks_mode_sort.sql rename to manual_migrations/stacks_mode_sort.sql diff --git a/manual_migrations/uploads.js b/manual_migrations/uploads.js new file mode 100644 index 0000000..96dfaf3 --- /dev/null +++ b/manual_migrations/uploads.js @@ -0,0 +1,43 @@ +import postgres from "npm:postgres"; +import { parse } from "npm:smol-toml"; + +const config = parse(await Deno.readTextFile(Deno.cwd() + "/tetratto.toml"), { + integersAsBigInt: true, +}); + +const db = postgres({ + user: config.database.user, + password: config.database.password, + database: config.database.name, + hostname: config.database.url.split(":")[0], + port: config.database.url.split(":")[1], +}); + +const whats = {}; +const alts = {}; + +for (const row of await db`SELECT * FROM uploads;`) { + whats[row.id] = row.what.replaceAll('"', ""); + alts[row.id] = row.alt; +} + +await db`ALTER TABLE uploads DROP COLUMN IF EXISTS what;`; +await db`ALTER TABLE uploads DROP COLUMN IF EXISTS alt;`; +await db`ALTER TABLE uploads ADD COLUMN IF NOT EXISTS bucket TEXT NOT NULL DEFAULT '';`; +await db`ALTER TABLE uploads ADD COLUMN IF NOT EXISTS metadata TEXT NOT NULL DEFAULT '';`; + +let i = 0; +for (const row of await db`SELECT * FROM uploads;`) { + await db`DELETE FROM uploads WHERE id = ${BigInt(row.id)}`; + await db`INSERT INTO uploads VALUES (${BigInt(row.id)}, ${BigInt(row.created)}, ${BigInt(row.owner)}, DEFAULT, ${JSON.stringify( + { + what: whats[row.id], + alt: whats[row.alt], + }, + )});`; + + i += 1; + console.log(`done ${i}`); +} + +await db.end(); diff --git a/sql_changes/uploads_alt.sql b/manual_migrations/uploads_alt.sql similarity index 100% rename from sql_changes/uploads_alt.sql rename to manual_migrations/uploads_alt.sql diff --git a/sql_changes/users_achievements.sql b/manual_migrations/users_achievements.sql similarity index 100% rename from sql_changes/users_achievements.sql rename to manual_migrations/users_achievements.sql diff --git a/sql_changes/users_associated.sql b/manual_migrations/users_associated.sql similarity index 100% rename from sql_changes/users_associated.sql rename to manual_migrations/users_associated.sql diff --git a/sql_changes/users_awaiting_purchase.sql b/manual_migrations/users_awaiting_purchase.sql similarity index 100% rename from sql_changes/users_awaiting_purchase.sql rename to manual_migrations/users_awaiting_purchase.sql diff --git a/sql_changes/users_ban_reason.sql b/manual_migrations/users_ban_reason.sql similarity index 100% rename from sql_changes/users_ban_reason.sql rename to manual_migrations/users_ban_reason.sql diff --git a/sql_changes/users_connections.sql b/manual_migrations/users_connections.sql similarity index 100% rename from sql_changes/users_connections.sql rename to manual_migrations/users_connections.sql diff --git a/sql_changes/users_grants.sql b/manual_migrations/users_grants.sql similarity index 100% rename from sql_changes/users_grants.sql rename to manual_migrations/users_grants.sql diff --git a/sql_changes/users_invite_code.sql b/manual_migrations/users_invite_code.sql similarity index 100% rename from sql_changes/users_invite_code.sql rename to manual_migrations/users_invite_code.sql diff --git a/sql_changes/users_layouts.sql b/manual_migrations/users_layouts.sql similarity index 100% rename from sql_changes/users_layouts.sql rename to manual_migrations/users_layouts.sql diff --git a/sql_changes/users_post_count.sql b/manual_migrations/users_post_count.sql similarity index 100% rename from sql_changes/users_post_count.sql rename to manual_migrations/users_post_count.sql diff --git a/sql_changes/users_request_count.sql b/manual_migrations/users_request_count.sql similarity index 100% rename from sql_changes/users_request_count.sql rename to manual_migrations/users_request_count.sql diff --git a/sql_changes/users_secondary_permissions.sql b/manual_migrations/users_secondary_permissions.sql similarity index 100% rename from sql_changes/users_secondary_permissions.sql rename to manual_migrations/users_secondary_permissions.sql diff --git a/sql_changes/users_seller_data.sql b/manual_migrations/users_seller_data.sql similarity index 100% rename from sql_changes/users_seller_data.sql rename to manual_migrations/users_seller_data.sql diff --git a/sql_changes/users_stripe_id.sql b/manual_migrations/users_stripe_id.sql similarity index 100% rename from sql_changes/users_stripe_id.sql rename to manual_migrations/users_stripe_id.sql diff --git a/sql_changes/users_totp.sql b/manual_migrations/users_totp.sql similarity index 100% rename from sql_changes/users_totp.sql rename to manual_migrations/users_totp.sql