feat: static server support etag
This commit is contained in:
parent
cc06142050
commit
c12b9b360a
45
apps/recorder/examples/optimize_image.mjs
Normal file
45
apps/recorder/examples/optimize_image.mjs
Normal file
@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env zx
|
||||
import { glob } from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { chunk } from 'es-toolkit/array';
|
||||
|
||||
const dataDir = path.join(import.meta.dirname, '../../../data')
|
||||
/**
|
||||
* @type {string[]}
|
||||
*/
|
||||
const images = [];
|
||||
for await (const image of glob('**/*.{jpg,jpeg,png,gif,svg}', {
|
||||
cwd: dataDir,
|
||||
})) {
|
||||
images.push(image)
|
||||
}
|
||||
|
||||
const cpus = os.cpus().length - 1;
|
||||
|
||||
const chunkSize = Math.ceil(images.length / cpus);
|
||||
const chunks = chunk(images, chunkSize);
|
||||
|
||||
/**
|
||||
* @param {string[]} images
|
||||
*/
|
||||
async function convertImages(images) {
|
||||
for await (const image of images) {
|
||||
const imagePath = path.resolve(dataDir, image)
|
||||
const webp = imagePath.replace(path.extname(imagePath), '.webp')
|
||||
const avif = imagePath.replace(path.extname(imagePath), '.avif')
|
||||
console.log(`Converting ${imagePath} to ${webp}...`);
|
||||
await $`ffmpeg -i "${imagePath}" -c:v libwebp -lossless 1 "${webp}"`;
|
||||
console.log(`Converting ${imagePath} to ${avif}...`);
|
||||
await $`ffmpeg -i "${imagePath}" -c:v libaom-av1 -still-picture 1 -pix_fmt yuv420p10le -crf 0 -strict experimental "${avif}"`;
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
chunks.map(convertImages)
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
use std::fmt;
|
||||
use std::{borrow::Cow, fmt};
|
||||
|
||||
use async_stream::try_stream;
|
||||
use axum::{body::Body, response::Response};
|
||||
@ -206,6 +206,12 @@ impl StorageService {
|
||||
let mime_type = mime_guess::from_path(storage_path.as_ref()).first_or_octet_stream();
|
||||
|
||||
let content_type = HeaderValue::from_str(mime_type.as_ref())?;
|
||||
let etag = metadata.etag().map(Cow::Borrowed).or_else(|| {
|
||||
let len = metadata.content_length();
|
||||
let lm = metadata.last_modified()?.timestamp();
|
||||
Some(Cow::Owned(format!("\"{lm:x}-{len:x}\"")))
|
||||
});
|
||||
let last_modified = metadata.last_modified().map(|lm| lm.to_rfc2822());
|
||||
|
||||
let response = if let Some(TypedHeader(range)) = range {
|
||||
let ranges = range
|
||||
@ -240,7 +246,7 @@ impl StorageService {
|
||||
};
|
||||
let body = Body::from_stream(stream);
|
||||
|
||||
Response::builder()
|
||||
let mut builder = Response::builder()
|
||||
.status(StatusCode::PARTIAL_CONTENT)
|
||||
.header(
|
||||
header::CONTENT_TYPE,
|
||||
@ -248,17 +254,34 @@ impl StorageService {
|
||||
format!("multipart/byteranges; boundary={boundary}").as_str(),
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
.body(body)?
|
||||
);
|
||||
|
||||
if let Some(etag) = etag {
|
||||
builder = builder.header(header::ETAG, etag.to_string());
|
||||
}
|
||||
|
||||
if let Some(last_modified) = last_modified {
|
||||
builder = builder.header(header::LAST_MODIFIED, last_modified);
|
||||
}
|
||||
|
||||
builder.body(body)?
|
||||
} else if let Some((r, content_range)) = ranges.pop() {
|
||||
let reader = self.reader(storage_path.as_ref()).await?;
|
||||
let stream = reader.into_bytes_stream(r).await?;
|
||||
|
||||
Response::builder()
|
||||
let mut builder = Response::builder()
|
||||
.status(StatusCode::PARTIAL_CONTENT)
|
||||
.header(header::CONTENT_TYPE, content_type.clone())
|
||||
.header(header::CONTENT_RANGE, content_range)
|
||||
.body(Body::from_stream(stream))?
|
||||
.header(header::CONTENT_RANGE, content_range);
|
||||
|
||||
if let Some(etag) = metadata.etag() {
|
||||
builder = builder.header(header::ETAG, etag);
|
||||
}
|
||||
if let Some(last_modified) = last_modified {
|
||||
builder = builder.header(header::LAST_MODIFIED, last_modified);
|
||||
}
|
||||
|
||||
builder.body(Body::from_stream(stream))?
|
||||
} else {
|
||||
unreachable!("ranges length should be greater than 0")
|
||||
}
|
||||
@ -276,10 +299,19 @@ impl StorageService {
|
||||
let reader = self.reader(storage_path.as_ref()).await?;
|
||||
let stream = reader.into_bytes_stream(..).await?;
|
||||
|
||||
Response::builder()
|
||||
let mut builder = Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header(header::CONTENT_TYPE, content_type)
|
||||
.body(Body::from_stream(stream))?
|
||||
.header(header::CONTENT_TYPE, content_type);
|
||||
|
||||
if let Some(etag) = etag {
|
||||
builder = builder.header(header::ETAG, etag.to_string());
|
||||
}
|
||||
|
||||
if let Some(last_modified) = last_modified {
|
||||
builder = builder.header(header::LAST_MODIFIED, last_modified);
|
||||
}
|
||||
|
||||
builder.body(Body::from_stream(stream))?
|
||||
};
|
||||
|
||||
Ok(response)
|
||||
|
@ -77,7 +77,6 @@
|
||||
"tw-animate-css": "^1.3.4",
|
||||
"type-fest": "^4.41.0",
|
||||
"vaul": "^1.1.2",
|
||||
"es-toolkit": "^1.39.3",
|
||||
"@tanstack/react-router": "^1.121.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -1,16 +1,18 @@
|
||||
import { type ComponentProps } from "react";
|
||||
import { type ComponentProps, useMemo, useState } from "react";
|
||||
|
||||
export type ImgProps = Omit<ComponentProps<"img">, "alt"> &
|
||||
Required<Pick<ComponentProps<"img">, "alt">> & {
|
||||
optimize?: boolean;
|
||||
};
|
||||
|
||||
// biome-ignore lint/correctness/noUnusedVariables: <explanation>
|
||||
const LEGACY_IMAGE_REGEX = /\.(jpg|jpeg|png|gif|svg)$/;
|
||||
|
||||
export const Img = (props: ImgProps) => {
|
||||
const src = props.src;
|
||||
|
||||
const isLegacy = useMemo(() => src?.match(LEGACY_IMAGE_REGEX), [src]);
|
||||
const [isError, setIsError] = useState(false);
|
||||
|
||||
if (!src) {
|
||||
// biome-ignore lint/nursery/noImgElement: <explanation>
|
||||
return <img {...props} alt={props.alt} />;
|
||||
@ -18,7 +20,19 @@ export const Img = (props: ImgProps) => {
|
||||
|
||||
return (
|
||||
<picture {...props}>
|
||||
<img {...props} alt={props.alt} />
|
||||
{isLegacy && !isError && (
|
||||
<>
|
||||
<source
|
||||
srcSet={src.replace(LEGACY_IMAGE_REGEX, ".webp")}
|
||||
type="image/webp"
|
||||
/>
|
||||
<source
|
||||
srcSet={src.replace(LEGACY_IMAGE_REGEX, ".avif")}
|
||||
type="image/avif"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<img {...props} alt={props.alt} onError={() => setIsError(true)} />
|
||||
</picture>
|
||||
);
|
||||
};
|
||||
|
3
justfile
3
justfile
@ -11,6 +11,9 @@ prepare-dev-testcontainers:
|
||||
docker pull ghcr.io/dumtruck/konobangu-testing-torrents:latest
|
||||
docker pull postgres:17-alpine
|
||||
|
||||
dev-optimize-images:
|
||||
npx -y zx apps/recorder/examples/optimize_image.mjs
|
||||
|
||||
dev-webui:
|
||||
pnpm run --filter=webui dev
|
||||
|
||||
|
@ -19,6 +19,9 @@
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
},
|
||||
"dependencies": {
|
||||
"es-toolkit": "^1.39.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "1.9.4",
|
||||
"@types/node": "^24.0.1",
|
||||
|
7
pnpm-lock.yaml
generated
7
pnpm-lock.yaml
generated
@ -10,6 +10,10 @@ overrides:
|
||||
importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
es-toolkit:
|
||||
specifier: ^1.39.3
|
||||
version: 1.39.3
|
||||
devDependencies:
|
||||
'@biomejs/biome':
|
||||
specifier: 1.9.4
|
||||
@ -209,9 +213,6 @@ importers:
|
||||
embla-carousel-react:
|
||||
specifier: ^8.6.0
|
||||
version: 8.6.0(react@19.1.0)
|
||||
es-toolkit:
|
||||
specifier: ^1.39.3
|
||||
version: 1.39.3
|
||||
graphiql:
|
||||
specifier: ^4.1.2
|
||||
version: 4.1.2(@codemirror/language@6.11.1)(@emotion/is-prop-valid@0.8.8)(@types/node@24.0.1)(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@6.0.5)))(graphql@16.11.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(use-sync-external-store@1.5.0(react@19.1.0))
|
||||
|
Loading…
Reference in New Issue
Block a user