Remove ecosystem configuration file, add Docker entrypoint script, and update deployment workflow to build and push Docker images. Enhance AdminPage with Docker export functionality and improve post management API to use dynamic posts directory path.
This commit is contained in:
@@ -78,6 +78,7 @@ export default function AdminPage() {
|
||||
const [changePwFeedback, setChangePwFeedback] = useState<string | null>(null);
|
||||
const [previewHtml, setPreviewHtml] = useState('');
|
||||
const [editingPost, setEditingPost] = useState<{ slug: string, path: string } | null>(null);
|
||||
const [isDocker, setIsDocker] = useState<boolean>(false);
|
||||
const router = useRouter();
|
||||
const usernameRef = useRef<HTMLInputElement>(null);
|
||||
const passwordRef = useRef<HTMLInputElement>(null);
|
||||
@@ -96,6 +97,7 @@ export default function AdminPage() {
|
||||
useEffect(() => {
|
||||
localStorage.setItem('pinnedPosts', JSON.stringify(pinned));
|
||||
}, [pinned]);
|
||||
|
||||
useEffect(() => {
|
||||
marked.setOptions({
|
||||
gfm: true,
|
||||
@@ -111,6 +113,14 @@ export default function AdminPage() {
|
||||
setPreviewHtml(marked.parse(newPost.content || '') as string);
|
||||
}, [newPost.content]);
|
||||
|
||||
useEffect(() => {
|
||||
// Check if docker is used
|
||||
fetch('/api/admin/docker')
|
||||
.then(res => res.json())
|
||||
.then(data => setIsDocker(!!data.docker))
|
||||
.catch(() => setIsDocker(false));
|
||||
}, []);
|
||||
|
||||
const loadContent = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/posts');
|
||||
@@ -480,6 +490,25 @@ export default function AdminPage() {
|
||||
}
|
||||
};
|
||||
|
||||
function handleExportTarball() {
|
||||
fetch('/api/admin/export')
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw new Error('Export failed');
|
||||
const blob = await res.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'markdownblog-export.tar.gz';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
window.URL.revokeObjectURL(url);
|
||||
})
|
||||
.catch((err) => {
|
||||
alert('Export failed: ' + err.message);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-100 p-8">
|
||||
{pinFeedback && (
|
||||
@@ -548,6 +577,19 @@ export default function AdminPage() {
|
||||
>
|
||||
Passwort ändern
|
||||
</button>
|
||||
{/* Docker warning above export button */}
|
||||
{isDocker && (
|
||||
<div className="mb-2 px-4 py-2 bg-yellow-200 text-yellow-900 rounded border border-yellow-400 font-semibold text-sm text-center">
|
||||
<span className="font-bold">Warning:</span> Docker is in use. Exporting will export the entire <span className="font-mono">/app</span> root directory (including all files and folders in the container's root).
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
onClick={handleExportTarball}
|
||||
className="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700"
|
||||
title="Export the entire root folder as a tarball"
|
||||
>
|
||||
Export Root as Tarball
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import { getPostsDirectory } from '@/lib/postsDirectory';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
@@ -16,7 +17,7 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
// Construct the full path to the item
|
||||
const basePath = process.cwd();
|
||||
const postsDir = path.join(basePath, 'posts');
|
||||
const postsDir = getPostsDirectory();
|
||||
|
||||
// Ensure the posts directory exists
|
||||
try {
|
||||
|
||||
10
src/app/api/admin/docker/route.ts
Normal file
10
src/app/api/admin/docker/route.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { existsSync } from 'fs';
|
||||
import path from 'path';
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
export async function GET() {
|
||||
const rootDir = process.cwd();
|
||||
const dockerDir = path.join(rootDir, 'docker');
|
||||
const isDocker = existsSync(dockerDir);
|
||||
return NextResponse.json({ docker: isDocker });
|
||||
}
|
||||
69
src/app/api/admin/export/route.ts
Normal file
69
src/app/api/admin/export/route.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import tar from 'tar';
|
||||
import { NextResponse } from 'next/server';
|
||||
import { statSync, createReadStream, existsSync } from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const rootDir = process.cwd();
|
||||
const dockerDir = path.join(rootDir, 'docker');
|
||||
const postsDir = path.join(rootDir, 'posts');
|
||||
let tarballName: string;
|
||||
let tarballPath: string;
|
||||
let tarCwd: string;
|
||||
let tarItems: string[];
|
||||
let tarOptions: any = {
|
||||
gzip: true,
|
||||
portable: true,
|
||||
noMtime: true,
|
||||
};
|
||||
|
||||
if (existsSync(dockerDir)) {
|
||||
// Docker is in use: export the entire root directory (excluding node_modules, .next, etc)
|
||||
tarballName = 'root-export.tar.gz';
|
||||
tarballPath = path.join('/tmp', tarballName);
|
||||
tarCwd = rootDir;
|
||||
tarItems = ['.'];
|
||||
tarOptions.file = tarballPath;
|
||||
tarOptions.cwd = tarCwd;
|
||||
tarOptions.filter = (filepath: string) => {
|
||||
// Exclude node_modules, .next, .git, /tmp, and tarball itself
|
||||
const excludes = [
|
||||
'node_modules', '.next', '.git', 'tmp', 'docker.sock', tarballName
|
||||
];
|
||||
// Only check top-level folders/files
|
||||
const rel = filepath.split(path.sep)[0];
|
||||
return !excludes.includes(rel);
|
||||
};
|
||||
} else {
|
||||
// Not docker: export only the posts directory
|
||||
tarballName = 'posts-export.tar.gz';
|
||||
tarballPath = path.join('/tmp', tarballName);
|
||||
tarCwd = rootDir;
|
||||
tarItems = ['posts'];
|
||||
tarOptions.file = tarballPath;
|
||||
tarOptions.cwd = tarCwd;
|
||||
}
|
||||
|
||||
// Create tarball
|
||||
await tar.c(
|
||||
tarOptions,
|
||||
tarItems
|
||||
);
|
||||
|
||||
// Stream the tarball as a response
|
||||
const stat = statSync(tarballPath);
|
||||
const stream = createReadStream(tarballPath);
|
||||
return new Response(stream as any, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/gzip',
|
||||
'Content-Disposition': `attachment; filename="${tarballName}"`,
|
||||
'Content-Length': stat.size.toString(),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error exporting tarball:', error);
|
||||
return NextResponse.json({ error: 'Error exporting tarball' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { getPostsDirectory } from '@/lib/postsDirectory';
|
||||
|
||||
const postsDirectory = path.join(process.cwd(), 'posts');
|
||||
const postsDirectory = getPostsDirectory();
|
||||
|
||||
function getFolderStats(folderPath: string) {
|
||||
const fullPath = path.join(postsDirectory, folderPath);
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { getPostsDirectory } from '@/lib/postsDirectory';
|
||||
|
||||
const postsDirectory = path.join(process.cwd(), 'posts');
|
||||
const postsDirectory = getPostsDirectory();
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { getPostsDirectory } from '@/lib/postsDirectory';
|
||||
|
||||
const postsDirectory = path.join(process.cwd(), 'posts');
|
||||
const postsDirectory = getPostsDirectory();
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { getPostsDirectory } from '@/lib/postsDirectory';
|
||||
|
||||
const postsDirectory = path.join(process.cwd(), 'posts');
|
||||
const postsDirectory = getPostsDirectory();
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
|
||||
@@ -2,8 +2,9 @@ import { NextResponse } from 'next/server';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import matter from 'gray-matter';
|
||||
import { getPostsDirectory } from '@/lib/postsDirectory';
|
||||
|
||||
const postsDirectory = path.join(process.cwd(), 'posts');
|
||||
const postsDirectory = getPostsDirectory();
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { getPostsDirectory } from '@/lib/postsDirectory';
|
||||
|
||||
const postsDirectory = path.join(process.cwd(), 'posts');
|
||||
const postsDirectory = getPostsDirectory();
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
|
||||
@@ -2,8 +2,9 @@ import { NextResponse } from 'next/server';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import matter from 'gray-matter';
|
||||
import { getPostsDirectory } from '@/lib/postsDirectory';
|
||||
|
||||
const postsDirectory = path.join(process.cwd(), 'posts');
|
||||
const postsDirectory = getPostsDirectory();
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
|
||||
@@ -8,8 +8,9 @@ import { marked } from 'marked';
|
||||
import DOMPurify from 'dompurify';
|
||||
import { JSDOM } from 'jsdom';
|
||||
import hljs from 'highlight.js';
|
||||
import { getPostsDirectory } from '@/lib/postsDirectory';
|
||||
|
||||
const postsDirectory = path.join(process.cwd(), 'posts');
|
||||
const postsDirectory = getPostsDirectory();
|
||||
|
||||
const renderer = new marked.Renderer();
|
||||
renderer.code = (code, infostring, escaped) => {
|
||||
|
||||
@@ -8,8 +8,9 @@ import { marked } from 'marked';
|
||||
import DOMPurify from 'dompurify';
|
||||
import { JSDOM } from 'jsdom';
|
||||
import hljs from 'highlight.js';
|
||||
import { getPostsDirectory } from '@/lib/postsDirectory';
|
||||
|
||||
const postsDirectory = path.join(process.cwd(), 'posts');
|
||||
const postsDirectory = getPostsDirectory();
|
||||
|
||||
const pinnedPath = path.join(postsDirectory, 'pinned.json');
|
||||
let pinnedSlugs: string[] = [];
|
||||
|
||||
@@ -7,6 +7,7 @@ import { JSDOM } from 'jsdom';
|
||||
import chokidar from 'chokidar';
|
||||
import type { FSWatcher } from 'chokidar';
|
||||
import hljs from 'highlight.js';
|
||||
import { getPostsDirectory } from './postsDirectory';
|
||||
|
||||
export interface Post {
|
||||
slug: string;
|
||||
@@ -19,7 +20,7 @@ export interface Post {
|
||||
author: string;
|
||||
}
|
||||
|
||||
const postsDirectory = path.join(process.cwd(), 'posts');
|
||||
const postsDirectory = getPostsDirectory();
|
||||
|
||||
// Function to get file creation date
|
||||
function getFileCreationDate(filePath: string): Date {
|
||||
|
||||
9
src/lib/postsDirectory.ts
Normal file
9
src/lib/postsDirectory.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import path from 'path';
|
||||
import { existsSync } from 'fs';
|
||||
|
||||
export function getPostsDirectory() {
|
||||
const rootDir = process.cwd();
|
||||
const dockerDir = path.join(rootDir, 'docker');
|
||||
const postsDir = path.join(rootDir, 'posts');
|
||||
return existsSync(dockerDir) ? dockerDir : postsDir;
|
||||
}
|
||||
Reference in New Issue
Block a user