This commit is contained in:
2025-11-27 02:43:23 +00:00
parent 8013dec09e
commit 7da7148569
12 changed files with 310 additions and 313 deletions
+12 -22
View File
@@ -1,32 +1,21 @@
# COS498 HW3: PDF Document Management System # COS498 HW3: PDF Document Management System
This is Homework 3 for **COS498: Server Side Programming Languages** that demonstrates a PDF document management system with custom routing and validation modules. This is Homework 3 for **COS498: Server Side Programming Languages** that demonstrates a PDF document management system with custom routing.
## Project Overview ## Project Overview
This project implements a PDF document management system with the following features: This project implements a PDF document management system with the following features:
- **Frontend**: Nginx serving static files and Handlebars templates - **Frontend**: Nginx serving static files and Handlebars templates
- **Backend**: Node.js/Express server with custom routing and PDF management - **Backend**: Node.js/Express server with custom routing
- **PDF Management**: PDF validation and secure serving system - **JSON Metadata**: Book and chapter metadata stored as JSON files
- **Security**: Path validation and access control for PDF serving
- **Database Integration**: SQLite database for book and chapter metadata
- **Containerization**: Docker containers orchestrated with Docker Compose - **Containerization**: Docker containers orchestrated with Docker Compose
## Features ## Features
### PDF Document Management ### PDF Document Management
- **PDF Validation Module**: Comprehensive validation before serving any PDF
- **Custom Routing**: Dedicated routing module for book and chapter navigation - **Custom Routing**: Dedicated routing module for book and chapter navigation
- **Security Controls**: Path validation and access restrictions - **JSON Metadata**: Book and chapter information stored in JSON format
- **Database Integration**: SQLite database with books and chapters metadata - **File Serving**: Direct access to PDF files through designated routes
### PDF Validation System
- **File Existence Checks**: Validates PDF files exist before serving
- **Path Security**: Prevents access outside designated directories
- **Input Validation**: Sanitizes book names and filenames
- **Extension Validation**: Only allows `.pdf` files
- **File Size Limits**: Enforces maximum file size restrictions
- **Error Responses**: Appropriate HTTP status codes (400, 403, 404, 413, 500)
### User Interface ### User Interface
- **Book Browser**: Navigate through available books and chapters - **Book Browser**: Navigate through available books and chapters
@@ -52,14 +41,13 @@ COS498-HW3/
│ ├── Dockerfile # Backend container configuration │ ├── Dockerfile # Backend container configuration
│ ├── package.json # Node.js dependencies and scripts │ ├── package.json # Node.js dependencies and scripts
│ ├── package-lock.json # Locked dependency versions │ ├── package-lock.json # Locked dependency versions
│ ├── server.js # Node.js Express server with PDF management │ ├── server.js # Node.js Express server
│ ├── database/ │ ├── database/
│ │ ── backend.db # SQLite database with book metadata │ │ ── backend.schema # Database schema definition
│ │ ├── backend.schema # Database schema definition
│ │ └── backend_initial.db # Initial database backup
│ └── modules/ │ └── modules/
│ ├── RoutingManager.js # Custom routing module │ ├── RoutingManager.js # Custom routing module
── PDFValidationManager.js # PDF validation and security ── PDFValidationManager.js # PDF validation module
│ └── PDFDatabaseManager.js # Database management module
└── frontend/ └── frontend/
├── Dockerfile # Frontend container configuration ├── Dockerfile # Frontend container configuration
├── default.conf # Nginx configuration ├── default.conf # Nginx configuration
@@ -74,7 +62,9 @@ COS498-HW3/
│ └── WaysOfTheWorld-Strayer/ # Sample textbook (git submodule) │ └── WaysOfTheWorld-Strayer/ # Sample textbook (git submodule)
│ ├── README.md # Book information │ ├── README.md # Book information
│ ├── .git # Submodule repository metadata │ ├── .git # Submodule repository metadata
── chapter1-23.pdf # 23 chapter PDF files ── bookMetadata.json # Book metadata
│ ├── chapter1-23.json # Chapter metadata files
│ └── chapter1-23.pdf # 23 chapter PDF files
└── styles/ └── styles/
└── main.css # Application stylesheet └── main.css # Application stylesheet
``` ```
Binary file not shown.
+1 -27
View File
@@ -13,30 +13,4 @@ CREATE TABLE IF NOT EXISTS "chapters" (
"book_id" INTEGER NOT NULL, "book_id" INTEGER NOT NULL,
PRIMARY KEY("id" AUTOINCREMENT), PRIMARY KEY("id" AUTOINCREMENT),
FOREIGN KEY("book_id") REFERENCES "books"("id") FOREIGN KEY("book_id") REFERENCES "books"("id")
); );
INSERT INTO chapters
(chapter_number, display_name, filename, book_id)
VALUES
(1, "First Peoples; First Farmers", "chapter1.pdf", 1),
(2, "First Civilizations", "chapter2.pdf", 1),
(3, "State and Empire in Eurasia / North Africa", "chapter3.pdf", 1),
(4, "Culture and Religion in Eurasia / North Africa", "chapter4.pdf", 1),
(5, "Society and Inequality in Eurasia / North Africa", "chapter5.pdf", 1),
(6, "Commonalities and Variations", "chapter6.pdf", 1),
(7, "Commerce and Culture", "chapter7.pdf", 1),
(8, "China and the World", "chapter8.pdf", 1),
(9, "The Worlds Of Island", "chapter9.pdf", 1),
(10, "The Worlds of Christendom", "chapter10.pdf", 1),
(11, "Pastoral Peoples on the Global Stage", "chapter11.pdf", 1),
(12, "The Worlds of the Fifteenth Century", "chapter12.pdf", 1),
(13, "Political Transformations", "chapter13.pdf", 1),
(14, "Economic Transformations", "chapter14.pdf", 1),
(15, "Cultural Transformations", "chapter15.pdf", 1),
(16, "Atlantic Revolutions, Global Echos", "chapter16.pdf", 1),
(17, "Revolutions of Industrialization", "chapter17.pdf", 1),
(18, "Colonial Encounters in Asia and Africa", "chapter18.pdf", 1),
(19, "Empires in Collision", "chapter19.pdf", 1),
(20, "Collapse at the Center", "chapter20.pdf", 1),
(21, "Revolution, Socialism, and Global Conflict", "chapter21.pdf", 1),
(22, "The End of Empire", "chapter22.pdf", 1),
(23, "Capitalism and Culture", "chapter23.pdf", 1)
Binary file not shown.
+124
View File
@@ -0,0 +1,124 @@
// Imports
const sqlite3 = require('sqlite3').verbose();
const fs = require('fs');
const path = require('path');
// Configuration Constants
const DB_PATH = "database/backend.db";
const SCHEMA_PATH = "database/backend.schema";
const BOOK_PATH = "../frontend/public/books";
// PDF Database Manager - Manages SQLite database for storing PDF metadata
// Also handles scanning book directory and building DB
class PDFDatabaseManager {
constructor() {
if (fs.existsSync(DB_PATH)) {
fs.unlinkSync(DB_PATH);
}
this.db = new sqlite3.Database(DB_PATH);
this.createDatabase();
this.scanAndPopulateDatabase();
}
// Scans the books folder and adds all books / chapters to the database
async scanAndPopulateDatabase() {
const bookFolders = fs.readdirSync(BOOK_PATH);
bookFolders.forEach(folder => {
const bookDir = path.join(BOOK_PATH, folder);
const bookMetadataPath = path.join(bookDir, 'bookMetadata.json');
if (fs.existsSync(bookMetadataPath)) {
const bookMetadata = JSON.parse(fs.readFileSync(bookMetadataPath, 'utf-8'));
this.addBook(bookMetadata);
const chapterFiles = fs.readdirSync(bookDir).filter(file => file.endsWith('.json') && file !== 'bookMetadata.json');
chapterFiles.forEach(async chapterFile => {
const chapterMetadataPath = path.join(bookDir, chapterFile);
const chapterMetadata = JSON.parse(fs.readFileSync(chapterMetadataPath, 'utf-8'));
chapterMetadata.book_id = await this.getBookIdByFolderName(folder);
this.addChapter(chapterMetadata);
});
}
});
}
// Adds chapter metadata from FS to db
addChapter(chapterMetadata) {
let chapterQuery = `INSERT INTO chapters (chapter_number, display_name, filename, book_id) VALUES (?, ?, ?, ?)`;
this.db.run(chapterQuery, [chapterMetadata.chapter_number, chapterMetadata.display_name, chapterMetadata.filename, chapterMetadata.book_id]);
}
// Adds book metadata from FS to db
addBook(bookMetadata) {
let bookQuery = `INSERT INTO books (folder_name, display_name, author) VALUES (?, ?, ?)`;
this.db.run(bookQuery, [bookMetadata.folder_name, bookMetadata.display_name, bookMetadata.author]);
}
// Retrieves book ID by folder name
getBookIdByFolderName(folderName) {
return new Promise((resolve) => {
let query = `SELECT id FROM books WHERE folder_name = ?`;
this.db.get(query, [folderName], (err, row) => {
if (err) {
resolve(null);
} else {
resolve(row ? row.id : null);
}
});
});
}
// Creates the database if it doesn't exist
createDatabase() {
// Load DB schema from file and initialize database
const schema = fs.readFileSync(SCHEMA_PATH, 'utf-8');
this.db.exec(schema, (err) => {
if (err) {
console.error('Error creating database schema:', err.message);
} else {
console.log('Database schema created successfully.');
}
});
}
// List all books in the database
loadBooks() {
return new Promise((resolve, reject) => {
const query = 'SELECT id, folder_name, display_name, author FROM books ORDER BY display_name';
this.db.all(query, [], (err, rows) => {
const books = rows.map(row => ({
id: row.id,
name: row.folder_name,
displayName: row.display_name,
author: row.author
}));
resolve(books);
});
});
}
// List all chapters for a specific book
loadChapters(bookName) {
return new Promise((resolve, reject) => {
const query = `
SELECT c.id, c.chapter_number, c.display_name, c.filename, b.folder_name
FROM chapters c
JOIN books b ON c.book_id = b.id
WHERE b.folder_name = ?
ORDER BY c.chapter_number
`;
this.db.all(query, [bookName], (err, rows) => {
const chapters = rows.map(row => ({
id: row.id,
chapterNumber: row.chapter_number,
filename: row.filename,
displayName: row.display_name,
url: `/pdf/${row.folder_name}/${row.filename}`
}));
resolve(chapters);
});
});
}
}
module.exports = PDFDatabaseManager;
+17 -44
View File
@@ -1,52 +1,25 @@
// Imports
const fs = require('fs').promises; const fs = require('fs').promises;
const path = require('path'); const path = require('path');
/** // Configuration Constants
* PDF Validation Module - Validates PDF existence and access permissions const BOOK_PATH = "../frontend/public/books";
* Ensures only authorized access to PDFs within designated folders
*/ // PDF Validation Module - Validates PDF existence and access permissions
// Ensures only authorized access to PDFs within designated folders
class PDFValidationManager { class PDFValidationManager {
constructor(baseDir = '/app/frontend/public/books') { // Validate if a requested PDF document exists and is accessible
this.baseDir = path.resolve(baseDir); static async validatePDF(bookName, filename) {
}
/**
* Validate if a requested PDF document exists and is accessible
*/
async validatePDF(bookName, filename) {
try { try {
// Construct file path const filePath = path.resolve(BOOK_PATH, bookName, filename)
const filePath = path.resolve(this.baseDir, bookName, filename) const file = await fs.stat(filePath);
if (file.isFile()) {
// Check if file exists and get file stats return filePath;
const fileStats = await fs.stat(filePath); } else {
if (!fileStats.isFile()) { throw new Error('Invalid PDF request');
return { }
valid: false, } catch {
error: 'File not found or is not a regular file', throw new Error('Invalid PDF request');
statusCode: 404
};
}
// All validations passed
return {
valid: true,
filePath: filePath,
fileStats: {
size: fileStats.size,
lastModified: fileStats.mtime,
isFile: fileStats.isFile()
}
};
} catch (error) {
console.error(`PDF validation error for ${bookName}/${filename}:`, error);
return {
valid: false,
error: 'Internal validation error',
statusCode: 500
};
} }
} }
} }
+147 -141
View File
@@ -1,154 +1,160 @@
// Helper functions for server-side rendering // Imports
function loadBooks(db) { const hbs = require('hbs');
return new Promise((resolve, reject) => { const express = require('express');
const query = 'SELECT id, folder_name, display_name, author FROM books ORDER BY display_name';
db.all(query, [], (err, rows) => {
if (err) {
console.error('Error loading books from database:', err.message);
resolve([]);
} else {
const books = rows.map(row => ({
id: row.id,
name: row.folder_name,
displayName: row.display_name || row.folder_name.replace(/-/g, ' '),
author: row.author
}));
resolve(books);
}
});
});
}
function loadChapters(db, bookName) { // Local Module Imports
return new Promise((resolve, reject) => { const PDFValidationManager = require('./PDFValidationManager');
const query = ` const PDFDatabaseManager = require('./PDFDatabaseManager');
SELECT c.id, c.chapter_number, c.display_name, c.filename, b.folder_name
FROM chapters c
JOIN books b ON c.book_id = b.id
WHERE b.folder_name = ?
ORDER BY c.chapter_number
`;
db.all(query, [bookName], (err, rows) => {
if (err) {
console.error('Error loading chapters from database:', err.message);
resolve([]);
} else {
const chapters = rows.map(row => ({
id: row.id,
chapterNumber: row.chapter_number,
filename: row.filename,
displayName: row.display_name,
url: `/pdf/${row.folder_name}/${encodeURIComponent(row.filename)}`
}));
resolve(chapters);
}
});
});
}
// Page Route handlers class RoutingManager {
async function homePage(db, req, res) { constructor(app) {
try { this.databaseManager = new PDFDatabaseManager();
const books = await loadBooks(db); this.db = this.databaseManager.db;
res.render('index', { this.app = app;
title: 'Book Viewer', }
books: books,
selectedBook: null, // Setup Routing Manager
selectedChapter: null, setup() {
chapters: [] this.setupHandlebars();
}); this.middleware();
} catch (error) { this.setupRoutes();
console.error('Error loading books for home page:', error); }
res.render('index', {
title: 'Book Viewer', // Setup Handlebars
books: [], setupHandlebars() {
selectedBook: null, // Handlebars - Views
selectedChapter: null, this.app.set('view engine', 'hbs');
chapters: [], this.app.set('views', '../frontend/views');
error: 'Failed to load books' // Handlebars - Partials
hbs.registerPartials('../frontend/partials');
// Handlebars Helpers
hbs.registerHelper('eq', function(a, b) {
return a === b;
}); });
} }
}
// Render page with specific book // Middleware for Application
async function bookPage(db, req, res) { middleware() {
const bookName = req.params.bookName; this.app.use(express.static('../frontend/public'));
this.app.use(express.json());
try { // this.app.use(cookieParser());
const [books, chapters] = await Promise.all([ }
loadBooks(db),
loadChapters(db, bookName) // Setup Page Routes
]); setupRoutes() {
const selectedBook = books.find(book => book.name === bookName); this.app.get('/', async (req, res) => {
await this.homePage(req, res);
if (!selectedBook) { });
return res.redirect('/');
this.app.get('/book/:bookName', async (req, res) => {
await this.bookPage(req, res);
});
this.app.get('/book/:bookName/chapter/:chapterFile', async (req, res) => {
await this.chapterPage(req, res);
});
this.app.get('/pdf/:bookName/:chapterFile', async (req, res) => {
await this.servePDF(req, res);
});
}
// Home Page Handler
async homePage(req, res) {
try {
const books = await this.databaseManager.loadBooks();
res.render('index', {
title: 'Book Viewer',
books: books,
selectedBook: null,
selectedChapter: null,
chapters: []
});
} catch (error) {
res.render('index', {
title: 'Book Viewer',
books: [],
selectedBook: null,
selectedChapter: null,
chapters: [],
error: 'Failed to load books'
});
} }
res.render('index', {
title: 'Book Viewer',
books: books,
selectedBook: selectedBook,
selectedChapter: null,
chapters: chapters
});
} catch (error) {
console.error('Error loading book page:', error);
res.redirect('/');
} }
}
// Render page with specific chapter // Book Page Handler
async function chapterPage(db, req, res) { async bookPage(req, res) {
const bookName = req.params.bookName; const bookName = req.params.bookName;
const chapterFile = decodeURIComponent(req.params.chapterFile); try {
const [books, chapters] = await Promise.all([
try { this.databaseManager.loadBooks(),
const [books, chapters] = await Promise.all([ this.databaseManager.loadChapters(bookName)
loadBooks(db), ]);
loadChapters(db, bookName) const selectedBook = books.find(book => book.name === bookName);
]);
const selectedBook = books.find(book => book.name === bookName); if (!selectedBook) {
const selectedChapter = chapters.find(chapter => chapter.filename === chapterFile); return res.redirect('/');
}
if (!selectedBook || !selectedChapter) {
return res.redirect('/'); res.render('index', {
title: 'Book Viewer',
books: books,
selectedBook: selectedBook,
selectedChapter: null,
chapters: chapters
});
} catch (error) {
res.redirect('/');
} }
}
// Chapter Page Handler
async chapterPage(req, res) {
const bookName = req.params.bookName;
const chapterFile = req.params.chapterFile;
res.render('index', { try {
title: 'Book Viewer', const [books, chapters] = await Promise.all([
books: books, this.databaseManager.loadBooks(),
selectedBook: selectedBook, this.databaseManager.loadChapters(bookName)
selectedChapter: selectedChapter, ]);
chapters: chapters const selectedBook = books.find(book => book.name === bookName);
}); const selectedChapter = chapters.find(chapter => chapter.filename === chapterFile);
} catch (error) {
console.error('Error loading chapter page:', error); if (!selectedBook || !selectedChapter) {
res.redirect('/'); return res.redirect('/');
}
res.render('index', {
title: 'Book Viewer',
books: books,
selectedBook: selectedBook,
selectedChapter: selectedChapter,
chapters: chapters
});
} catch (error) {
res.redirect('/');
}
}
// PDF Serving Handler
async servePDF(req, res) {
const { bookName, chapterFile } = req.params;
try {
// Validate PDF using validation module
const filePath = await PDFValidationManager.validatePDF(bookName, chapterFile);
// Serve the PDF file using sendFile
res.sendFile(filePath, (sendErr) => {
if (sendErr) {
res.status(500).json({ error: 'Error serving PDF file' });
}
});
} catch (error) {
res.status(500).json({ error: 'Invalid PDF request' });
}
} }
} }
// Setup routes function module.exports = RoutingManager;
function setupRoutes(app, db) {
// Page Routes
app.get('/', async (req, res) => {
await homePage(db, req, res);
});
app.get('/book/:bookName', async (req, res) => {
await bookPage(db, req, res);
});
app.get('/book/:bookName/chapter/:chapterFile', async (req, res) => {
await chapterPage(db, req, res);
});
}
module.exports = {
setupRoutes,
loadBooks,
loadChapters,
homePage,
bookPage,
chapterPage
};
+5 -75
View File
@@ -1,85 +1,15 @@
// Imports
const express = require('express'); const express = require('express');
const hbs = require('hbs');
const cookieParser = require('cookie-parser');
const path = require('path');
const fs = require('fs');
const sqlite3 = require('sqlite3').verbose();
const app = express(); const app = express();
const PORT = process.env.PORT || 3000; const PORT = process.env.PORT || 3000;
// Handlebars // Local Module Imports
app.set('view engine', 'hbs');
app.set('views', '../frontend/views');
// Register partials directory
hbs.registerPartials('../frontend/partials');
// Register Handlebars helpers
hbs.registerHelper('eq', function(a, b) {
return a === b;
});
hbs.registerHelper('encodeURIComponent', function(str) {
return encodeURIComponent(str);
});
// Database setup
const dbPath = path.join(__dirname, 'database/backend.db');
const db = new sqlite3.Database(dbPath, (err) => {
if (err) {
console.error('Error opening database:', err.message);
}
});
// Import routing module
const RoutingManager = require('./modules/RoutingManager'); const RoutingManager = require('./modules/RoutingManager');
const PDFValidationManager = require('./modules/PDFValidationManager');
// Initialize PDF validation module
const pdfValidator = new PDFValidationManager();
// Middleware
// Serve only specific static assets, not the entire public directory
app.use(express.static('../frontend/public'));
app.use(express.json());
app.use(cookieParser());
// Setup routes using RoutingManager // Setup routes using RoutingManager
RoutingManager.setupRoutes(app, db); const routingManager = new RoutingManager(app);
routingManager.setup();
// PDF serving route with validation
app.get('/pdf/:bookName/:chapterFile', async (req, res) => {
const { bookName, chapterFile } = req.params;
try {
// Validate PDF using validation module
const validation = await pdfValidator.validatePDF(bookName, chapterFile);
if (!validation.valid) {
console.warn(`PDF validation failed: ${validation.error}`);
return res.status(validation.statusCode).json({
error: validation.error,
bookName,
chapterFile
});
}
// Serve the PDF file using sendFile
res.sendFile(validation.filePath, (sendErr) => {
if (sendErr) {
console.error(`Error serving PDF file: ${sendErr.message}`);
if (!res.headersSent) {
res.status(500).json({ error: 'Error serving PDF file' });
}
}
});
} catch (error) {
console.error('Error in PDF serving route:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// Start server // Start server
app.listen(PORT, '0.0.0.0', () => { app.listen(PORT, '0.0.0.0', () => {
+1 -1
View File
@@ -1,7 +1,7 @@
{{#if chapters}} {{#if chapters}}
{{#each chapters}} {{#each chapters}}
<div class="chapter-item {{#if (eq this.filename ../selectedChapter.filename)}}selected{{/if}}" data-chapter-url="{{this.url}}"> <div class="chapter-item {{#if (eq this.filename ../selectedChapter.filename)}}selected{{/if}}" data-chapter-url="{{this.url}}">
<a href="/book/{{../selectedBook.name}}/chapter/{{encodeURIComponent this.filename}}">{{this.displayName}}</a> <a href="/book/{{../selectedBook.name}}/chapter/{{this.filename}}">{{this.displayName}}</a>
</div> </div>
{{/each}} {{/each}}
{{else}} {{else}}
+1
View File
@@ -162,6 +162,7 @@ body {
border: 1px solid #e9ecef; border: 1px solid #e9ecef;
transition: all 0.2s ease; transition: all 0.2s ease;
font-size: 0.95rem; font-size: 0.95rem;
width: 95%;
} }
.chapter-item a { .chapter-item a {
+1 -2
View File
@@ -1,2 +1 @@
<!-- This content will be injected into the layout template --> <!-- Intentionally left empty to inject dynamic content via Handlebars in layout -->
<!-- The main functionality is handled by the layout template and JavaScript -->