Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
node_modules
dist
.git
.angular
backend/node_modules
backend/dist
backend/uploads
8 changes: 8 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# PostgreSQL
POSTGRES_USER=libreader
POSTGRES_PASSWORD=libreader_pass
POSTGRES_DB=libreader

# Backend
PORT=3030
FILE_STORAGE_STRATEGY=filesystem
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,7 @@ __screenshots__/
# System files
.DS_Store
Thumbs.db

backend/.env
backend/node_modules
backend/uploads
21 changes: 21 additions & 0 deletions Dockerfile.frontend
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
FROM node:20-alpine AS build

WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build -- --configuration=production

FROM nginx:alpine

# Copy Angular build output
COPY --from=build /app/dist/lib-reader/browser /usr/share/nginx/html

# Copy custom nginx config
COPY nginx.conf /etc/nginx/conf.d/default.conf

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]
6 changes: 6 additions & 0 deletions angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"budgets": [
{
"type": "initial",
Expand Down
4 changes: 4 additions & 0 deletions backend/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules
dist
uploads
.env
11 changes: 11 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Backend Environment Variables
POSTGRES_USER=libreader
POSTGRES_PASSWORD=libreader_pass
POSTGRES_DB=libreader
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
PORT=3030
NODE_ENV=development
FILE_STORAGE_PATH=./uploads
# Set to 'database' to store files in PostgreSQL BYTEA, 'filesystem' for disk storage
FILE_STORAGE_STRATEGY=filesystem
16 changes: 16 additions & 0 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
FROM node:20-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production

COPY . .
RUN npm run build

# Create uploads directory
RUN mkdir -p uploads

EXPOSE 3030

CMD ["node", "dist/server.js"]
164 changes: 164 additions & 0 deletions backend/migrations/1700000000000-InitialSchema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class InitialSchema1700000000000 implements MigrationInterface {
name = 'InitialSchema1700000000000';

public async up(queryRunner: QueryRunner): Promise<void> {
// Shelves table (must come first for FK reference)
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS "shelves" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
"name" varchar(200) NOT NULL,
"color" varchar(7) NOT NULL,
"display_order" integer NOT NULL,
"created_at" timestamptz NOT NULL DEFAULT now(),
"updated_at" timestamptz NOT NULL DEFAULT now()
)
`);

// Documents table
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS "documents" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
"title" varchar(500) NOT NULL,
"type" varchar(10) NOT NULL CHECK ("type" IN ('epub', 'pdf')),
"file_size" bigint NOT NULL,
"upload_date" timestamptz NOT NULL DEFAULT now(),
"last_opened" timestamptz,
"current_page" integer,
"total_pages" integer,
"current_cfi" text,
"reading_progress_percent" numeric(5,2),
"shelf_id" uuid REFERENCES "shelves"("id") ON DELETE SET NULL,
"created_at" timestamptz NOT NULL DEFAULT now(),
"updated_at" timestamptz NOT NULL DEFAULT now()
)
`);

// Book metadata
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS "book_metadata" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
"document_id" uuid NOT NULL UNIQUE REFERENCES "documents"("id") ON DELETE CASCADE,
"author" varchar(500),
"publisher" varchar(500),
"publish_year" varchar(10),
"isbn" varchar(20),
"cover_url" text,
"description" text,
"page_count" integer,
"open_library_key" varchar(100),
"created_at" timestamptz NOT NULL DEFAULT now(),
"updated_at" timestamptz NOT NULL DEFAULT now()
)
`);

// Subjects
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS "subjects" (
"id" serial PRIMARY KEY,
"name" varchar(200) NOT NULL UNIQUE
)
`);

// Book subjects (many-to-many)
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS "book_subjects" (
"book_metadata_id" uuid REFERENCES "book_metadata"("id") ON DELETE CASCADE,
"subject_id" integer REFERENCES "subjects"("id") ON DELETE CASCADE,
PRIMARY KEY ("book_metadata_id", "subject_id")
)
`);

// Bookmarks
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS "bookmarks" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
"document_id" uuid NOT NULL REFERENCES "documents"("id") ON DELETE CASCADE,
"location" varchar(500) NOT NULL,
"label" varchar(500) NOT NULL,
"note" text,
"created_at" timestamptz NOT NULL DEFAULT now()
)
`);

// Reading sessions
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS "reading_sessions" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
"document_id" uuid NOT NULL REFERENCES "documents"("id") ON DELETE CASCADE,
"started_at" timestamptz NOT NULL,
"ended_at" timestamptz NOT NULL,
"duration" integer NOT NULL,
"pages_read" integer NOT NULL,
"created_at" timestamptz NOT NULL DEFAULT now()
)
`);

// Reading stats (aggregated)
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS "reading_stats" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
"document_id" uuid NOT NULL UNIQUE REFERENCES "documents"("id") ON DELETE CASCADE,
"total_reading_time" bigint NOT NULL DEFAULT 0,
"first_opened_at" timestamptz,
"created_at" timestamptz NOT NULL DEFAULT now(),
"updated_at" timestamptz NOT NULL DEFAULT now()
)
`);

// Reading goals
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS "reading_goals" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
"document_id" uuid NOT NULL UNIQUE REFERENCES "documents"("id") ON DELETE CASCADE,
"daily_minutes" integer NOT NULL,
"current_streak" integer NOT NULL DEFAULT 0,
"created_at" timestamptz NOT NULL DEFAULT now(),
"updated_at" timestamptz NOT NULL DEFAULT now()
)
`);

// Reading goal completed days
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS "reading_goal_completed_days" (
"id" serial PRIMARY KEY,
"reading_goal_id" uuid NOT NULL REFERENCES "reading_goals"("id") ON DELETE CASCADE,
"completed_date" date NOT NULL,
CONSTRAINT "unique_goal_date" UNIQUE ("reading_goal_id", "completed_date")
)
`);

// Document files (for database storage strategy)
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS "document_files" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
"document_id" uuid NOT NULL UNIQUE REFERENCES "documents"("id") ON DELETE CASCADE,
"file_path" text,
"file_data" bytea,
"mime_type" varchar(100) NOT NULL,
"created_at" timestamptz NOT NULL DEFAULT now()
)
`);

// Indexes for performance
await queryRunner.query(`CREATE INDEX IF NOT EXISTS "idx_documents_shelf_id" ON "documents"("shelf_id")`);
await queryRunner.query(`CREATE INDEX IF NOT EXISTS "idx_bookmarks_document_id" ON "bookmarks"("document_id")`);
await queryRunner.query(`CREATE INDEX IF NOT EXISTS "idx_reading_sessions_document_id" ON "reading_sessions"("document_id")`);
await queryRunner.query(`CREATE INDEX IF NOT EXISTS "idx_reading_sessions_started_at" ON "reading_sessions"("started_at")`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE IF EXISTS "reading_goal_completed_days" CASCADE`);
await queryRunner.query(`DROP TABLE IF EXISTS "reading_goals" CASCADE`);
await queryRunner.query(`DROP TABLE IF EXISTS "reading_stats" CASCADE`);
await queryRunner.query(`DROP TABLE IF EXISTS "reading_sessions" CASCADE`);
await queryRunner.query(`DROP TABLE IF EXISTS "bookmarks" CASCADE`);
await queryRunner.query(`DROP TABLE IF EXISTS "book_subjects" CASCADE`);
await queryRunner.query(`DROP TABLE IF EXISTS "subjects" CASCADE`);
await queryRunner.query(`DROP TABLE IF EXISTS "book_metadata" CASCADE`);
await queryRunner.query(`DROP TABLE IF EXISTS "document_files" CASCADE`);
await queryRunner.query(`DROP TABLE IF EXISTS "documents" CASCADE`);
await queryRunner.query(`DROP TABLE IF EXISTS "shelves" CASCADE`);
}
}
19 changes: 19 additions & 0 deletions backend/migrations/1708523246000-AddBookmarkChapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class AddBookmarkChapter1708523246000 implements MigrationInterface {
name = 'AddBookmarkChapter1708523246000';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE "bookmarks"
ADD COLUMN IF NOT EXISTS "chapter" varchar(500)
`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE "bookmarks"
DROP COLUMN IF EXISTS "chapter"
`);
}
}
Loading
Loading