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
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
This project implements a PDF document management system with the following features:
- **Frontend**: Nginx serving static files and Handlebars templates
- **Backend**: Node.js/Express server with custom routing and PDF management
- **PDF Management**: PDF validation and secure serving system
- **Security**: Path validation and access control for PDF serving
- **Database Integration**: SQLite database for book and chapter metadata
- **Backend**: Node.js/Express server with custom routing
- **JSON Metadata**: Book and chapter metadata stored as JSON files
- **Containerization**: Docker containers orchestrated with Docker Compose
## Features
### PDF Document Management
- **PDF Validation Module**: Comprehensive validation before serving any PDF
- **Custom Routing**: Dedicated routing module for book and chapter navigation
- **Security Controls**: Path validation and access restrictions
- **Database Integration**: SQLite database with books and chapters metadata
### 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)
- **JSON Metadata**: Book and chapter information stored in JSON format
- **File Serving**: Direct access to PDF files through designated routes
### User Interface
- **Book Browser**: Navigate through available books and chapters
@@ -52,14 +41,13 @@ COS498-HW3/
│ ├── Dockerfile # Backend container configuration
│ ├── package.json # Node.js dependencies and scripts
│ ├── package-lock.json # Locked dependency versions
│ ├── server.js # Node.js Express server with PDF management
│ ├── server.js # Node.js Express server
│ ├── database/
│ │ ── backend.db # SQLite database with book metadata
│ │ ├── backend.schema # Database schema definition
│ │ └── backend_initial.db # Initial database backup
│ │ ── backend.schema # Database schema definition
│ └── modules/
│ ├── RoutingManager.js # Custom routing module
── PDFValidationManager.js # PDF validation and security
── PDFValidationManager.js # PDF validation module
│ └── PDFDatabaseManager.js # Database management module
└── frontend/
├── Dockerfile # Frontend container configuration
├── default.conf # Nginx configuration
@@ -74,7 +62,9 @@ COS498-HW3/
│ └── WaysOfTheWorld-Strayer/ # Sample textbook (git submodule)
│ ├── README.md # Book information
│ ├── .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/
└── 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,
PRIMARY KEY("id" AUTOINCREMENT),
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 path = require('path');
/**
* PDF Validation Module - Validates PDF existence and access permissions
* Ensures only authorized access to PDFs within designated folders
*/
// Configuration Constants
const BOOK_PATH = "../frontend/public/books";
// PDF Validation Module - Validates PDF existence and access permissions
// Ensures only authorized access to PDFs within designated folders
class PDFValidationManager {
constructor(baseDir = '/app/frontend/public/books') {
this.baseDir = path.resolve(baseDir);
}
/**
* Validate if a requested PDF document exists and is accessible
*/
async validatePDF(bookName, filename) {
// Validate if a requested PDF document exists and is accessible
static async validatePDF(bookName, filename) {
try {
// Construct file path
const filePath = path.resolve(this.baseDir, bookName, filename)
// Check if file exists and get file stats
const fileStats = await fs.stat(filePath);
if (!fileStats.isFile()) {
return {
valid: false,
error: 'File not found or is not a regular file',
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
};
const filePath = path.resolve(BOOK_PATH, bookName, filename)
const file = await fs.stat(filePath);
if (file.isFile()) {
return filePath;
} else {
throw new Error('Invalid PDF request');
}
} catch {
throw new Error('Invalid PDF request');
}
}
}
+147 -141
View File
@@ -1,154 +1,160 @@
// Helper functions for server-side rendering
function loadBooks(db) {
return new Promise((resolve, reject) => {
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);
}
});
});
}
// Imports
const hbs = require('hbs');
const express = require('express');
function loadChapters(db, 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
`;
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);
}
});
});
}
// Local Module Imports
const PDFValidationManager = require('./PDFValidationManager');
const PDFDatabaseManager = require('./PDFDatabaseManager');
// Page Route handlers
async function homePage(db, req, res) {
try {
const books = await loadBooks(db);
res.render('index', {
title: 'Book Viewer',
books: books,
selectedBook: null,
selectedChapter: null,
chapters: []
});
} catch (error) {
console.error('Error loading books for home page:', error);
res.render('index', {
title: 'Book Viewer',
books: [],
selectedBook: null,
selectedChapter: null,
chapters: [],
error: 'Failed to load books'
class RoutingManager {
constructor(app) {
this.databaseManager = new PDFDatabaseManager();
this.db = this.databaseManager.db;
this.app = app;
}
// Setup Routing Manager
setup() {
this.setupHandlebars();
this.middleware();
this.setupRoutes();
}
// Setup Handlebars
setupHandlebars() {
// Handlebars - Views
this.app.set('view engine', 'hbs');
this.app.set('views', '../frontend/views');
// Handlebars - Partials
hbs.registerPartials('../frontend/partials');
// Handlebars Helpers
hbs.registerHelper('eq', function(a, b) {
return a === b;
});
}
}
// Render page with specific book
async function bookPage(db, req, res) {
const bookName = req.params.bookName;
try {
const [books, chapters] = await Promise.all([
loadBooks(db),
loadChapters(db, bookName)
]);
const selectedBook = books.find(book => book.name === bookName);
if (!selectedBook) {
return res.redirect('/');
// Middleware for Application
middleware() {
this.app.use(express.static('../frontend/public'));
this.app.use(express.json());
// this.app.use(cookieParser());
}
// Setup Page Routes
setupRoutes() {
this.app.get('/', async (req, res) => {
await this.homePage(req, res);
});
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
async function chapterPage(db, req, res) {
const bookName = req.params.bookName;
const chapterFile = decodeURIComponent(req.params.chapterFile);
try {
const [books, chapters] = await Promise.all([
loadBooks(db),
loadChapters(db, bookName)
]);
const selectedBook = books.find(book => book.name === bookName);
const selectedChapter = chapters.find(chapter => chapter.filename === chapterFile);
if (!selectedBook || !selectedChapter) {
return res.redirect('/');
// Book Page Handler
async bookPage(req, res) {
const bookName = req.params.bookName;
try {
const [books, chapters] = await Promise.all([
this.databaseManager.loadBooks(),
this.databaseManager.loadChapters(bookName)
]);
const selectedBook = books.find(book => book.name === bookName);
if (!selectedBook) {
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', {
title: 'Book Viewer',
books: books,
selectedBook: selectedBook,
selectedChapter: selectedChapter,
chapters: chapters
});
} catch (error) {
console.error('Error loading chapter page:', error);
res.redirect('/');
try {
const [books, chapters] = await Promise.all([
this.databaseManager.loadBooks(),
this.databaseManager.loadChapters(bookName)
]);
const selectedBook = books.find(book => book.name === bookName);
const selectedChapter = chapters.find(chapter => chapter.filename === chapterFile);
if (!selectedBook || !selectedChapter) {
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
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
};
module.exports = RoutingManager;
+5 -75
View File
@@ -1,85 +1,15 @@
// Imports
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 PORT = process.env.PORT || 3000;
// Handlebars
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
// Local Module Imports
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
RoutingManager.setupRoutes(app, db);
// 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' });
}
});
const routingManager = new RoutingManager(app);
routingManager.setup();
// Start server
app.listen(PORT, '0.0.0.0', () => {
+1 -1
View File
@@ -1,7 +1,7 @@
{{#if chapters}}
{{#each chapters}}
<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>
{{/each}}
{{else}}
+1
View File
@@ -162,6 +162,7 @@ body {
border: 1px solid #e9ecef;
transition: all 0.2s ease;
font-size: 0.95rem;
width: 95%;
}
.chapter-item a {
+1 -2
View File
@@ -1,2 +1 @@
<!-- This content will be injected into the layout template -->
<!-- The main functionality is handled by the layout template and JavaScript -->
<!-- Intentionally left empty to inject dynamic content via Handlebars in layout -->