summaryrefslogtreecommitdiff
path: root/frontend/app
diff options
context:
space:
mode:
authordiogo464 <[email protected]>2025-08-11 11:51:39 +0100
committerdiogo464 <[email protected]>2025-08-11 11:51:39 +0100
commit4af66f418b6837b6441b4e8eaf2d8ede585238b9 (patch)
tree34a4e913a2848515166b2ac0489794419a33bfcc /frontend/app
parent0d3488a3811c8d58bd570af64cc29840df9ba439 (diff)
snapshot
Diffstat (limited to 'frontend/app')
-rw-r--r--frontend/app/blob/[blobId]/route.ts80
-rw-r--r--frontend/app/drive/[...path]/page.tsx136
-rw-r--r--frontend/app/drive/page.tsx94
-rw-r--r--frontend/app/favicon.icobin0 -> 25931 bytes
-rw-r--r--frontend/app/globals.css26
-rw-r--r--frontend/app/layout.tsx38
-rw-r--r--frontend/app/page.tsx5
7 files changed, 379 insertions, 0 deletions
diff --git a/frontend/app/blob/[blobId]/route.ts b/frontend/app/blob/[blobId]/route.ts
new file mode 100644
index 0000000..1643b70
--- /dev/null
+++ b/frontend/app/blob/[blobId]/route.ts
@@ -0,0 +1,80 @@
1import { Drive_blob_path } from "@/lib/drive"
2import { NextRequest, NextResponse } from "next/server"
3import { createReadStream, statSync } from "fs"
4import path from "path"
5
6export async function GET(
7 request: NextRequest,
8 { params }: { params: Promise<{ blobId: string }> }
9) {
10 try {
11 const { blobId } = await params
12 const { searchParams } = new URL(request.url)
13 const filename = searchParams.get('filename') || 'download'
14
15 // Get the filesystem path for this blob
16 const blobPath = await Drive_blob_path(blobId)
17
18 // Get file stats
19 const stats = statSync(blobPath)
20 const fileSize = stats.size
21
22 // Determine content type based on file extension
23 const ext = path.extname(filename).toLowerCase()
24 const contentType = getContentType(ext)
25
26 // Create readable stream
27 const stream = createReadStream(blobPath)
28
29 // Convert stream to web stream
30 const readableStream = new ReadableStream({
31 start(controller) {
32 stream.on('data', (chunk: Buffer) => {
33 controller.enqueue(new Uint8Array(chunk))
34 })
35 stream.on('end', () => {
36 controller.close()
37 })
38 stream.on('error', (error) => {
39 controller.error(error)
40 })
41 }
42 })
43
44 return new NextResponse(readableStream, {
45 headers: {
46 'Content-Type': contentType,
47 'Content-Length': fileSize.toString(),
48 'Content-Disposition': `attachment; filename="${filename}"`,
49 'Cache-Control': 'public, max-age=31536000', // Cache for 1 year
50 },
51 })
52
53 } catch (error) {
54 console.error('Error serving blob:', error)
55 return new NextResponse('File not found', { status: 404 })
56 }
57}
58
59function getContentType(extension: string): string {
60 const mimeTypes: Record<string, string> = {
61 '.pdf': 'application/pdf',
62 '.txt': 'text/plain',
63 '.md': 'text/markdown',
64 '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
65 '.doc': 'application/msword',
66 '.zip': 'application/zip',
67 '.png': 'image/png',
68 '.jpg': 'image/jpeg',
69 '.jpeg': 'image/jpeg',
70 '.gif': 'image/gif',
71 '.svg': 'image/svg+xml',
72 '.css': 'text/css',
73 '.js': 'application/javascript',
74 '.json': 'application/json',
75 '.html': 'text/html',
76 '.htm': 'text/html',
77 }
78
79 return mimeTypes[extension] || 'application/octet-stream'
80} \ No newline at end of file
diff --git a/frontend/app/drive/[...path]/page.tsx b/frontend/app/drive/[...path]/page.tsx
new file mode 100644
index 0000000..75a1bb1
--- /dev/null
+++ b/frontend/app/drive/[...path]/page.tsx
@@ -0,0 +1,136 @@
1import { Drive_ls, Drive_basename, Drive_parent } from "@/lib/drive"
2import { formatSize } from "@/lib/utils"
3import Link from "next/link"
4import { cookies } from 'next/headers';
5import { Auth_get_user } from "@/lib/auth";
6
7interface DrivePageProps {
8 params: Promise<{
9 path: string[]
10 }>
11}
12
13export default async function DrivePage({ params }: DrivePageProps) {
14 // Await params as required by Next.js 15
15 const { path } = await params
16
17 const user = await Auth_get_user();
18 console.log(user);
19
20 // Construct the full path from params
21 const fullPath = path ? `/${path.join('/')}` : ""
22
23 const entries = await Drive_ls(fullPath, false)
24
25 // Check if we have a parent directory
26 const parentDir = Drive_parent(fullPath)
27 const parentPath = path && path.length > 1
28 ? `/drive/${path.slice(0, -1).join('/')}`
29 : path && path.length === 1
30 ? '/drive'
31 : null
32
33 // Create entries with optional parent directory at top
34 const allEntries = []
35 if (parentDir !== null && parentPath !== null) {
36 allEntries.push({
37 path: '(parent)',
38 type: 'dir' as const,
39 lastmod: 0,
40 blob: null,
41 size: null,
42 author: '',
43 isParent: true,
44 parentPath
45 })
46 }
47
48 // Sort entries: directories first, then files, both alphabetically
49 const sortedEntries = entries.sort((a, b) => {
50 // First sort by type (directories before files)
51 if (a.type !== b.type) {
52 return a.type === 'dir' ? -1 : 1
53 }
54 // Then sort alphabetically by path
55 return a.path.localeCompare(b.path)
56 })
57
58 allEntries.push(...sortedEntries)
59
60 return (
61 <div className="min-h-screen bg-background">
62 <div className="container mx-auto p-4">
63 <Link href="/drive" className="inline-block mb-6">
64 <h1 className="text-2xl font-bold text-foreground hover:text-blue-600 dark:hover:text-blue-400 transition-colors cursor-pointer">FCTDrive</h1>
65 </Link>
66
67 <div className="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden">
68 <div className="overflow-x-auto">
69 <table className="w-full">
70 <thead className="bg-gray-50 dark:bg-gray-700">
71 <tr>
72 <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
73 Name
74 </th>
75 <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
76 Size
77 </th>
78 <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
79 Author
80 </th>
81 <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
82 Modified
83 </th>
84 </tr>
85 </thead>
86 <tbody className="divide-y divide-gray-200 dark:divide-gray-600">
87 {allEntries.map((entry, index) => (
88 <tr
89 key={entry.path}
90 className="hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
91 >
92 <td className="px-4 py-4 whitespace-nowrap">
93 <div className="flex items-center">
94 <div className="flex-shrink-0 h-5 w-5 mr-3">
95 {entry.type === 'dir' ? (
96 <div className="h-5 w-5 text-blue-500">📁</div>
97 ) : (
98 <div className="h-5 w-5 text-gray-400">📄</div>
99 )}
100 </div>
101 {entry.type === 'dir' ? (
102 <Link
103 href={(entry as any).isParent ? (entry as any).parentPath : `/drive${entry.path}`}
104 className="text-sm font-medium text-blue-600 dark:text-blue-400 hover:underline"
105 >
106 {(entry as any).isParent ? '(parent)' : Drive_basename(entry.path)}
107 </Link>
108 ) : (
109 <Link
110 href={`/blob/${entry.blob}?filename=${encodeURIComponent(Drive_basename(entry.path))}`}
111 className="text-sm font-medium text-blue-600 dark:text-blue-400 hover:underline"
112 >
113 {Drive_basename(entry.path)}
114 </Link>
115 )}
116 </div>
117 </td>
118 <td className="px-4 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-300">
119 {formatSize(entry.size)}
120 </td>
121 <td className="px-4 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-300">
122 {entry.author}
123 </td>
124 <td className="px-4 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-300">
125 {new Date(entry.lastmod * 1000).toLocaleString()}
126 </td>
127 </tr>
128 ))}
129 </tbody>
130 </table>
131 </div>
132 </div>
133 </div>
134 </div>
135 )
136}
diff --git a/frontend/app/drive/page.tsx b/frontend/app/drive/page.tsx
new file mode 100644
index 0000000..218774a
--- /dev/null
+++ b/frontend/app/drive/page.tsx
@@ -0,0 +1,94 @@
1import { Drive_ls, Drive_basename } from "@/lib/drive"
2import { formatSize } from "@/lib/utils"
3import Link from "next/link"
4
5export default async function DrivePage() {
6 const entries = await Drive_ls("", false)
7
8 // Sort entries: directories first, then files, both alphabetically
9 const sortedEntries = entries.sort((a, b) => {
10 // First sort by type (directories before files)
11 if (a.type !== b.type) {
12 return a.type === 'dir' ? -1 : 1
13 }
14 // Then sort alphabetically by path
15 return a.path.localeCompare(b.path)
16 })
17
18 return (
19 <div className="min-h-screen bg-background">
20 <div className="container mx-auto p-4">
21 <Link href="/drive" className="inline-block mb-6">
22 <h1 className="text-2xl font-bold text-foreground hover:text-blue-600 dark:hover:text-blue-400 transition-colors cursor-pointer">FCTDrive</h1>
23 </Link>
24
25 <div className="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden">
26 <div className="overflow-x-auto">
27 <table className="w-full">
28 <thead className="bg-gray-50 dark:bg-gray-700">
29 <tr>
30 <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
31 Name
32 </th>
33 <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
34 Size
35 </th>
36 <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
37 Author
38 </th>
39 <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
40 Modified
41 </th>
42 </tr>
43 </thead>
44 <tbody className="divide-y divide-gray-200 dark:divide-gray-600">
45 {sortedEntries.map((entry, index) => (
46 <tr
47 key={entry.path}
48 className="hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
49 >
50 <td className="px-4 py-4 whitespace-nowrap">
51 <div className="flex items-center">
52 <div className="flex-shrink-0 h-5 w-5 mr-3">
53 {entry.type === 'dir' ? (
54 <div className="h-5 w-5 text-blue-500">📁</div>
55 ) : (
56 <div className="h-5 w-5 text-gray-400">📄</div>
57 )}
58 </div>
59 {entry.type === 'dir' ? (
60 <Link
61 href={`/drive${entry.path}`}
62 className="text-sm font-medium text-blue-600 dark:text-blue-400 hover:underline"
63 >
64 {Drive_basename(entry.path)}
65 </Link>
66 ) : (
67 <Link
68 href={`/blob/${entry.blob}?filename=${encodeURIComponent(Drive_basename(entry.path))}`}
69 className="text-sm font-medium text-blue-600 dark:text-blue-400 hover:underline"
70 >
71 {Drive_basename(entry.path)}
72 </Link>
73 )}
74 </div>
75 </td>
76 <td className="px-4 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-300">
77 {formatSize(entry.size)}
78 </td>
79 <td className="px-4 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-300">
80 {entry.author}
81 </td>
82 <td className="px-4 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-300">
83 {new Date(entry.lastmod * 1000).toLocaleString()}
84 </td>
85 </tr>
86 ))}
87 </tbody>
88 </table>
89 </div>
90 </div>
91 </div>
92 </div>
93 )
94} \ No newline at end of file
diff --git a/frontend/app/favicon.ico b/frontend/app/favicon.ico
new file mode 100644
index 0000000..718d6fe
--- /dev/null
+++ b/frontend/app/favicon.ico
Binary files differ
diff --git a/frontend/app/globals.css b/frontend/app/globals.css
new file mode 100644
index 0000000..a2dc41e
--- /dev/null
+++ b/frontend/app/globals.css
@@ -0,0 +1,26 @@
1@import "tailwindcss";
2
3:root {
4 --background: #ffffff;
5 --foreground: #171717;
6}
7
8@theme inline {
9 --color-background: var(--background);
10 --color-foreground: var(--foreground);
11 --font-sans: var(--font-geist-sans);
12 --font-mono: var(--font-geist-mono);
13}
14
15@media (prefers-color-scheme: dark) {
16 :root {
17 --background: #0a0a0a;
18 --foreground: #ededed;
19 }
20}
21
22body {
23 background: var(--background);
24 color: var(--foreground);
25 font-family: Arial, Helvetica, sans-serif;
26}
diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx
new file mode 100644
index 0000000..2e001e4
--- /dev/null
+++ b/frontend/app/layout.tsx
@@ -0,0 +1,38 @@
1import type { Metadata } from "next";
2import { Geist, Geist_Mono } from "next/font/google";
3import "./globals.css";
4import AuthButton from "@/components/AuthButton";
5
6const geistSans = Geist({
7 variable: "--font-geist-sans",
8 subsets: ["latin"],
9});
10
11const geistMono = Geist_Mono({
12 variable: "--font-geist-mono",
13 subsets: ["latin"],
14});
15
16export const metadata: Metadata = {
17 title: "FCTDrive",
18 description: "Simple file browsing interface for FCTDrive",
19};
20
21export default function RootLayout({
22 children,
23}: Readonly<{
24 children: React.ReactNode;
25}>) {
26 return (
27 <html lang="en">
28 <body
29 className={`${geistSans.variable} ${geistMono.variable} antialiased`}
30 >
31 <div className="absolute top-4 right-4 z-10">
32 <AuthButton />
33 </div>
34 {children}
35 </body>
36 </html>
37 );
38}
diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx
new file mode 100644
index 0000000..3333985
--- /dev/null
+++ b/frontend/app/page.tsx
@@ -0,0 +1,5 @@
1import { redirect } from 'next/navigation'
2
3export default function Home() {
4 redirect('/drive')
5}