Initial commit of YouTube Downloader application, including core functionality for downloading videos, user authentication, and Docker support. Added configuration files, environment setup, and basic UI components using Astro.js and Tailwind CSS.
This commit is contained in:
58
src/pages/api/download-file.ts
Normal file
58
src/pages/api/download-file.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import type { APIRoute } from "astro";
|
||||
import { getSession, isLoginEnabled } from "../../lib/session";
|
||||
import { tApi } from "../../lib/i18n";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { existsSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
export const GET: APIRoute = async ({ request }) => {
|
||||
// Session prüfen (nur wenn Login aktiviert ist)
|
||||
const loginEnabled = isLoginEnabled();
|
||||
if (loginEnabled) {
|
||||
const session = await getSession(request);
|
||||
if (!session) {
|
||||
return new Response(tApi(request, "api.notAuthenticated"), { status: 401 });
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const url = new URL(request.url);
|
||||
const fileName = url.searchParams.get("file");
|
||||
|
||||
if (!fileName) {
|
||||
return new Response(tApi(request, "api.filenameMissing"), { status: 400 });
|
||||
}
|
||||
|
||||
// Download-Verzeichnis aus Environment-Variable
|
||||
const downloadDir =
|
||||
import.meta.env.DOWNLOAD_DIR || path.join(process.cwd(), "downloaded");
|
||||
|
||||
const filePath = path.join(downloadDir, fileName);
|
||||
|
||||
// Sicherheitsprüfung: Verhindere Path Traversal
|
||||
if (!filePath.startsWith(downloadDir)) {
|
||||
return new Response(tApi(request, "api.invalidFilePath"), { status: 400 });
|
||||
}
|
||||
|
||||
if (!existsSync(filePath)) {
|
||||
return new Response(tApi(request, "api.fileNotFound"), { status: 404 });
|
||||
}
|
||||
|
||||
const fileContent = await readFile(filePath);
|
||||
|
||||
return new Response(fileContent, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/octet-stream",
|
||||
"Content-Disposition": `attachment; filename="${fileName}"`,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Fehler beim Download der Datei:", error);
|
||||
return new Response(
|
||||
error instanceof Error ? error.message : tApi(request, "api.errorDownloadingFile"),
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
227
src/pages/api/download.ts
Normal file
227
src/pages/api/download.ts
Normal file
@@ -0,0 +1,227 @@
|
||||
import type { APIRoute } from "astro";
|
||||
import { getSession, isLoginEnabled } from "../../lib/session";
|
||||
import { tApi } from "../../lib/i18n";
|
||||
import { mkdir, unlink, readFile } from "node:fs/promises";
|
||||
import { existsSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { createRequire } from "module";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const YTDlpWrap = require("yt-dlp-wrap").default || require("yt-dlp-wrap");
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
// Session prüfen (nur wenn Login aktiviert ist)
|
||||
const loginEnabled = isLoginEnabled();
|
||||
if (loginEnabled) {
|
||||
const session = await getSession(request);
|
||||
if (!session) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: tApi(request, "api.notAuthenticated") }),
|
||||
{
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const { url } = await request.json();
|
||||
|
||||
if (!url || typeof url !== "string") {
|
||||
return new Response(
|
||||
JSON.stringify({ error: tApi(request, "api.invalidUrl") }),
|
||||
{
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// YouTube URL validieren
|
||||
if (!url.includes("youtube.com") && !url.includes("youtu.be")) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: tApi(request, "api.invalidYouTubeUrl") }),
|
||||
{ status: 400, headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
}
|
||||
|
||||
// Prüfe ob Stream-Modus aktiviert ist
|
||||
const streamOnly = import.meta.env.STREAM_ONLY === "true";
|
||||
|
||||
// yt-dlp-wrap Instanz erstellen
|
||||
const ytDlpWrap = new YTDlpWrap();
|
||||
|
||||
// Format aus Request Header (optional, Standard: MP4)
|
||||
const format = request.headers.get("x-format") || "mp4";
|
||||
|
||||
// Zuerst Video-Informationen abrufen, um den Dateinamen zu erhalten
|
||||
const videoInfo = await ytDlpWrap.getVideoInfo(url);
|
||||
const title = videoInfo.title || "Video";
|
||||
const ext = format === "mp4" ? "mp4" : videoInfo.ext || "mp4";
|
||||
const filename = `${title}.${ext}`;
|
||||
|
||||
// Format-String für yt-dlp
|
||||
const formatString =
|
||||
format === "mp4"
|
||||
? "bestvideo[ext=mp4]+bestaudio[ext=m4a]/bestvideo[ext=mp4]+bestaudio/best[ext=mp4]/best"
|
||||
: "best";
|
||||
|
||||
if (streamOnly) {
|
||||
// STREAM-MODUS: Datei temporär speichern, streamen, dann löschen
|
||||
const tempDir = tmpdir();
|
||||
const tempFilePath = path.join(tempDir, `${Date.now()}-${filename}`);
|
||||
|
||||
try {
|
||||
// Download in temporäres Verzeichnis
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const execArgs = [
|
||||
url,
|
||||
"-o",
|
||||
tempFilePath,
|
||||
"-f",
|
||||
formatString,
|
||||
"--no-mtime",
|
||||
"--no-playlist",
|
||||
];
|
||||
|
||||
if (format === "mp4") {
|
||||
execArgs.push("--merge-output-format", "mp4");
|
||||
}
|
||||
|
||||
ytDlpWrap
|
||||
.exec(execArgs)
|
||||
.on("error", (error: Error) => {
|
||||
reject(error);
|
||||
})
|
||||
.on("close", (code: number) => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`yt-dlp exited with code ${code}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Datei als Stream zurückgeben
|
||||
const fileBuffer = await readFile(tempFilePath);
|
||||
|
||||
// Temporäre Datei löschen
|
||||
await unlink(tempFilePath).catch((err) => {
|
||||
console.error("Fehler beim Löschen der temporären Datei:", err);
|
||||
});
|
||||
|
||||
// Stream als Response zurückgeben
|
||||
return new Response(fileBuffer, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type":
|
||||
format === "mp4" ? "video/mp4" : "application/octet-stream",
|
||||
"Content-Disposition": `attachment; filename="${encodeURIComponent(
|
||||
filename
|
||||
)}"`,
|
||||
"Content-Length": fileBuffer.length.toString(),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
// Aufräumen bei Fehler
|
||||
if (existsSync(tempFilePath)) {
|
||||
await unlink(tempFilePath).catch(() => {});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
// NORMALER MODUS: Datei speichern wie bisher
|
||||
const downloadDir =
|
||||
import.meta.env.DOWNLOAD_DIR || path.join(process.cwd(), "downloaded");
|
||||
|
||||
// Verzeichnis erstellen falls nicht vorhanden
|
||||
try {
|
||||
if (!existsSync(downloadDir)) {
|
||||
await mkdir(downloadDir, { recursive: true });
|
||||
}
|
||||
} catch (mkdirError) {
|
||||
console.error(
|
||||
"Fehler beim Erstellen des Download-Verzeichnisses:",
|
||||
mkdirError
|
||||
);
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: tApi(request, "api.couldNotCreateDirectory", {
|
||||
dir: downloadDir,
|
||||
}),
|
||||
}),
|
||||
{ status: 500, headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
}
|
||||
|
||||
const outputPath = path.join(downloadDir, "%(title)s.%(ext)s");
|
||||
|
||||
// Download durchführen mit yt-dlp-wrap
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const execArgs = [
|
||||
url,
|
||||
"-o",
|
||||
outputPath,
|
||||
"-f",
|
||||
formatString,
|
||||
"--no-mtime",
|
||||
"--no-playlist",
|
||||
];
|
||||
|
||||
if (format === "mp4") {
|
||||
execArgs.push("--merge-output-format", "mp4");
|
||||
}
|
||||
|
||||
ytDlpWrap
|
||||
.exec(execArgs)
|
||||
.on("progress", (progress: any) => {
|
||||
// Progress-Events können hier verarbeitet werden
|
||||
})
|
||||
.on("ytDlpEvent", (eventType: string, eventData: any) => {
|
||||
if (eventType === "download") {
|
||||
// Download-Event verarbeiten
|
||||
}
|
||||
})
|
||||
.on("error", (error: Error) => {
|
||||
reject(error);
|
||||
})
|
||||
.on("close", (code: number) => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`yt-dlp exited with code ${code}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Dateiname aus dem tatsächlichen Output-Pfad extrahieren
|
||||
const actualFilename = path.join(downloadDir, `${title}.${ext}`);
|
||||
let finalFilename = filename;
|
||||
if (existsSync(actualFilename)) {
|
||||
finalFilename = path.basename(actualFilename);
|
||||
}
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: true,
|
||||
message: tApi(request, "api.downloadSuccessful"),
|
||||
filename: finalFilename,
|
||||
}),
|
||||
{ status: 200, headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Download-Fehler:", error);
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: tApi(request, "api.downloadFailed"),
|
||||
}),
|
||||
{ status: 500, headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
}
|
||||
};
|
||||
83
src/pages/api/files.ts
Normal file
83
src/pages/api/files.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import type { APIRoute } from "astro";
|
||||
import { getSession, isLoginEnabled } from "../../lib/session";
|
||||
import { tApi } from "../../lib/i18n";
|
||||
import { readdir, stat } from "node:fs/promises";
|
||||
import { existsSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
export const GET: APIRoute = async ({ request }) => {
|
||||
// Session prüfen (nur wenn Login aktiviert ist)
|
||||
const loginEnabled = isLoginEnabled();
|
||||
if (loginEnabled) {
|
||||
const session = await getSession(request);
|
||||
if (!session) {
|
||||
return new Response(JSON.stringify({ error: tApi(request, "api.notAuthenticated") }), {
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Prüfe ob Stream-Modus aktiviert ist
|
||||
const streamOnly = import.meta.env.STREAM_ONLY === "true";
|
||||
if (streamOnly) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: tApi(request, "api.fileListNotAvailable"),
|
||||
files: []
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
// Download-Verzeichnis aus Environment-Variable
|
||||
const downloadDir =
|
||||
import.meta.env.DOWNLOAD_DIR || path.join(process.cwd(), "downloaded");
|
||||
|
||||
if (!existsSync(downloadDir)) {
|
||||
return new Response(JSON.stringify({ files: [] }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
// Alle Dateien im Verzeichnis lesen
|
||||
const files = await readdir(downloadDir);
|
||||
const fileList = await Promise.all(
|
||||
files.map(async (filename) => {
|
||||
const filePath = path.join(downloadDir, filename);
|
||||
const stats = await stat(filePath);
|
||||
|
||||
return {
|
||||
name: filename,
|
||||
size: stats.size,
|
||||
createdAt: stats.birthtime.toISOString(),
|
||||
modifiedAt: stats.mtime.toISOString(),
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
// Nach Erstellungsdatum sortieren (neueste zuerst)
|
||||
fileList.sort((a, b) =>
|
||||
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
||||
);
|
||||
|
||||
return new Response(JSON.stringify({ files: fileList }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Fehler beim Lesen der Dateien:", error);
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: error instanceof Error ? error.message : tApi(request, "api.errorReadingFiles"),
|
||||
}),
|
||||
{ status: 500, headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
57
src/pages/api/login.ts
Normal file
57
src/pages/api/login.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import { createSessionCookie, isLoginEnabled } from '../../lib/session';
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
// Wenn Login deaktiviert ist, weiterleiten
|
||||
if (!isLoginEnabled()) {
|
||||
return new Response(null, {
|
||||
status: 302,
|
||||
headers: {
|
||||
'Location': new URL('/download', request.url).toString(),
|
||||
},
|
||||
});
|
||||
}
|
||||
const formData = await request.formData();
|
||||
const username = formData.get('username')?.toString();
|
||||
const password = formData.get('password')?.toString();
|
||||
|
||||
// Credentials aus Environment-Variablen
|
||||
const envUsername = import.meta.env.LOGIN_USERNAME;
|
||||
const envPassword = import.meta.env.LOGIN_PASSWORD;
|
||||
|
||||
// Prüfe ob Credentials konfiguriert sind
|
||||
if (!envUsername || !envPassword) {
|
||||
console.error('LOGIN_USERNAME oder LOGIN_PASSWORD nicht in Environment-Variablen gesetzt!');
|
||||
return new Response(null, {
|
||||
status: 302,
|
||||
headers: {
|
||||
'Location': new URL('/', request.url).toString(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Authentifizierung gegen Environment-Variablen
|
||||
if (username === envUsername && password === envPassword) {
|
||||
const session = {
|
||||
username,
|
||||
loggedIn: true,
|
||||
};
|
||||
|
||||
return new Response(null, {
|
||||
status: 302,
|
||||
headers: {
|
||||
'Location': new URL('/download', request.url).toString(),
|
||||
'Set-Cookie': createSessionCookie(session),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Falsche Credentials
|
||||
return new Response(null, {
|
||||
status: 302,
|
||||
headers: {
|
||||
'Location': new URL('/', request.url).toString(),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
13
src/pages/api/logout.ts
Normal file
13
src/pages/api/logout.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import { clearSessionCookie } from '../../lib/session';
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
return new Response(null, {
|
||||
status: 302,
|
||||
headers: {
|
||||
'Location': new URL('/', request.url).toString(),
|
||||
'Set-Cookie': clearSessionCookie(),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
18
src/pages/download.astro
Normal file
18
src/pages/download.astro
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
import Layout from '../layouts/Layout.astro';
|
||||
import DownloadForm from '../components/DownloadForm.astro';
|
||||
import { getSession, isLoginEnabled } from '../lib/session';
|
||||
|
||||
const loginEnabled = isLoginEnabled();
|
||||
const session = await getSession(Astro.request);
|
||||
|
||||
// Nur Session-Prüfung wenn Login aktiviert ist
|
||||
if (loginEnabled && !session) {
|
||||
return Astro.redirect('/');
|
||||
}
|
||||
---
|
||||
|
||||
<Layout title="YouTube Downloader">
|
||||
<DownloadForm />
|
||||
</Layout>
|
||||
|
||||
218
src/pages/files.astro
Normal file
218
src/pages/files.astro
Normal file
@@ -0,0 +1,218 @@
|
||||
---
|
||||
import Layout from '../layouts/Layout.astro';
|
||||
import { getSession, isLoginEnabled } from '../lib/session';
|
||||
import { t } from '../lib/i18n';
|
||||
|
||||
const loginEnabled = isLoginEnabled();
|
||||
const session = await getSession(Astro.request);
|
||||
|
||||
// Nur Session-Prüfung wenn Login aktiviert ist
|
||||
if (loginEnabled && !session) {
|
||||
return Astro.redirect('/');
|
||||
}
|
||||
|
||||
const streamOnly = import.meta.env.STREAM_ONLY === "true";
|
||||
---
|
||||
|
||||
<Layout title={t(Astro, "files.title")}>
|
||||
<div class="min-h-screen bg-base-200 p-4">
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h1 class="card-title text-2xl mb-4">{t(Astro, "files.title")}</h1>
|
||||
|
||||
{streamOnly ? (
|
||||
<div class="alert alert-info">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<div>
|
||||
<h3 class="font-bold">{t(Astro, "files.streamModeEnabled")}</h3>
|
||||
<div class="text-sm">{t(Astro, "files.streamModeDescription")}</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-zebra">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<button class="btn btn-ghost btn-xs" onclick="sortTable('name')">
|
||||
{t(Astro, "common.name")}
|
||||
<span id="sort-name-icon">↕</span>
|
||||
</button>
|
||||
</th>
|
||||
<th>
|
||||
<button class="btn btn-ghost btn-xs" onclick="sortTable('size')">
|
||||
{t(Astro, "common.size")}
|
||||
<span id="sort-size-icon">↕</span>
|
||||
</button>
|
||||
</th>
|
||||
<th>
|
||||
<button class="btn btn-ghost btn-xs" onclick="sortTable('createdAt')">
|
||||
{t(Astro, "common.created")}
|
||||
<span id="sort-createdAt-icon">↕</span>
|
||||
</button>
|
||||
</th>
|
||||
<th>{t(Astro, "common.actions")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="filesTableBody">
|
||||
<tr>
|
||||
<td colspan="4" class="text-center">
|
||||
<span class="loading loading-spinner loading-lg"></span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
<script>
|
||||
let files: Array<{name: string; size: number; createdAt: string; modifiedAt: string}> = [];
|
||||
let sortColumn: string | null = null;
|
||||
let sortDirection: 'asc' | 'desc' = 'desc';
|
||||
|
||||
function formatFileSize(bytes: number): string {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
function formatDate(dateString: string): string {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleString('en-US', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
}
|
||||
|
||||
function sortTable(column: string) {
|
||||
if (sortColumn === column) {
|
||||
sortDirection = sortDirection === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
sortColumn = column;
|
||||
sortDirection = 'asc';
|
||||
}
|
||||
|
||||
files.sort((a, b) => {
|
||||
let aVal: any = a[column as keyof typeof a];
|
||||
let bVal: any = b[column as keyof typeof b];
|
||||
|
||||
if (column === 'size') {
|
||||
aVal = Number(aVal);
|
||||
bVal = Number(bVal);
|
||||
} else if (column === 'createdAt' || column === 'modifiedAt') {
|
||||
aVal = new Date(aVal).getTime();
|
||||
bVal = new Date(bVal).getTime();
|
||||
} else {
|
||||
aVal = String(aVal).toLowerCase();
|
||||
bVal = String(bVal).toLowerCase();
|
||||
}
|
||||
|
||||
if (aVal < bVal) return sortDirection === 'asc' ? -1 : 1;
|
||||
if (aVal > bVal) return sortDirection === 'asc' ? 1 : -1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
updateSortIcons();
|
||||
renderTable();
|
||||
}
|
||||
|
||||
function updateSortIcons() {
|
||||
// Reset all icons
|
||||
document.querySelectorAll('[id^="sort-"]').forEach(icon => {
|
||||
(icon as HTMLElement).textContent = '↕';
|
||||
});
|
||||
|
||||
if (sortColumn) {
|
||||
const icon = document.getElementById(`sort-${sortColumn}-icon`);
|
||||
if (icon) {
|
||||
icon.textContent = sortDirection === 'asc' ? '↑' : '↓';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renderTable() {
|
||||
const tbody = document.getElementById('filesTableBody');
|
||||
if (!tbody) return;
|
||||
|
||||
if (files.length === 0) {
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="4" class="text-center">No files found</td>
|
||||
</tr>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = files.map(file => `
|
||||
<tr>
|
||||
<td>${file.name}</td>
|
||||
<td>${formatFileSize(file.size)}</td>
|
||||
<td>${formatDate(file.createdAt)}</td>
|
||||
<td>
|
||||
<a href="/api/download-file?file=${encodeURIComponent(file.name)}" class="btn btn-sm btn-primary" download>
|
||||
Download
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
async function loadFiles() {
|
||||
try {
|
||||
const response = await fetch('/api/files');
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
files = data.files || [];
|
||||
if (files.length > 0) {
|
||||
sortTable('createdAt'); // Standard: nach Erstellungsdatum sortieren
|
||||
} else {
|
||||
renderTable();
|
||||
}
|
||||
} else {
|
||||
console.error('Error loading files:', data.error);
|
||||
const tbody = document.getElementById('filesTableBody');
|
||||
if (tbody) {
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="4" class="text-center text-error" data-i18n="files.errorLoadingFiles">Error loading files</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
const tbody = document.getElementById('filesTableBody');
|
||||
if (tbody) {
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="4" class="text-center text-error" data-i18n="downloadForm.networkError">Network error</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Globale Funktion für onclick
|
||||
(window as any).sortTable = sortTable;
|
||||
|
||||
// Dateien beim Laden der Seite abrufen (nur wenn nicht im Stream-Modus)
|
||||
const streamOnly = false; // Wird server-seitig geprüft
|
||||
if (!streamOnly) {
|
||||
loadFiles();
|
||||
}
|
||||
</script>
|
||||
|
||||
23
src/pages/index.astro
Normal file
23
src/pages/index.astro
Normal file
@@ -0,0 +1,23 @@
|
||||
---
|
||||
import Layout from '../layouts/Layout.astro';
|
||||
import LoginForm from '../components/LoginForm.astro';
|
||||
import { getSession, isLoginEnabled } from '../lib/session';
|
||||
|
||||
const loginEnabled = isLoginEnabled();
|
||||
|
||||
// Wenn Login deaktiviert ist, direkt zu /download weiterleiten
|
||||
if (!loginEnabled) {
|
||||
return Astro.redirect('/download');
|
||||
}
|
||||
|
||||
const session = await getSession(Astro.request);
|
||||
|
||||
if (session) {
|
||||
return Astro.redirect('/download');
|
||||
}
|
||||
---
|
||||
|
||||
<Layout title="Login">
|
||||
<LoginForm />
|
||||
</Layout>
|
||||
|
||||
Reference in New Issue
Block a user