mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 00:11:37 -06:00
feat(docker): add monorepo Docker build with auto-migrate and pdf-lib
This commit is contained in:
+34
-23
@@ -1,33 +1,44 @@
|
||||
# Stage 1: Build client
|
||||
FROM node:24-alpine AS client-builder
|
||||
WORKDIR /app/client
|
||||
COPY client/package.json client/package-lock.json ./
|
||||
RUN npm ci
|
||||
COPY client/ .
|
||||
RUN npm run build
|
||||
|
||||
# Stage 2: Build server
|
||||
FROM node:24-alpine AS server-builder
|
||||
WORKDIR /app/server
|
||||
COPY server/package.json server/package-lock.json ./
|
||||
RUN npm ci
|
||||
COPY server/ .
|
||||
RUN npm run build
|
||||
|
||||
# Stage 3: Final image — production only, no devDependencies
|
||||
FROM node:24-alpine AS final
|
||||
RUN npm install -g pnpm@10
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=server-builder /app/server/dist ./dist
|
||||
COPY --from=server-builder /app/server/package.json ./package.json
|
||||
COPY --from=server-builder /app/server/package-lock.json ./package-lock.json
|
||||
RUN npm ci --omit=dev
|
||||
COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./
|
||||
COPY packages/ packages/
|
||||
COPY client/package.json ./client/
|
||||
RUN pnpm install --filter client... --frozen-lockfile
|
||||
|
||||
# Vue build served as static files by NestJS
|
||||
COPY client/ ./client/
|
||||
RUN pnpm --filter client run build-only
|
||||
|
||||
# Stage 2: Build server + create prod deploy bundle
|
||||
FROM node:24-alpine AS server-builder
|
||||
RUN npm install -g pnpm@10
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./
|
||||
COPY packages/ packages/
|
||||
COPY server/package.json ./server/
|
||||
RUN pnpm install --filter server... --frozen-lockfile
|
||||
|
||||
COPY server/ ./server/
|
||||
RUN pnpm --filter server run build
|
||||
|
||||
# pnpm deploy prunes to prod deps; dist/ is gitignored so copy it in after
|
||||
RUN pnpm --filter server deploy --prod --legacy /deploy
|
||||
RUN cp -r /app/server/dist /deploy/dist
|
||||
|
||||
# Stage 3: Final image
|
||||
FROM node:24-alpine
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=server-builder /deploy ./
|
||||
COPY --from=client-builder /app/client/dist ./public
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
ENV NODE_ENV=production
|
||||
|
||||
CMD ["node", "dist/main.js"]
|
||||
COPY --from=server-builder /app/server/entrypoint.sh ./entrypoint.sh
|
||||
RUN chmod +x entrypoint.sh
|
||||
|
||||
CMD ["sh", "entrypoint.sh"]
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import type { Config } from 'drizzle-kit';
|
||||
import * as dotenv from 'dotenv';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
export default {
|
||||
schema: './src/db/schema',
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
exec node dist/main.js
|
||||
+2
-4
@@ -36,12 +36,10 @@
|
||||
"@nestjs/passport": "^11.0.5",
|
||||
"@nestjs/platform-fastify": "^11.1.11",
|
||||
"@nestjs/schedule": "^6.1.0",
|
||||
"@types/pdf-parse": "^1.1.5",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"chokidar": "^4.0.3",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.3",
|
||||
"drizzle-kit": "^0.31.8",
|
||||
"drizzle-orm": "^0.45.1",
|
||||
"fast-xml-parser": "^5.3.3",
|
||||
"jose": "^6.1.3",
|
||||
@@ -49,7 +47,7 @@
|
||||
"nodemailer": "^7.0.12",
|
||||
"passport": "^0.7.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"pdf-parse": "^2.4.5",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"pg": "^8.16.3",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
@@ -73,9 +71,9 @@
|
||||
"@types/nodemailer": "^7.0.5",
|
||||
"@types/passport-jwt": "^4.0.1",
|
||||
"@types/pg": "^8.16.0",
|
||||
"@types/sharp": "^0.32.0",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"@types/unzipper": "^0.10.11",
|
||||
"drizzle-kit": "^0.31.8",
|
||||
"eslint": "^9.18.0",
|
||||
"eslint-config-prettier": "^10.0.1",
|
||||
"eslint-plugin-prettier": "^5.2.2",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "7",
|
||||
"when": 1768364315691,
|
||||
"when": 1768382694329,
|
||||
"tag": "0000_add_oidc_schema",
|
||||
"breakpoints": true
|
||||
}
|
||||
|
||||
@@ -4,8 +4,14 @@ import { ValidationPipe } from '@nestjs/common';
|
||||
import { AppModule } from './app.module';
|
||||
import { GlobalExceptionFilter } from './common/filters/http-exception.filter';
|
||||
import { join } from 'path';
|
||||
import { drizzle } from 'drizzle-orm/node-postgres';
|
||||
import { migrate } from 'drizzle-orm/node-postgres/migrator';
|
||||
import { Pool } from 'pg';
|
||||
|
||||
async function bootstrap() {
|
||||
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
||||
await migrate(drizzle(pool), { migrationsFolder: join(__dirname, '..', 'src', 'db', 'migrations') });
|
||||
await pool.end();
|
||||
const app = await NestFactory.create<NestFastifyApplication>(AppModule, new FastifyAdapter({ logger: true }));
|
||||
|
||||
app.setGlobalPrefix('api');
|
||||
|
||||
@@ -3,7 +3,7 @@ import { mkdtemp, readFile, readdir, rm } from 'fs/promises';
|
||||
import { tmpdir } from 'os';
|
||||
import { join } from 'path';
|
||||
import { promisify } from 'util';
|
||||
import { PDFParse } from 'pdf-parse';
|
||||
import { PDFDocument } from 'pdf-lib';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
@@ -17,8 +17,8 @@ export interface PdfParsed {
|
||||
coverBuffer: Buffer | null;
|
||||
}
|
||||
|
||||
function clean(value: unknown): string | null {
|
||||
if (typeof value !== 'string') return null;
|
||||
function clean(value: string | undefined): string | null {
|
||||
if (!value) return null;
|
||||
const s = value.trim();
|
||||
return s.length > 0 ? s : null;
|
||||
}
|
||||
@@ -51,22 +51,16 @@ async function extractPdfCover(absolutePath: string): Promise<Buffer | null> {
|
||||
export async function parsePdfFile(absolutePath: string): Promise<PdfParsed | null> {
|
||||
try {
|
||||
const buf = await readFile(absolutePath);
|
||||
const parser = new PDFParse({ data: buf });
|
||||
const data = await parser.getInfo();
|
||||
await parser.destroy();
|
||||
const doc = await PDFDocument.load(buf, { ignoreEncryption: true });
|
||||
|
||||
const info = (data.info ?? {}) as Record<string, unknown>;
|
||||
|
||||
const title = clean(info['Title']);
|
||||
const authorRaw = clean(info['Author']);
|
||||
const subject = clean(info['Subject']);
|
||||
const keywords = splitKeywords(clean(info['Keywords']));
|
||||
// PDFs rarely store publisher; some use 'Creator' for the authoring app
|
||||
// and 'Producer' for the PDF engine — neither is a publisher field.
|
||||
const title = clean(doc.getTitle());
|
||||
const authorRaw = clean(doc.getAuthor());
|
||||
const subject = clean(doc.getSubject());
|
||||
const keywords = splitKeywords(clean(doc.getKeywords()));
|
||||
const publisher: string | null = null;
|
||||
const pageCount = typeof data.total === 'number' ? data.total : null;
|
||||
const pageCount = doc.getPageCount();
|
||||
|
||||
const authors: PdfParsed['authors'] = authorRaw
|
||||
const authors = authorRaw
|
||||
? authorRaw
|
||||
.split(/[,;]/)
|
||||
.map((s) => s.trim())
|
||||
|
||||
Reference in New Issue
Block a user