Compare commits
15 Commits
e890881221
...
new-design
| Author | SHA1 | Date | |
|---|---|---|---|
| 0dfdbc1a7e | |||
| a584cd0c66 | |||
| 3122f04fa4 | |||
| 872d590ff0 | |||
| ac52eaf984 | |||
| a310fe9564 | |||
| fde0dd7216 | |||
| cf72ed90e7 | |||
| 966450a2de | |||
| f533d86896 | |||
| 97b10716b3 | |||
| ae452de3d0 | |||
| bc723445fa | |||
| d2d50ae860 | |||
| 3a9b29c158 |
19
.gitea/workflows/demo.yaml
Normal file
@@ -0,0 +1,19 @@
|
||||
name: Gitea Actions Demo
|
||||
run-name: ${{ gitea.actor }} is testing out Gitea Actions
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
Explore-Gitea-Actions:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo "The job was automatically triggered by a ${{ gitea.event_name }} event."
|
||||
- run: echo "This job is now running on a ${{ runner.os }} server hosted by Gitea!"
|
||||
- run: echo "The name of your branch is ${{ gitea.ref }} and your repository is ${{ gitea.repository }}."
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v3
|
||||
- run: echo "The ${{ gitea.repository }} repository has been cloned to the runner."
|
||||
- run: echo "The workflow is now ready to test your code on the runner."
|
||||
- name: List files in the repository
|
||||
run: |
|
||||
ls ${{ gitea.workspace }}
|
||||
- run: echo "This job's status is ${{ job.status }}."
|
||||
23
.gitea/workflows/vercel-preview.yaml
Normal file
@@ -0,0 +1,23 @@
|
||||
name: Vercel Preview Deployment
|
||||
env:
|
||||
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
|
||||
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
|
||||
on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- main
|
||||
jobs:
|
||||
Deploy-Preview:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install pnpm
|
||||
run: npm install -g pnpm
|
||||
- name: Install Vercel CLI
|
||||
run: npm install --global vercel@latest
|
||||
- name: Pull Vercel Environment Information
|
||||
run: vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }}
|
||||
- name: Build Project Artifacts
|
||||
run: vercel build --token=${{ secrets.VERCEL_TOKEN }}
|
||||
- name: Deploy Project Artifacts to Vercel
|
||||
run: vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }}
|
||||
23
.gitea/workflows/vercel-production.yaml
Normal file
@@ -0,0 +1,23 @@
|
||||
name: Vercel Production Deployment
|
||||
env:
|
||||
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
|
||||
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
jobs:
|
||||
Deploy-Production:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install pnpm
|
||||
run: npm install -g pnpm
|
||||
- name: Install Vercel CLI
|
||||
run: npm install --global vercel@latest
|
||||
- name: Pull Vercel Environment Information
|
||||
run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}
|
||||
- name: Build Project Artifacts
|
||||
run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}
|
||||
- name: Deploy Project Artifacts to Vercel
|
||||
run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}
|
||||
@@ -9,6 +9,7 @@
|
||||
"lint": "eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vercel/analytics": "^1.5.0",
|
||||
"bcrypt": "^6.0.0",
|
||||
"mysql2": "^3.15.2",
|
||||
"next": "15.5.5",
|
||||
|
||||
34
pnpm-lock.yaml
generated
@@ -8,6 +8,9 @@ importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@vercel/analytics':
|
||||
specifier: ^1.5.0
|
||||
version: 1.5.0(next@15.5.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)
|
||||
bcrypt:
|
||||
specifier: ^6.0.0
|
||||
version: 6.0.0
|
||||
@@ -623,6 +626,32 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@vercel/analytics@1.5.0':
|
||||
resolution: {integrity: sha512-MYsBzfPki4gthY5HnYN7jgInhAZ7Ac1cYDoRWFomwGHWEX7odTEzbtg9kf/QSo7XEsEAqlQugA6gJ2WS2DEa3g==}
|
||||
peerDependencies:
|
||||
'@remix-run/react': ^2
|
||||
'@sveltejs/kit': ^1 || ^2
|
||||
next: '>= 13'
|
||||
react: ^18 || ^19 || ^19.0.0-rc
|
||||
svelte: '>= 4'
|
||||
vue: ^3
|
||||
vue-router: ^4
|
||||
peerDependenciesMeta:
|
||||
'@remix-run/react':
|
||||
optional: true
|
||||
'@sveltejs/kit':
|
||||
optional: true
|
||||
next:
|
||||
optional: true
|
||||
react:
|
||||
optional: true
|
||||
svelte:
|
||||
optional: true
|
||||
vue:
|
||||
optional: true
|
||||
vue-router:
|
||||
optional: true
|
||||
|
||||
acorn-jsx@5.3.2:
|
||||
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
|
||||
peerDependencies:
|
||||
@@ -2360,6 +2389,11 @@ snapshots:
|
||||
'@unrs/resolver-binding-win32-x64-msvc@1.11.1':
|
||||
optional: true
|
||||
|
||||
'@vercel/analytics@1.5.0(next@15.5.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)':
|
||||
optionalDependencies:
|
||||
next: 15.5.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
react: 19.1.0
|
||||
|
||||
acorn-jsx@5.3.2(acorn@8.15.0):
|
||||
dependencies:
|
||||
acorn: 8.15.0
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
||||
|
Before Width: | Height: | Size: 391 B |
@@ -1 +0,0 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
||||
|
Before Width: | Height: | Size: 1.0 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1 +0,0 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
|
||||
|
Before Width: | Height: | Size: 128 B |
@@ -1 +0,0 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
||||
|
Before Width: | Height: | Size: 385 B |
@@ -2,6 +2,7 @@ import { NextResponse } from 'next/server';
|
||||
import pool from '@/lib/db';
|
||||
import bcrypt from 'bcrypt';
|
||||
import crypto from 'crypto';
|
||||
import { ResultSetHeader } from 'mysql2/promise';
|
||||
|
||||
const SALT_ROUNDS = 10;
|
||||
|
||||
@@ -28,7 +29,7 @@ export async function POST(request: Request) {
|
||||
|
||||
const connection = await pool.getConnection();
|
||||
try {
|
||||
const [result]: any = await connection.query(
|
||||
const [result]: [ResultSetHeader, unknown] = await connection.query(
|
||||
'UPDATE files SET token_hash = ?, expires_at = ? WHERE id = ?',
|
||||
[tokenHash, expiresAt, file_id]
|
||||
);
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import pool from '@/lib/db';
|
||||
import { RowDataPacket } from 'mysql2/promise';
|
||||
|
||||
interface FileId {
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface FilePart {
|
||||
discord_message_id: string | null;
|
||||
}
|
||||
|
||||
if (!process.env.DISCORD_BOT_TOKEN || !process.env.DISCORD_CHANNEL_ID || !process.env.CRON_SECRET) {
|
||||
throw new Error('Discord or Cron secret environment variables are not configured');
|
||||
@@ -21,19 +30,19 @@ export async function POST(request: Request) {
|
||||
const staleTime = new Date();
|
||||
staleTime.setHours(staleTime.getHours() - 24); // Older than 24 hours
|
||||
|
||||
const [incompleteFiles]: any[] = await connection.query(
|
||||
const [incompleteFiles]: [RowDataPacket[], unknown] = await connection.query(
|
||||
'SELECT id FROM files WHERE token_hash IS NULL AND upload_at <= ?',
|
||||
[staleTime]
|
||||
);
|
||||
|
||||
if (incompleteFiles.length > 0) {
|
||||
for (const file of incompleteFiles) {
|
||||
if (incompleteFiles[0].length > 0) {
|
||||
for (const file of incompleteFiles[0] as FileId[]) {
|
||||
const file_id = file.id;
|
||||
|
||||
const [parts]: any[] = await connection.query('SELECT discord_message_id FROM file_parts WHERE file_id = ?', [file_id]);
|
||||
const [parts]: [RowDataPacket[], unknown] = await connection.query('SELECT discord_message_id FROM file_parts WHERE file_id = ?', [file_id]);
|
||||
|
||||
if (parts.length > 0) {
|
||||
for (const part of parts) {
|
||||
if (parts[0].length > 0) {
|
||||
for (const part of parts[0] as FilePart[]) {
|
||||
if (!part.discord_message_id) continue;
|
||||
const deleteUrl = `https://discord.com/api/v10/channels/${process.env.DISCORD_CHANNEL_ID}/messages/${part.discord_message_id}`;
|
||||
try {
|
||||
@@ -58,21 +67,21 @@ export async function POST(request: Request) {
|
||||
}
|
||||
|
||||
// --- 2. Cleanup for EXPIRED files ---
|
||||
const [expiredFiles]: any[] = await connection.query(
|
||||
const [expiredFiles]: [RowDataPacket[], unknown] = await connection.query(
|
||||
'SELECT id FROM files WHERE expires_at <= NOW() AND deleted = 0'
|
||||
);
|
||||
|
||||
if (expiredFiles.length > 0) {
|
||||
for (const file of expiredFiles) {
|
||||
if (expiredFiles[0].length > 0) {
|
||||
for (const file of expiredFiles[0] as FileId[]) {
|
||||
const file_id = file.id;
|
||||
|
||||
const [partsRows]: any[] = await connection.query(
|
||||
const [partsRows]: [RowDataPacket[], unknown] = await connection.query(
|
||||
'SELECT discord_message_id FROM file_parts WHERE file_id = ?',
|
||||
[file_id]
|
||||
);
|
||||
|
||||
if (partsRows.length > 0) {
|
||||
for (const part of partsRows) {
|
||||
if (partsRows[0].length > 0) {
|
||||
for (const part of partsRows[0] as FilePart[]) {
|
||||
if (!part.discord_message_id) continue;
|
||||
const deleteUrl = `https://discord.com/api/v10/channels/${process.env.DISCORD_CHANNEL_ID}/messages/${part.discord_message_id}`;
|
||||
try {
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import pool from '@/lib/db';
|
||||
import bcrypt from 'bcrypt';
|
||||
import { RowDataPacket } from 'mysql2/promise';
|
||||
|
||||
interface FileData extends RowDataPacket {
|
||||
token_hash: string;
|
||||
}
|
||||
|
||||
interface FilePart extends RowDataPacket {
|
||||
discord_message_id: string | null;
|
||||
}
|
||||
|
||||
if (!process.env.DISCORD_BOT_TOKEN || !process.env.DISCORD_CHANNEL_ID) {
|
||||
throw new Error('Discord bot token or channel ID is not configured');
|
||||
@@ -17,8 +26,8 @@ export async function POST(request: Request) {
|
||||
const connection = await pool.getConnection();
|
||||
try {
|
||||
// 1. Fetch file and validate token
|
||||
const [fileRows]: any[] = await connection.query('SELECT * FROM files WHERE id = ? AND deleted = 0', [file_id]);
|
||||
const file = fileRows[0];
|
||||
const [fileRows]: [RowDataPacket[], unknown] = await connection.query('SELECT * FROM files WHERE id = ? AND deleted = 0', [file_id]);
|
||||
const file = fileRows[0] as FileData;
|
||||
|
||||
if (!file) {
|
||||
return NextResponse.json({ error: 'File not found' }, { status: 404 });
|
||||
@@ -30,13 +39,13 @@ export async function POST(request: Request) {
|
||||
}
|
||||
|
||||
// 2. Fetch all message IDs for the file parts
|
||||
const [partsRows]: any[] = await connection.query(
|
||||
const [partsRows]: [RowDataPacket[], unknown] = await connection.query(
|
||||
'SELECT discord_message_id FROM file_parts WHERE file_id = ?',
|
||||
[file_id]
|
||||
);
|
||||
|
||||
// 3. Delete each message from Discord sequentially to avoid rate limits
|
||||
for (const part of partsRows) {
|
||||
for (const part of partsRows[0] as FilePart[]) {
|
||||
if (!part.discord_message_id) continue;
|
||||
|
||||
const deleteUrl = `https://discord.com/api/v10/channels/${process.env.DISCORD_CHANNEL_ID}/messages/${part.discord_message_id}`;
|
||||
|
||||
@@ -1,15 +1,25 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { NextResponse, NextRequest } from 'next/server';
|
||||
import pool from '@/lib/db';
|
||||
import { RowDataPacket } from 'mysql2/promise';
|
||||
|
||||
interface FilePart extends RowDataPacket {
|
||||
discord_message_id: string | null;
|
||||
}
|
||||
|
||||
interface RouteContext {
|
||||
params: Promise<{
|
||||
file_id: string;
|
||||
part_index: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
if (!process.env.DISCORD_BOT_TOKEN || !process.env.DISCORD_CHANNEL_ID) {
|
||||
throw new Error('Discord bot token or channel ID is not configured');
|
||||
}
|
||||
|
||||
export async function GET(request: Request, context: any) {
|
||||
export async function GET(request: NextRequest, context: RouteContext) {
|
||||
try {
|
||||
const params = await context.params;
|
||||
const file_id = params.file_id as string;
|
||||
const part_index = params.part_index as string;
|
||||
const { file_id, part_index } = await context.params;
|
||||
|
||||
// NOTE: In a real-world scenario, you MUST validate if the user has permission to download this part.
|
||||
// This would involve checking the download_token against the hash in the `files` table.
|
||||
@@ -19,11 +29,11 @@ export async function GET(request: Request, context: any) {
|
||||
const connection = await pool.getConnection();
|
||||
let part;
|
||||
try {
|
||||
const [partsRows]: any[] = await connection.query(
|
||||
const [partsRows]: [RowDataPacket[], unknown] = await connection.query(
|
||||
'SELECT discord_message_id FROM file_parts WHERE file_id = ? AND part_index = ?',
|
||||
[file_id, part_index]
|
||||
);
|
||||
part = partsRows[0];
|
||||
part = partsRows[0] as FilePart;
|
||||
} finally {
|
||||
connection.release();
|
||||
}
|
||||
|
||||
@@ -1,11 +1,30 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { NextResponse, NextRequest } from 'next/server';
|
||||
import pool from '@/lib/db';
|
||||
import bcrypt from 'bcrypt';
|
||||
import { RowDataPacket } from 'mysql2/promise';
|
||||
|
||||
export async function GET(request: Request, context: any) {
|
||||
interface FileData extends RowDataPacket {
|
||||
filename: string;
|
||||
size: number;
|
||||
num_parts: number;
|
||||
expires_at: Date | null;
|
||||
token_hash: string;
|
||||
}
|
||||
|
||||
interface FilePartMetadata extends RowDataPacket {
|
||||
part_index: number;
|
||||
size: number;
|
||||
}
|
||||
|
||||
interface RouteContext {
|
||||
params: Promise<{
|
||||
file_id: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest, context: RouteContext) {
|
||||
try {
|
||||
const params = await context.params;
|
||||
const file_id = params.file_id as string;
|
||||
const { file_id } = await context.params;
|
||||
const { searchParams } = new URL(request.url);
|
||||
const token = searchParams.get('token');
|
||||
|
||||
@@ -16,8 +35,8 @@ export async function GET(request: Request, context: any) {
|
||||
const connection = await pool.getConnection();
|
||||
try {
|
||||
// 1. Fetch file metadata and token hash
|
||||
const [fileRows]: any[] = await connection.query('SELECT * FROM files WHERE id = ? AND deleted = 0', [file_id]);
|
||||
const file = fileRows[0];
|
||||
const [fileRows]: [RowDataPacket[], unknown] = await connection.query('SELECT * FROM files WHERE id = ? AND deleted = 0', [file_id]);
|
||||
const file = fileRows[0] as FileData;
|
||||
|
||||
if (!file) {
|
||||
return NextResponse.json({ error: 'File not found' }, { status: 404 });
|
||||
@@ -35,7 +54,7 @@ export async function GET(request: Request, context: any) {
|
||||
}
|
||||
|
||||
// 4. Fetch file parts
|
||||
const [partsRows]: any[] = await connection.query(
|
||||
const [partsRows]: [RowDataPacket[], unknown] = await connection.query(
|
||||
'SELECT part_index, size FROM file_parts WHERE file_id = ? ORDER BY part_index ASC',
|
||||
[file_id]
|
||||
);
|
||||
@@ -45,7 +64,7 @@ export async function GET(request: Request, context: any) {
|
||||
filename: file.filename,
|
||||
size: file.size,
|
||||
num_parts: file.num_parts,
|
||||
parts: partsRows,
|
||||
parts: partsRows[0] as FilePartMetadata[],
|
||||
};
|
||||
|
||||
return NextResponse.json(response);
|
||||
|
||||
@@ -3,6 +3,11 @@
|
||||
import { useEffect, useState, useRef } from 'react';
|
||||
import { useParams, useSearchParams } from 'next/navigation';
|
||||
|
||||
interface MetadataPart {
|
||||
part_index: number;
|
||||
size: number;
|
||||
}
|
||||
|
||||
// Helper to convert Base64 string back to ArrayBuffer
|
||||
function base64ToBuffer(base64: string) {
|
||||
const binary_string = window.atob(base64.replace(/-/g, '+').replace(/_/g, '/'));
|
||||
@@ -106,7 +111,7 @@ export default function DownloadPage() {
|
||||
setDownloadState('downloading');
|
||||
const encryptedParts = new Array(metadata.num_parts);
|
||||
|
||||
const downloadPromises = metadata.parts.map(async (part: any) => {
|
||||
const downloadPromises = metadata.parts.map(async (part: MetadataPart) => {
|
||||
const response = await fetchWithRetry(`/api/download-part/${file_id}/${part.part_index}`);
|
||||
const buffer = await response.arrayBuffer();
|
||||
encryptedParts[part.part_index] = { index: part.part_index, data: buffer };
|
||||
@@ -127,7 +132,7 @@ export default function DownloadPage() {
|
||||
['decrypt']
|
||||
);
|
||||
|
||||
const decryptPromises = encryptedParts.map(async (part) => {
|
||||
const decryptPromises = encryptedParts.map(async (part: { index: number; data: ArrayBuffer }) => {
|
||||
const iv = part.data.slice(0, 12);
|
||||
const data = part.data.slice(12);
|
||||
const decrypted = await window.crypto.subtle.decrypt({ name: 'AES-GCM', iv }, fileKey, data);
|
||||
@@ -148,9 +153,8 @@ export default function DownloadPage() {
|
||||
|
||||
setDownloadState('complete');
|
||||
|
||||
} catch (e: any) {
|
||||
setError(e.message || 'An unknown error occurred.');
|
||||
setDownloadState('error');
|
||||
} catch (e: unknown) {
|
||||
setError(e instanceof Error ? e.message : 'An unknown error occurred.'); setDownloadState('error');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -181,9 +185,9 @@ export default function DownloadPage() {
|
||||
|
||||
setDeleteState('deleted');
|
||||
|
||||
} catch (e: any) {
|
||||
} catch (e: unknown) {
|
||||
setDeleteState('error');
|
||||
setDeleteError(e.message || 'An unknown error occurred during deletion.');
|
||||
setDeleteError(e instanceof Error ? e.message : 'An unknown error occurred during deletion.');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -243,10 +247,10 @@ export default function DownloadPage() {
|
||||
{deleteState === 'idle' && (
|
||||
<button
|
||||
onClick={handleDelete}
|
||||
disabled={downloadState === 'downloading' || downloadState === 'decrypting' || deleteState === 'deleting'}
|
||||
disabled={downloadState === 'downloading' || downloadState === 'decrypting'}
|
||||
className="w-full bg-red-600 hover:bg-red-700 text-white font-bold py-3 px-4 rounded-lg transition duration-300 disabled:bg-gray-600 disabled:cursor-not-allowed"
|
||||
>
|
||||
{deleteState === 'deleting' ? 'Deleting...' : 'Delete File'}
|
||||
{'Delete File'}
|
||||
</button>
|
||||
)}
|
||||
{deleteState === 'deleting' && (
|
||||
|
||||
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 43 KiB |
@@ -1,6 +1,7 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import { Analytics } from "@vercel/analytics/next"
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
@@ -13,8 +14,8 @@ const geistMono = Geist_Mono({
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
title: "LockLoad",
|
||||
description: "Securely upload and share files.",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
@@ -24,11 +25,10 @@ export default function RootLayout({
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
|
||||
{children}
|
||||
</body>
|
||||
<Analytics />
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -154,7 +154,7 @@ export default function Home() {
|
||||
};
|
||||
|
||||
const queue = [...chunks];
|
||||
const active = [];
|
||||
const active: Promise<void>[] = [];
|
||||
while (queue.length > 0 || active.length > 0) {
|
||||
while (active.length < PARALLEL_UPLOADS && queue.length > 0) {
|
||||
const task = queue.shift()!;
|
||||
@@ -184,7 +184,7 @@ export default function Home() {
|
||||
<main className="flex min-h-screen flex-col items-center justify-center p-6 bg-gray-900 text-white font-sans">
|
||||
<div className="w-full max-w-xl">
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-4xl font-bold">Discord Storage</h1>
|
||||
<h1 className="text-4xl font-bold">LockLoad</h1>
|
||||
<p className="text-lg text-gray-400">Upload large files, encrypted and stored on Discord. 7-day retention.</p>
|
||||
</div>
|
||||
|
||||
|
||||