Compare commits
10 Commits
e890881221
...
a310fe9564
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 }}."
|
||||||
21
.gitea/workflows/vercel-preview.yaml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
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 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 }}
|
||||||
21
.gitea/workflows/vercel-production.yaml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
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 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 }}
|
||||||
@@ -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 pool from '@/lib/db';
|
||||||
import bcrypt from 'bcrypt';
|
import bcrypt from 'bcrypt';
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
|
import { ResultSetHeader } from 'mysql2/promise';
|
||||||
|
|
||||||
const SALT_ROUNDS = 10;
|
const SALT_ROUNDS = 10;
|
||||||
|
|
||||||
@@ -28,7 +29,7 @@ export async function POST(request: Request) {
|
|||||||
|
|
||||||
const connection = await pool.getConnection();
|
const connection = await pool.getConnection();
|
||||||
try {
|
try {
|
||||||
const [result]: any = await connection.query(
|
const [result]: [ResultSetHeader, unknown] = await connection.query(
|
||||||
'UPDATE files SET token_hash = ?, expires_at = ? WHERE id = ?',
|
'UPDATE files SET token_hash = ?, expires_at = ? WHERE id = ?',
|
||||||
[tokenHash, expiresAt, file_id]
|
[tokenHash, expiresAt, file_id]
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import pool from '@/lib/db';
|
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) {
|
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');
|
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();
|
const staleTime = new Date();
|
||||||
staleTime.setHours(staleTime.getHours() - 24); // Older than 24 hours
|
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 <= ?',
|
'SELECT id FROM files WHERE token_hash IS NULL AND upload_at <= ?',
|
||||||
[staleTime]
|
[staleTime]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (incompleteFiles.length > 0) {
|
if (incompleteFiles[0].length > 0) {
|
||||||
for (const file of incompleteFiles) {
|
for (const file of incompleteFiles[0] as FileId[]) {
|
||||||
const file_id = file.id;
|
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) {
|
if (parts[0].length > 0) {
|
||||||
for (const part of parts) {
|
for (const part of parts[0] as FilePart[]) {
|
||||||
if (!part.discord_message_id) continue;
|
if (!part.discord_message_id) continue;
|
||||||
const deleteUrl = `https://discord.com/api/v10/channels/${process.env.DISCORD_CHANNEL_ID}/messages/${part.discord_message_id}`;
|
const deleteUrl = `https://discord.com/api/v10/channels/${process.env.DISCORD_CHANNEL_ID}/messages/${part.discord_message_id}`;
|
||||||
try {
|
try {
|
||||||
@@ -58,21 +67,21 @@ export async function POST(request: Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- 2. Cleanup for EXPIRED files ---
|
// --- 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'
|
'SELECT id FROM files WHERE expires_at <= NOW() AND deleted = 0'
|
||||||
);
|
);
|
||||||
|
|
||||||
if (expiredFiles.length > 0) {
|
if (expiredFiles[0].length > 0) {
|
||||||
for (const file of expiredFiles) {
|
for (const file of expiredFiles[0] as FileId[]) {
|
||||||
const file_id = file.id;
|
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 = ?',
|
'SELECT discord_message_id FROM file_parts WHERE file_id = ?',
|
||||||
[file_id]
|
[file_id]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (partsRows.length > 0) {
|
if (partsRows[0].length > 0) {
|
||||||
for (const part of partsRows) {
|
for (const part of partsRows[0] as FilePart[]) {
|
||||||
if (!part.discord_message_id) continue;
|
if (!part.discord_message_id) continue;
|
||||||
const deleteUrl = `https://discord.com/api/v10/channels/${process.env.DISCORD_CHANNEL_ID}/messages/${part.discord_message_id}`;
|
const deleteUrl = `https://discord.com/api/v10/channels/${process.env.DISCORD_CHANNEL_ID}/messages/${part.discord_message_id}`;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,6 +1,15 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import pool from '@/lib/db';
|
import pool from '@/lib/db';
|
||||||
import bcrypt from 'bcrypt';
|
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) {
|
if (!process.env.DISCORD_BOT_TOKEN || !process.env.DISCORD_CHANNEL_ID) {
|
||||||
throw new Error('Discord bot token or channel ID is not configured');
|
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();
|
const connection = await pool.getConnection();
|
||||||
try {
|
try {
|
||||||
// 1. Fetch file and validate token
|
// 1. Fetch file and validate token
|
||||||
const [fileRows]: any[] = await connection.query('SELECT * FROM files WHERE id = ? AND deleted = 0', [file_id]);
|
const [fileRows]: [RowDataPacket[], unknown] = await connection.query('SELECT * FROM files WHERE id = ? AND deleted = 0', [file_id]);
|
||||||
const file = fileRows[0];
|
const file = fileRows[0] as FileData;
|
||||||
|
|
||||||
if (!file) {
|
if (!file) {
|
||||||
return NextResponse.json({ error: 'File not found' }, { status: 404 });
|
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
|
// 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 = ?',
|
'SELECT discord_message_id FROM file_parts WHERE file_id = ?',
|
||||||
[file_id]
|
[file_id]
|
||||||
);
|
);
|
||||||
|
|
||||||
// 3. Delete each message from Discord sequentially to avoid rate limits
|
// 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;
|
if (!part.discord_message_id) continue;
|
||||||
|
|
||||||
const deleteUrl = `https://discord.com/api/v10/channels/${process.env.DISCORD_CHANNEL_ID}/messages/${part.discord_message_id}`;
|
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 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) {
|
if (!process.env.DISCORD_BOT_TOKEN || !process.env.DISCORD_CHANNEL_ID) {
|
||||||
throw new Error('Discord bot token or channel ID is not configured');
|
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 {
|
try {
|
||||||
const params = await context.params;
|
const { file_id, part_index } = await context.params;
|
||||||
const file_id = params.file_id as string;
|
|
||||||
const part_index = params.part_index as string;
|
|
||||||
|
|
||||||
// NOTE: In a real-world scenario, you MUST validate if the user has permission to download this part.
|
// 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.
|
// 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();
|
const connection = await pool.getConnection();
|
||||||
let part;
|
let part;
|
||||||
try {
|
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 = ?',
|
'SELECT discord_message_id FROM file_parts WHERE file_id = ? AND part_index = ?',
|
||||||
[file_id, part_index]
|
[file_id, part_index]
|
||||||
);
|
);
|
||||||
part = partsRows[0];
|
part = partsRows[0] as FilePart;
|
||||||
} finally {
|
} finally {
|
||||||
connection.release();
|
connection.release();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,30 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse, NextRequest } from 'next/server';
|
||||||
import pool from '@/lib/db';
|
import pool from '@/lib/db';
|
||||||
import bcrypt from 'bcrypt';
|
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 {
|
try {
|
||||||
const params = await context.params;
|
const { file_id } = await context.params;
|
||||||
const file_id = params.file_id as string;
|
|
||||||
const { searchParams } = new URL(request.url);
|
const { searchParams } = new URL(request.url);
|
||||||
const token = searchParams.get('token');
|
const token = searchParams.get('token');
|
||||||
|
|
||||||
@@ -16,8 +35,8 @@ export async function GET(request: Request, context: any) {
|
|||||||
const connection = await pool.getConnection();
|
const connection = await pool.getConnection();
|
||||||
try {
|
try {
|
||||||
// 1. Fetch file metadata and token hash
|
// 1. Fetch file metadata and token hash
|
||||||
const [fileRows]: any[] = await connection.query('SELECT * FROM files WHERE id = ? AND deleted = 0', [file_id]);
|
const [fileRows]: [RowDataPacket[], unknown] = await connection.query('SELECT * FROM files WHERE id = ? AND deleted = 0', [file_id]);
|
||||||
const file = fileRows[0];
|
const file = fileRows[0] as FileData;
|
||||||
|
|
||||||
if (!file) {
|
if (!file) {
|
||||||
return NextResponse.json({ error: 'File not found' }, { status: 404 });
|
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
|
// 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',
|
'SELECT part_index, size FROM file_parts WHERE file_id = ? ORDER BY part_index ASC',
|
||||||
[file_id]
|
[file_id]
|
||||||
);
|
);
|
||||||
@@ -45,7 +64,7 @@ export async function GET(request: Request, context: any) {
|
|||||||
filename: file.filename,
|
filename: file.filename,
|
||||||
size: file.size,
|
size: file.size,
|
||||||
num_parts: file.num_parts,
|
num_parts: file.num_parts,
|
||||||
parts: partsRows,
|
parts: partsRows[0] as FilePartMetadata[],
|
||||||
};
|
};
|
||||||
|
|
||||||
return NextResponse.json(response);
|
return NextResponse.json(response);
|
||||||
|
|||||||
@@ -3,6 +3,11 @@
|
|||||||
import { useEffect, useState, useRef } from 'react';
|
import { useEffect, useState, useRef } from 'react';
|
||||||
import { useParams, useSearchParams } from 'next/navigation';
|
import { useParams, useSearchParams } from 'next/navigation';
|
||||||
|
|
||||||
|
interface MetadataPart {
|
||||||
|
part_index: number;
|
||||||
|
size: number;
|
||||||
|
}
|
||||||
|
|
||||||
// Helper to convert Base64 string back to ArrayBuffer
|
// Helper to convert Base64 string back to ArrayBuffer
|
||||||
function base64ToBuffer(base64: string) {
|
function base64ToBuffer(base64: string) {
|
||||||
const binary_string = window.atob(base64.replace(/-/g, '+').replace(/_/g, '/'));
|
const binary_string = window.atob(base64.replace(/-/g, '+').replace(/_/g, '/'));
|
||||||
@@ -106,7 +111,7 @@ export default function DownloadPage() {
|
|||||||
setDownloadState('downloading');
|
setDownloadState('downloading');
|
||||||
const encryptedParts = new Array(metadata.num_parts);
|
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 response = await fetchWithRetry(`/api/download-part/${file_id}/${part.part_index}`);
|
||||||
const buffer = await response.arrayBuffer();
|
const buffer = await response.arrayBuffer();
|
||||||
encryptedParts[part.part_index] = { index: part.part_index, data: buffer };
|
encryptedParts[part.part_index] = { index: part.part_index, data: buffer };
|
||||||
@@ -127,7 +132,7 @@ export default function DownloadPage() {
|
|||||||
['decrypt']
|
['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 iv = part.data.slice(0, 12);
|
||||||
const data = part.data.slice(12);
|
const data = part.data.slice(12);
|
||||||
const decrypted = await window.crypto.subtle.decrypt({ name: 'AES-GCM', iv }, fileKey, data);
|
const decrypted = await window.crypto.subtle.decrypt({ name: 'AES-GCM', iv }, fileKey, data);
|
||||||
@@ -148,9 +153,8 @@ export default function DownloadPage() {
|
|||||||
|
|
||||||
setDownloadState('complete');
|
setDownloadState('complete');
|
||||||
|
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
setError(e.message || 'An unknown error occurred.');
|
setError(e instanceof Error ? e.message : 'An unknown error occurred.'); setDownloadState('error');
|
||||||
setDownloadState('error');
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -181,9 +185,9 @@ export default function DownloadPage() {
|
|||||||
|
|
||||||
setDeleteState('deleted');
|
setDeleteState('deleted');
|
||||||
|
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
setDeleteState('error');
|
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' && (
|
{deleteState === 'idle' && (
|
||||||
<button
|
<button
|
||||||
onClick={handleDelete}
|
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"
|
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>
|
</button>
|
||||||
)}
|
)}
|
||||||
{deleteState === 'deleting' && (
|
{deleteState === 'deleting' && (
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 43 KiB |
@@ -13,8 +13,8 @@ const geistMono = Geist_Mono({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Create Next App",
|
title: "LockLoad",
|
||||||
description: "Generated by create next app",
|
description: "Securely upload and share files.",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ export default function Home() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const queue = [...chunks];
|
const queue = [...chunks];
|
||||||
const active = [];
|
const active: Promise<void>[] = [];
|
||||||
while (queue.length > 0 || active.length > 0) {
|
while (queue.length > 0 || active.length > 0) {
|
||||||
while (active.length < PARALLEL_UPLOADS && queue.length > 0) {
|
while (active.length < PARALLEL_UPLOADS && queue.length > 0) {
|
||||||
const task = queue.shift()!;
|
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">
|
<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="w-full max-w-xl">
|
||||||
<div className="text-center mb-8">
|
<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>
|
<p className="text-lg text-gray-400">Upload large files, encrypted and stored on Discord. 7-day retention.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||