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 async_stream::try_stream;
|
||||||
use axum::{body::Body, response::Response};
|
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 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 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 response = if let Some(TypedHeader(range)) = range {
|
||||||
let ranges = range
|
let ranges = range
|
||||||
@ -240,7 +246,7 @@ impl StorageService {
|
|||||||
};
|
};
|
||||||
let body = Body::from_stream(stream);
|
let body = Body::from_stream(stream);
|
||||||
|
|
||||||
Response::builder()
|
let mut builder = Response::builder()
|
||||||
.status(StatusCode::PARTIAL_CONTENT)
|
.status(StatusCode::PARTIAL_CONTENT)
|
||||||
.header(
|
.header(
|
||||||
header::CONTENT_TYPE,
|
header::CONTENT_TYPE,
|
||||||
@ -248,17 +254,34 @@ impl StorageService {
|
|||||||
format!("multipart/byteranges; boundary={boundary}").as_str(),
|
format!("multipart/byteranges; boundary={boundary}").as_str(),
|
||||||
)
|
)
|
||||||
.unwrap(),
|
.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() {
|
} else if let Some((r, content_range)) = ranges.pop() {
|
||||||
let reader = self.reader(storage_path.as_ref()).await?;
|
let reader = self.reader(storage_path.as_ref()).await?;
|
||||||
let stream = reader.into_bytes_stream(r).await?;
|
let stream = reader.into_bytes_stream(r).await?;
|
||||||
|
|
||||||
Response::builder()
|
let mut builder = Response::builder()
|
||||||
.status(StatusCode::PARTIAL_CONTENT)
|
.status(StatusCode::PARTIAL_CONTENT)
|
||||||
.header(header::CONTENT_TYPE, content_type.clone())
|
.header(header::CONTENT_TYPE, content_type.clone())
|
||||||
.header(header::CONTENT_RANGE, content_range)
|
.header(header::CONTENT_RANGE, content_range);
|
||||||
.body(Body::from_stream(stream))?
|
|
||||||
|
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 {
|
} else {
|
||||||
unreachable!("ranges length should be greater than 0")
|
unreachable!("ranges length should be greater than 0")
|
||||||
}
|
}
|
||||||
@ -276,10 +299,19 @@ impl StorageService {
|
|||||||
let reader = self.reader(storage_path.as_ref()).await?;
|
let reader = self.reader(storage_path.as_ref()).await?;
|
||||||
let stream = reader.into_bytes_stream(..).await?;
|
let stream = reader.into_bytes_stream(..).await?;
|
||||||
|
|
||||||
Response::builder()
|
let mut builder = Response::builder()
|
||||||
.status(StatusCode::OK)
|
.status(StatusCode::OK)
|
||||||
.header(header::CONTENT_TYPE, content_type)
|
.header(header::CONTENT_TYPE, content_type);
|
||||||
.body(Body::from_stream(stream))?
|
|
||||||
|
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)
|
Ok(response)
|
||||||
|
@ -77,7 +77,6 @@
|
|||||||
"tw-animate-css": "^1.3.4",
|
"tw-animate-css": "^1.3.4",
|
||||||
"type-fest": "^4.41.0",
|
"type-fest": "^4.41.0",
|
||||||
"vaul": "^1.1.2",
|
"vaul": "^1.1.2",
|
||||||
"es-toolkit": "^1.39.3",
|
|
||||||
"@tanstack/react-router": "^1.121.2"
|
"@tanstack/react-router": "^1.121.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -1,16 +1,18 @@
|
|||||||
import { type ComponentProps } from "react";
|
import { type ComponentProps, useMemo, useState } from "react";
|
||||||
|
|
||||||
export type ImgProps = Omit<ComponentProps<"img">, "alt"> &
|
export type ImgProps = Omit<ComponentProps<"img">, "alt"> &
|
||||||
Required<Pick<ComponentProps<"img">, "alt">> & {
|
Required<Pick<ComponentProps<"img">, "alt">> & {
|
||||||
optimize?: boolean;
|
optimize?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
// biome-ignore lint/correctness/noUnusedVariables: <explanation>
|
|
||||||
const LEGACY_IMAGE_REGEX = /\.(jpg|jpeg|png|gif|svg)$/;
|
const LEGACY_IMAGE_REGEX = /\.(jpg|jpeg|png|gif|svg)$/;
|
||||||
|
|
||||||
export const Img = (props: ImgProps) => {
|
export const Img = (props: ImgProps) => {
|
||||||
const src = props.src;
|
const src = props.src;
|
||||||
|
|
||||||
|
const isLegacy = useMemo(() => src?.match(LEGACY_IMAGE_REGEX), [src]);
|
||||||
|
const [isError, setIsError] = useState(false);
|
||||||
|
|
||||||
if (!src) {
|
if (!src) {
|
||||||
// biome-ignore lint/nursery/noImgElement: <explanation>
|
// biome-ignore lint/nursery/noImgElement: <explanation>
|
||||||
return <img {...props} alt={props.alt} />;
|
return <img {...props} alt={props.alt} />;
|
||||||
@ -18,7 +20,19 @@ export const Img = (props: ImgProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<picture {...props}>
|
<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>
|
</picture>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
3
justfile
3
justfile
@ -11,6 +11,9 @@ prepare-dev-testcontainers:
|
|||||||
docker pull ghcr.io/dumtruck/konobangu-testing-torrents:latest
|
docker pull ghcr.io/dumtruck/konobangu-testing-torrents:latest
|
||||||
docker pull postgres:17-alpine
|
docker pull postgres:17-alpine
|
||||||
|
|
||||||
|
dev-optimize-images:
|
||||||
|
npx -y zx apps/recorder/examples/optimize_image.mjs
|
||||||
|
|
||||||
dev-webui:
|
dev-webui:
|
||||||
pnpm run --filter=webui dev
|
pnpm run --filter=webui dev
|
||||||
|
|
||||||
|
@ -19,6 +19,9 @@
|
|||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22"
|
"node": ">=22"
|
||||||
},
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"es-toolkit": "^1.39.3"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "1.9.4",
|
"@biomejs/biome": "1.9.4",
|
||||||
"@types/node": "^24.0.1",
|
"@types/node": "^24.0.1",
|
||||||
|
7
pnpm-lock.yaml
generated
7
pnpm-lock.yaml
generated
@ -10,6 +10,10 @@ overrides:
|
|||||||
importers:
|
importers:
|
||||||
|
|
||||||
.:
|
.:
|
||||||
|
dependencies:
|
||||||
|
es-toolkit:
|
||||||
|
specifier: ^1.39.3
|
||||||
|
version: 1.39.3
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@biomejs/biome':
|
'@biomejs/biome':
|
||||||
specifier: 1.9.4
|
specifier: 1.9.4
|
||||||
@ -209,9 +213,6 @@ importers:
|
|||||||
embla-carousel-react:
|
embla-carousel-react:
|
||||||
specifier: ^8.6.0
|
specifier: ^8.6.0
|
||||||
version: 8.6.0(react@19.1.0)
|
version: 8.6.0(react@19.1.0)
|
||||||
es-toolkit:
|
|
||||||
specifier: ^1.39.3
|
|
||||||
version: 1.39.3
|
|
||||||
graphiql:
|
graphiql:
|
||||||
specifier: ^4.1.2
|
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))
|
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