feat: Add Dockerignore files for backend and frontend

This commit is contained in:
Peifan Li
2025-11-24 23:23:45 -05:00
parent f03bcf3adb
commit 89a1451f20
16 changed files with 577 additions and 36 deletions

13
.dockerignore Normal file
View File

@@ -0,0 +1,13 @@
node_modules
dist
.git
.gitignore
.env
docker-compose.yml
README.md
*.log
.DS_Store
backend/node_modules
backend/dist
frontend/node_modules
frontend/dist

3
backend/.dockerignore Normal file
View File

@@ -0,0 +1,3 @@
node_modules
dist
.env

View File

@@ -1,4 +1,4 @@
FROM node:21-alpine
FROM node:22-alpine
WORKDIR /app
@@ -24,4 +24,4 @@ RUN mkdir -p uploads/videos uploads/images data
EXPOSE 5551
CMD ["node", "dist/server.js"]
CMD ["node", "dist/src/server.js"]

View File

@@ -1,9 +0,0 @@
{
"loginEnabled": false,
"defaultAutoPlay": false,
"defaultAutoLoop": false,
"maxConcurrentDownloads": 1,
"isPasswordSet": true,
"language": "en",
"password": "$2b$10$OC6wVmMg4p32ynOddLfALO8AqK/ByA.UjiLBldjCs3l/QhTm.qvvK"
}

View File

@@ -1,7 +0,0 @@
{
"isDownloading": false,
"title": "",
"timestamp": 1763754827104,
"activeDownloads": [],
"queuedDownloads": []
}

View File

@@ -0,0 +1,57 @@
CREATE TABLE `collection_videos` (
`collection_id` text NOT NULL,
`video_id` text NOT NULL,
`order` integer,
PRIMARY KEY(`collection_id`, `video_id`),
FOREIGN KEY (`collection_id`) REFERENCES `collections`(`id`) ON UPDATE no action ON DELETE cascade,
FOREIGN KEY (`video_id`) REFERENCES `videos`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE TABLE `collections` (
`id` text PRIMARY KEY NOT NULL,
`name` text NOT NULL,
`title` text,
`created_at` text NOT NULL,
`updated_at` text
);
--> statement-breakpoint
CREATE TABLE `downloads` (
`id` text PRIMARY KEY NOT NULL,
`title` text NOT NULL,
`timestamp` integer,
`filename` text,
`total_size` text,
`downloaded_size` text,
`progress` integer,
`speed` text,
`status` text DEFAULT 'active' NOT NULL
);
--> statement-breakpoint
CREATE TABLE `settings` (
`key` text PRIMARY KEY NOT NULL,
`value` text NOT NULL
);
--> statement-breakpoint
CREATE TABLE `videos` (
`id` text PRIMARY KEY NOT NULL,
`title` text NOT NULL,
`author` text,
`date` text,
`source` text,
`source_url` text,
`video_filename` text,
`thumbnail_filename` text,
`video_path` text,
`thumbnail_path` text,
`thumbnail_url` text,
`added_at` text,
`created_at` text NOT NULL,
`updated_at` text,
`part_number` integer,
`total_parts` integer,
`series_title` text,
`rating` integer,
`description` text,
`view_count` integer,
`duration` text
);

View File

@@ -0,0 +1,384 @@
{
"version": "6",
"dialect": "sqlite",
"id": "458257d1-ceab-4f29-ac3f-6ac2576d37f5",
"prevId": "00000000-0000-0000-0000-000000000000",
"tables": {
"collection_videos": {
"name": "collection_videos",
"columns": {
"collection_id": {
"name": "collection_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"video_id": {
"name": "video_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"order": {
"name": "order",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"collection_videos_collection_id_collections_id_fk": {
"name": "collection_videos_collection_id_collections_id_fk",
"tableFrom": "collection_videos",
"tableTo": "collections",
"columnsFrom": [
"collection_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"collection_videos_video_id_videos_id_fk": {
"name": "collection_videos_video_id_videos_id_fk",
"tableFrom": "collection_videos",
"tableTo": "videos",
"columnsFrom": [
"video_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"collection_videos_collection_id_video_id_pk": {
"columns": [
"collection_id",
"video_id"
],
"name": "collection_videos_collection_id_video_id_pk"
}
},
"uniqueConstraints": {},
"checkConstraints": {}
},
"collections": {
"name": "collections",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"updated_at": {
"name": "updated_at",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"downloads": {
"name": "downloads",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"timestamp": {
"name": "timestamp",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"filename": {
"name": "filename",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"total_size": {
"name": "total_size",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"downloaded_size": {
"name": "downloaded_size",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"progress": {
"name": "progress",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"speed": {
"name": "speed",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"status": {
"name": "status",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'active'"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"settings": {
"name": "settings",
"columns": {
"key": {
"name": "key",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"value": {
"name": "value",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"videos": {
"name": "videos",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"author": {
"name": "author",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"date": {
"name": "date",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"source": {
"name": "source",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"source_url": {
"name": "source_url",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"video_filename": {
"name": "video_filename",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"thumbnail_filename": {
"name": "thumbnail_filename",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"video_path": {
"name": "video_path",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"thumbnail_path": {
"name": "thumbnail_path",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"thumbnail_url": {
"name": "thumbnail_url",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"added_at": {
"name": "added_at",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"updated_at": {
"name": "updated_at",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"part_number": {
"name": "part_number",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"total_parts": {
"name": "total_parts",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"series_title": {
"name": "series_title",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"rating": {
"name": "rating",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"view_count": {
"name": "view_count",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"duration": {
"name": "duration",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}

View File

@@ -0,0 +1,13 @@
{
"version": "7",
"dialect": "sqlite",
"entries": [
{
"idx": 0,
"version": "6",
"when": 1764043254513,
"tag": "0000_known_guardsmen",
"breakpoints": true
}
]
}

View File

@@ -6,6 +6,7 @@
"start": "ts-node src/server.ts",
"dev": "nodemon src/server.ts",
"build": "tsc",
"generate": "drizzle-kit generate",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],

View File

@@ -1,3 +0,0 @@
import { getSettings } from '../src/services/storageService';
console.log('Imported getSettings:', typeof getSettings);

23
backend/src/db/migrate.ts Normal file
View File

@@ -0,0 +1,23 @@
import { migrate } from 'drizzle-orm/better-sqlite3/migrator';
import path from 'path';
import { ROOT_DIR } from '../config/paths';
import { db } from './index';
export function runMigrations() {
try {
console.log('Running database migrations...');
// In production/docker, the drizzle folder is copied to the root or src/drizzle
// We need to find where it is.
// Based on Dockerfile: COPY . . -> it should be at /app/drizzle
const migrationsFolder = path.join(ROOT_DIR, 'drizzle');
migrate(db, { migrationsFolder });
console.log('Database migrations completed successfully.');
} catch (error) {
console.error('Error running database migrations:', error);
// Don't throw, as we might want the app to start even if migration fails (though it might be broken)
// But for initial setup, it's critical.
throw error;
}
}

View File

@@ -24,6 +24,10 @@ app.use(express.urlencoded({ extended: true }));
// Initialize storage (create directories, etc.)
storageService.initializeStorage();
// Run database migrations
import { runMigrations } from "./db/migrate";
runMigrations();
// Serve static files
app.use("/videos", express.static(VIDEOS_DIR));
app.use("/images", express.static(IMAGES_DIR));

View File

@@ -1,6 +1,6 @@
import fs from 'fs-extra';
import path from 'path';
import { COLLECTIONS_DATA_PATH, STATUS_DATA_PATH, VIDEOS_DATA_PATH } from '../config/paths';
import { COLLECTIONS_DATA_PATH, DATA_DIR, STATUS_DATA_PATH, VIDEOS_DATA_PATH } from '../config/paths';
import { db } from '../db';
import { collections, collectionVideos, downloads, settings, videos } from '../db/schema';
@@ -10,21 +10,42 @@ const SETTINGS_DATA_PATH = path.join(path.dirname(VIDEOS_DATA_PATH), 'settings.j
export async function runMigration() {
console.log('Starting migration...');
const results = {
videos: 0,
collections: 0,
settings: 0,
downloads: 0,
errors: [] as string[]
videos: { count: 0, path: VIDEOS_DATA_PATH, found: false },
collections: { count: 0, path: COLLECTIONS_DATA_PATH, found: false },
settings: { count: 0, path: SETTINGS_DATA_PATH, found: false },
downloads: { count: 0, path: STATUS_DATA_PATH, found: false },
errors: [] as string[],
warnings: [] as string[]
};
// Check for common misconfiguration (nested data directory)
const nestedDataPath = path.join(DATA_DIR, 'data');
if (fs.existsSync(nestedDataPath)) {
results.warnings.push(`Found nested data directory at ${nestedDataPath}. Your volume mount might be incorrect (mounting /data to /app/data instead of /app/data contents).`);
}
// Migrate Videos
if (fs.existsSync(VIDEOS_DATA_PATH)) {
results.videos.found = true;
try {
const videosData = fs.readJSONSync(VIDEOS_DATA_PATH);
console.log(`Found ${videosData.length} videos to migrate.`);
for (const video of videosData) {
try {
// Fix for missing createdAt in legacy data
let createdAt = video.createdAt;
if (!createdAt) {
if (video.addedAt) {
createdAt = video.addedAt;
} else if (video.id && /^\d{13}$/.test(video.id)) {
// If ID is a timestamp (13 digits), use it
createdAt = new Date(parseInt(video.id)).toISOString();
} else {
createdAt = new Date().toISOString();
}
}
await db.insert(videos).values({
id: video.id,
title: video.title,
@@ -38,7 +59,7 @@ export async function runMigration() {
thumbnailPath: video.thumbnailPath,
thumbnailUrl: video.thumbnailUrl,
addedAt: video.addedAt,
createdAt: video.createdAt,
createdAt: createdAt,
updatedAt: video.updatedAt,
partNumber: video.partNumber,
totalParts: video.totalParts,
@@ -48,7 +69,7 @@ export async function runMigration() {
viewCount: video.viewCount,
duration: video.duration,
}).onConflictDoNothing();
results.videos++;
results.videos.count++;
} catch (error: any) {
console.error(`Error migrating video ${video.id}:`, error);
results.errors.push(`Video ${video.id}: ${error.message}`);
@@ -61,6 +82,7 @@ export async function runMigration() {
// Migrate Collections
if (fs.existsSync(COLLECTIONS_DATA_PATH)) {
results.collections.found = true;
try {
const collectionsData = fs.readJSONSync(COLLECTIONS_DATA_PATH);
console.log(`Found ${collectionsData.length} collections to migrate.`);
@@ -75,7 +97,7 @@ export async function runMigration() {
createdAt: collection.createdAt || new Date().toISOString(),
updatedAt: collection.updatedAt,
}).onConflictDoNothing();
results.collections++;
results.collections.count++;
// Insert Collection Videos
if (collection.videos && collection.videos.length > 0) {
@@ -103,6 +125,7 @@ export async function runMigration() {
// Migrate Settings
if (fs.existsSync(SETTINGS_DATA_PATH)) {
results.settings.found = true;
try {
const settingsData = fs.readJSONSync(SETTINGS_DATA_PATH);
console.log('Found settings.json to migrate.');
@@ -115,7 +138,7 @@ export async function runMigration() {
target: settings.key,
set: { value: JSON.stringify(value) },
});
results.settings++;
results.settings.count++;
}
} catch (error: any) {
console.error('Error migrating settings:', error);
@@ -125,6 +148,7 @@ export async function runMigration() {
// Migrate Status (Downloads)
if (fs.existsSync(STATUS_DATA_PATH)) {
results.downloads.found = true;
try {
const statusData = fs.readJSONSync(STATUS_DATA_PATH);
console.log('Found status.json to migrate.');
@@ -155,7 +179,7 @@ export async function runMigration() {
status: 'active',
}
});
results.downloads++;
results.downloads.count++;
}
}
@@ -175,7 +199,7 @@ export async function runMigration() {
status: 'queued',
}
});
results.downloads++;
results.downloads.count++;
}
}
} catch (error: any) {

3
frontend/.dockerignore Normal file
View File

@@ -0,0 +1,3 @@
node_modules
dist
.env

View File

@@ -1,5 +1,5 @@
import { AnimatePresence } from 'framer-motion';
import { Route, Routes, useLocation } from 'react-router-dom';
import { Navigate, Route, Routes, useLocation } from 'react-router-dom';
import AuthorVideos from '../pages/AuthorVideos';
import CollectionPage from '../pages/CollectionPage';
import Home from '../pages/Home';
@@ -152,6 +152,10 @@ const AnimatedRoutes = ({
</PageTransition>
}
/>
{/* Redirect /login to home if already authenticated (or login disabled) */}
<Route path="/login" element={<Navigate to="/" replace />} />
{/* Catch all - redirect to home */}
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</AnimatePresence>
);

View File

@@ -234,8 +234,39 @@ const SettingsPage: React.FC = () => {
setSaving(true);
try {
const res = await axios.post(`${API_URL}/settings/migrate`);
console.log('Migration results:', res.data.results);
setMessage({ text: 'Migration completed successfully!', type: 'success' });
const results = res.data.results;
console.log('Migration results:', results);
let msg = 'Migration Report:\n';
let hasData = false;
if (results.warnings && results.warnings.length > 0) {
msg += `\n⚠ WARNINGS:\n${results.warnings.join('\n')}\n`;
}
const categories = ['videos', 'collections', 'settings', 'downloads'];
categories.forEach(cat => {
const data = results[cat];
if (data) {
if (data.found) {
msg += `\n✅ ${cat}: ${data.count} items migrated`;
hasData = true;
} else {
msg += `\n❌ ${cat}: File not found at ${data.path}`;
}
}
});
if (results.errors && results.errors.length > 0) {
msg += `\n\n⛔ ERRORS:\n${results.errors.join('\n')}`;
}
if (!hasData && (!results.errors || results.errors.length === 0)) {
msg += '\n\n⚠ No data files were found to migrate. Please check your volume mappings.';
}
alert(msg);
setMessage({ text: hasData ? 'Migration completed. See details in alert.' : 'Migration finished but no data found.', type: hasData ? 'success' : 'warning' });
} catch (error: any) {
console.error('Migration failed:', error);
setMessage({