diff --git a/.github/workflows/rust.yaml b/.github/workflows/rust.yaml
index 8c8396fa..f0bd3389 100644
--- a/.github/workflows/rust.yaml
+++ b/.github/workflows/rust.yaml
@@ -67,7 +67,7 @@ jobs:
if: github.event_name != 'pull_request' && github.ref_name == 'main'
uses: nev7n/wait_for_response@v1
with:
- url: "http://localhost:8000/"
+ url: "http://localhost:14702/"
- name: Checkout API repository
if: github.event_name != 'pull_request' && github.ref_name == 'main'
@@ -79,7 +79,7 @@ jobs:
- name: Download OpenAPI specification
if: github.event_name != 'pull_request' && github.ref_name == 'main'
- run: curl http://localhost:8000/openapi.json -o api/OpenAPI.json
+ run: curl http://localhost:14702/openapi.json -o api/OpenAPI.json
- name: Commit changes
if: github.event_name != 'pull_request' && github.ref_name == 'main'
diff --git a/crates/services/autumn/src/api.rs b/crates/services/autumn/src/api.rs
index 59aac9a0..d068c2dc 100644
--- a/crates/services/autumn/src/api.rs
+++ b/crates/services/autumn/src/api.rs
@@ -216,6 +216,27 @@ async fn upload_file(
nanoid::nanoid!(42)
};
+ // Determine the mime type for the file
+ let mime_type = determine_mime_type(&mut file.contents, &buf, &filename);
+
+ // Check blocklist for mime type
+ if config
+ .files
+ .blocked_mime_types
+ .iter()
+ .any(|m| m == mime_type)
+ {
+ return Err(create_error!(FileTypeNotAllowed));
+ }
+
+ // Determine metadata for the file
+ let metadata = generate_metadata(&file.contents, mime_type);
+
+ // Block non-images for non-attachment uploads
+ if !matches!(tag, Tag::attachments) && !matches!(metadata, Metadata::Image { .. }) {
+ return Err(create_error!(FileTypeNotAllowed));
+ }
+
// Find an existing hash and use that if possible
let file_hash_exists = if let Ok(file_hash) = db
.fetch_attachment_hash(&format!("{original_hash:02x}"))
@@ -239,22 +260,6 @@ async fn upload_file(
false
};
- // Determine the mime type for the file
- let mime_type = determine_mime_type(&mut file.contents, &buf, &filename);
-
- // Check blocklist for mime type
- if config
- .files
- .blocked_mime_types
- .iter()
- .any(|m| m == mime_type)
- {
- return Err(create_error!(FileTypeNotAllowed));
- }
-
- // Determine metadata for the file
- let metadata = generate_metadata(&file.contents, mime_type);
-
// Strip metadata
let (buf, metadata) = strip_metadata(file.contents, buf, metadata, mime_type).await?;
@@ -322,21 +327,23 @@ pub static CACHE_CONTROL: &str = "public, max-age=604800, must-revalidate";
/// Fetch preview of file
///
-/// This route will only return image content.
+/// This route will only return image content.
/// For all other file types, please use the fetch route (you will receive a redirect if you try to use this route anyways!).
///
/// Depending on the given tag, the file will be re-processed to fit the criteria:
///
-/// | Tag | Image Resolution † |
-/// | :-: | --- |
-/// | attachments | Up to 1280px on any axis |
-/// | avatars | Up to 128px on any axis |
-/// | backgrounds | Up to 1280x720px |
-/// | icons | Up to 128px on any axis |
-/// | banners | Up to 480px on any axis |
-/// | emojis | Up to 128px on any axis |
+/// | Tag | Image Resolution † | Animations stripped by preview ‡ |
+/// | :-: | --- | :-: |
+/// | attachments | Up to 1280px on any axis | ❌ |
+/// | avatars | Up to 128px on any axis | ✅ |
+/// | backgrounds | Up to 1280x720px | ❌ |
+/// | icons | Up to 128px on any axis | ✅ |
+/// | banners | Up to 480px on any axis | ❌ |
+/// | emojis | Up to 128px on any axis | ❌ |
///
/// † aspect ratio will always be preserved
+///
+/// ‡ to fetch animated variant, suffix `/{file_name}` or `/original` to the path
#[utoipa::path(
get,
path = "/{tag}/{file_id}",
@@ -352,8 +359,8 @@ async fn fetch_preview(
State(db): State,
Path((tag, file_id)): Path<(Tag, String)>,
) -> Result {
- let tag: &'static str = tag.into();
- let file = db.fetch_attachment(tag, &file_id).await?;
+ let tag_str: &'static str = tag.clone().into();
+ let file = db.fetch_attachment(tag_str, &file_id).await?;
// Ignore deleted files
if file.deleted.is_some_and(|v| v) {
@@ -367,10 +374,14 @@ async fn fetch_preview(
let hash = file.as_hash(&db).await?;
- // Only process image files and don't process GIFs
- if !matches!(hash.metadata, Metadata::Image { .. }) || hash.content_type == "image/gif" {
+ let is_animated = hash.content_type == "image/gif"; // TODO: extract this data from files
+
+ // Only process image files and don't process GIFs if not avatar or icon
+ if !matches!(hash.metadata, Metadata::Image { .. })
+ || (is_animated && !matches!(tag, Tag::avatars | Tag::icons))
+ {
return Ok(
- Redirect::permanent(&format!("/{tag}/{file_id}/{}", file.filename)).into_response(),
+ Redirect::permanent(&format!("/{tag_str}/{file_id}/{}", file.filename)).into_response(),
);
}
@@ -380,7 +391,7 @@ async fn fetch_preview(
// Read image and create thumbnail
let data = create_thumbnail(
decode_image(&mut Cursor::new(data), &file.content_type)?,
- tag,
+ tag_str,
)
.await;
@@ -398,6 +409,8 @@ async fn fetch_preview(
/// Fetch original file
///
/// Content disposition header will be set to 'attachment' to prevent browser from rendering anything.
+///
+/// Using `original` as the file name parameter will redirect you to the original file.
#[utoipa::path(
get,
path = "/{tag}/{file_id}/{file_name}",
@@ -414,7 +427,8 @@ async fn fetch_file(
State(db): State,
Path((tag, file_id, file_name)): Path<(Tag, String, String)>,
) -> Result {
- let file = db.fetch_attachment(tag.into(), &file_id).await?;
+ let tag: &'static str = tag.clone().into();
+ let file = db.fetch_attachment(tag, &file_id).await?;
// Ignore deleted files
if file.deleted.is_some_and(|v| v) {
@@ -428,6 +442,12 @@ async fn fetch_file(
// Ensure filename is correct
if file_name != file.filename {
+ if file_name == "original" {
+ return Ok(
+ Redirect::permanent(&format!("/{tag}/{file_id}/{}", file.filename)).into_response(),
+ );
+ }
+
return Err(create_error!(NotFound));
}