Réponses 206 partielles avec Content-Range/Accept-Ranges pour la lecture PDF, types MIME explicites par format, en-tête Content-Disposition UTF-8 et nosniff. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
64 lines
2.4 KiB
TypeScript
64 lines
2.4 KiB
TypeScript
import { Controller, Get, Headers, Param, Query, Res, UseGuards } from "@nestjs/common";
|
|
import { FastifyReply } from "fastify";
|
|
import { lookup } from "mime-types";
|
|
import { BookQueryDto, BookQuerySchema } from "@readabook/shared";
|
|
import { AuthGuard } from "../auth/auth.guard.js";
|
|
import { ZodValidationPipe } from "../common/zod-validation.pipe.js";
|
|
import { BooksService } from "./books.service.js";
|
|
|
|
@Controller("books")
|
|
@UseGuards(AuthGuard)
|
|
export class BooksController {
|
|
constructor(private readonly books: BooksService) {}
|
|
|
|
@Get()
|
|
list(@Query(new ZodValidationPipe(BookQuerySchema)) query: BookQueryDto) {
|
|
return this.books.list(query);
|
|
}
|
|
|
|
@Get("search")
|
|
search(@Query(new ZodValidationPipe(BookQuerySchema)) query: BookQueryDto) {
|
|
return query.q ? this.books.search(query.q, query.limit, query.offset) : [];
|
|
}
|
|
|
|
@Get(":id")
|
|
get(@Param("id") id: string) {
|
|
return this.books.get(Number(id));
|
|
}
|
|
|
|
@Get(":id/pages")
|
|
pages(@Param("id") id: string) {
|
|
return this.books.listComicPages(Number(id));
|
|
}
|
|
|
|
@Get(":id/pages/:page")
|
|
async page(@Param("id") id: string, @Param("page") page: string, @Res() reply: FastifyReply) {
|
|
const result = await this.books.readComicPage(Number(id), Number(page));
|
|
reply.header("Content-Type", result.contentType);
|
|
reply.header("Cache-Control", "private, max-age=3600");
|
|
return reply.send(result.data);
|
|
}
|
|
|
|
@Get(":id/file")
|
|
file(@Param("id") id: string, @Headers("range") range: string | undefined, @Res() reply: FastifyReply) {
|
|
const { book, stream, contentLength, contentType, end, partial, size, start } = this.books.streamFile(Number(id), range);
|
|
if (partial) {
|
|
reply.code(206);
|
|
reply.header("Content-Range", `bytes ${start}-${end}/${size}`);
|
|
}
|
|
reply.header("Accept-Ranges", "bytes");
|
|
reply.header("Content-Length", String(contentLength));
|
|
reply.header("Content-Type", contentType || lookup(book.filePath) || "application/octet-stream");
|
|
reply.header("Content-Disposition", `inline; filename*=UTF-8''${encodeURIComponent(`${book.title}.${book.format}`)}`);
|
|
reply.header("X-Content-Type-Options", "nosniff");
|
|
return reply.send(stream);
|
|
}
|
|
|
|
@Get(":id/cover")
|
|
cover(@Param("id") id: string, @Res() reply: FastifyReply) {
|
|
const { coverPath, stream } = this.books.streamCover(Number(id));
|
|
reply.header("Content-Type", lookup(coverPath) || "image/jpeg");
|
|
return reply.send(stream);
|
|
}
|
|
}
|