summaryrefslogtreecommitdiff
path: root/frontend/app
diff options
context:
space:
mode:
authordiogo464 <[email protected]>2025-08-11 16:28:59 +0100
committerdiogo464 <[email protected]>2025-08-11 16:28:59 +0100
commit2e02765e4b79d0d145520f9005c75d382805dc2e (patch)
tree08e0279988c804ad9e4e9301a2e23648decf292d /frontend/app
parent68afafc281103c32b193d5f116d87f74187bdc63 (diff)
implement RESTful API and remove legacy endpoints
- Created unified /api/fs/[...path] endpoint with full REST methods: - GET: List directory contents or file info - POST: Create directories using Drive_mkdir() - PUT: Upload files with multipart form data - DELETE: Remove files/directories using Drive_remove() - Added /api/fs route for root directory listing - Added Drive_mkdir() function to drive_server.ts using fctdrive mkdir command - Removed legacy /api/delete and /api/upload endpoints - Updated CLAUDE.md with comprehensive API documentation and examples - All endpoints support authentication with AUTH: 1 header in development - Proper error handling, file size validation, and cache revalidation 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
Diffstat (limited to 'frontend/app')
-rw-r--r--frontend/app/api/delete/route.ts48
-rw-r--r--frontend/app/api/fs/[...path]/route.ts209
-rw-r--r--frontend/app/api/fs/route.ts19
-rw-r--r--frontend/app/api/upload/route.ts127
4 files changed, 228 insertions, 175 deletions
diff --git a/frontend/app/api/delete/route.ts b/frontend/app/api/delete/route.ts
deleted file mode 100644
index b4a27d4..0000000
--- a/frontend/app/api/delete/route.ts
+++ /dev/null
@@ -1,48 +0,0 @@
1import { NextRequest, NextResponse } from 'next/server'
2import { Auth_get_user, Auth_user_can_upload } from '@/lib/auth'
3import { Drive_remove } from '@/lib/drive_server'
4import { revalidatePath } from 'next/cache'
5
6export async function POST(request: NextRequest) {
7 try {
8 // Check user authentication and permissions
9 const user = await Auth_get_user()
10 if (!user.isLoggedIn) {
11 return NextResponse.json({ error: 'User not authenticated' }, { status: 401 })
12 }
13
14 if (!Auth_user_can_upload(user)) {
15 return NextResponse.json({ error: 'User does not have upload permissions' }, { status: 403 })
16 }
17
18 // Parse JSON body
19 const body = await request.json()
20 const path = body.path
21
22 // Validate path
23 if (!path || typeof path !== 'string') {
24 return NextResponse.json({ error: 'Path is required and must be a string' }, { status: 400 })
25 }
26
27 // Remove file/directory using Drive_remove
28 await Drive_remove(path, user.email)
29
30 // Revalidate the parent directory to refresh listings
31 const parentPath = path.split('/').slice(0, -1).join('/') || '/'
32 revalidatePath(`/drive${parentPath}`)
33 revalidatePath('/drive')
34
35 return NextResponse.json({
36 success: true,
37 message: 'Path deleted successfully',
38 deletedPath: path
39 })
40
41 } catch (error) {
42 console.error('Delete error:', error)
43 return NextResponse.json(
44 { error: error instanceof Error ? error.message : 'Internal server error' },
45 { status: 500 }
46 )
47 }
48} \ No newline at end of file
diff --git a/frontend/app/api/fs/[...path]/route.ts b/frontend/app/api/fs/[...path]/route.ts
new file mode 100644
index 0000000..3a299af
--- /dev/null
+++ b/frontend/app/api/fs/[...path]/route.ts
@@ -0,0 +1,209 @@
1import { NextRequest, NextResponse } from 'next/server'
2import { writeFile, unlink } from 'fs/promises'
3import { tmpdir } from 'os'
4import { join } from 'path'
5import { randomUUID } from 'crypto'
6import { Auth_get_user, Auth_user_can_upload } from '@/lib/auth'
7import { Drive_ls, Drive_remove, Drive_mkdir, Drive_import } from '@/lib/drive_server'
8import { UPLOAD_MAX_FILE_SIZE } from '@/lib/constants'
9import { revalidatePath } from 'next/cache'
10
11// GET /api/fs/path/to/file - Get file/directory listing
12export async function GET(
13 request: NextRequest,
14 { params }: { params: Promise<{ path: string[] }> }
15) {
16 try {
17 const { path: pathSegments } = await params
18 const filePath = '/' + (pathSegments?.join('/') || '')
19
20 // Get directory listing using Drive_ls (non-recursive)
21 const entries = await Drive_ls(filePath, false)
22
23 return NextResponse.json(entries)
24
25 } catch (error) {
26 console.error('GET fs error:', error)
27 return NextResponse.json(
28 { error: error instanceof Error ? error.message : 'Internal server error' },
29 { status: 500 }
30 )
31 }
32}
33
34// DELETE /api/fs/path/to/file - Delete file/directory
35export async function DELETE(
36 request: NextRequest,
37 { params }: { params: Promise<{ path: string[] }> }
38) {
39 try {
40 // Check user authentication and permissions
41 const user = await Auth_get_user()
42 if (!user.isLoggedIn) {
43 return NextResponse.json({ error: 'User not authenticated' }, { status: 401 })
44 }
45
46 if (!Auth_user_can_upload(user)) {
47 return NextResponse.json({ error: 'User does not have upload permissions' }, { status: 403 })
48 }
49
50 const { path: pathSegments } = await params
51 const filePath = '/' + (pathSegments?.join('/') || '')
52
53 // Remove file/directory using Drive_remove
54 await Drive_remove(filePath, user.email)
55
56 // Revalidate the parent directory to refresh listings
57 const parentPath = filePath.split('/').slice(0, -1).join('/') || '/'
58 revalidatePath(`/drive${parentPath}`)
59 revalidatePath('/drive')
60
61 return NextResponse.json({
62 success: true,
63 message: 'Path deleted successfully',
64 deletedPath: filePath
65 })
66
67 } catch (error) {
68 console.error('DELETE fs error:', error)
69 return NextResponse.json(
70 { error: error instanceof Error ? error.message : 'Internal server error' },
71 { status: 500 }
72 )
73 }
74}
75
76// PUT /api/fs/path/to/file - Create/upload file
77export async function PUT(
78 request: NextRequest,
79 { params }: { params: Promise<{ path: string[] }> }
80) {
81 try {
82 // Check user authentication and permissions
83 const user = await Auth_get_user()
84 if (!user.isLoggedIn) {
85 return NextResponse.json({ error: 'User not authenticated' }, { status: 401 })
86 }
87
88 if (!Auth_user_can_upload(user)) {
89 return NextResponse.json({ error: 'User does not have upload permissions' }, { status: 403 })
90 }
91
92 const { path: pathSegments } = await params
93 const filePath = '/' + (pathSegments?.join('/') || '')
94
95 // Check if request has file content
96 const contentType = request.headers.get('content-type')
97 if (!contentType || (!contentType.includes('multipart/form-data') && !contentType.includes('application/octet-stream'))) {
98 return NextResponse.json({
99 error: 'Content-Type must be multipart/form-data or application/octet-stream'
100 }, { status: 400 })
101 }
102
103 let fileBuffer: Buffer
104 let filename: string
105
106 if (contentType.includes('multipart/form-data')) {
107 // Handle multipart form data
108 const formData = await request.formData()
109 const file = formData.get('file') as File
110
111 if (!file) {
112 return NextResponse.json({ error: 'No file provided' }, { status: 400 })
113 }
114
115 if (file.size > UPLOAD_MAX_FILE_SIZE) {
116 return NextResponse.json({
117 error: `File exceeds maximum size of ${UPLOAD_MAX_FILE_SIZE / (1024 * 1024)}MB`
118 }, { status: 400 })
119 }
120
121 const bytes = await file.arrayBuffer()
122 fileBuffer = Buffer.from(bytes)
123 filename = file.name
124 } else {
125 // Handle raw binary data
126 const bytes = await request.arrayBuffer()
127 fileBuffer = Buffer.from(bytes)
128
129 if (fileBuffer.length > UPLOAD_MAX_FILE_SIZE) {
130 return NextResponse.json({
131 error: `File exceeds maximum size of ${UPLOAD_MAX_FILE_SIZE / (1024 * 1024)}MB`
132 }, { status: 400 })
133 }
134
135 // Extract filename from path
136 filename = pathSegments?.[pathSegments.length - 1] || 'upload'
137 }
138
139 // Create temporary file
140 const tempFileName = `${randomUUID()}-${filename}`
141 const tempFilePath = join(tmpdir(), tempFileName)
142
143 // Save file to temporary location
144 await writeFile(tempFilePath, fileBuffer)
145
146 // Import file using Drive_import (uses --mode move, so temp file is already deleted)
147 await Drive_import(tempFilePath, filePath, user.email)
148
149 // Revalidate the parent directory to refresh listings
150 const parentPath = filePath.split('/').slice(0, -1).join('/') || '/'
151 revalidatePath(`/drive${parentPath}`)
152 revalidatePath('/drive')
153
154 return NextResponse.json({
155 success: true,
156 message: 'File uploaded successfully',
157 path: filePath
158 })
159
160 } catch (error) {
161 console.error('PUT fs error:', error)
162 return NextResponse.json(
163 { error: error instanceof Error ? error.message : 'Internal server error' },
164 { status: 500 }
165 )
166 }
167}
168
169// POST /api/fs/path/to/directory - Create directory
170export async function POST(
171 request: NextRequest,
172 { params }: { params: Promise<{ path: string[] }> }
173) {
174 try {
175 // Check user authentication and permissions
176 const user = await Auth_get_user()
177 if (!user.isLoggedIn) {
178 return NextResponse.json({ error: 'User not authenticated' }, { status: 401 })
179 }
180
181 if (!Auth_user_can_upload(user)) {
182 return NextResponse.json({ error: 'User does not have upload permissions' }, { status: 403 })
183 }
184
185 const { path: pathSegments } = await params
186 const dirPath = '/' + (pathSegments?.join('/') || '')
187
188 // Create directory using Drive_mkdir
189 await Drive_mkdir(dirPath, user.email)
190
191 // Revalidate the parent directory to refresh listings
192 const parentPath = dirPath.split('/').slice(0, -1).join('/') || '/'
193 revalidatePath(`/drive${parentPath}`)
194 revalidatePath('/drive')
195
196 return NextResponse.json({
197 success: true,
198 message: 'Directory created successfully',
199 path: dirPath
200 })
201
202 } catch (error) {
203 console.error('POST fs error:', error)
204 return NextResponse.json(
205 { error: error instanceof Error ? error.message : 'Internal server error' },
206 { status: 500 }
207 )
208 }
209} \ No newline at end of file
diff --git a/frontend/app/api/fs/route.ts b/frontend/app/api/fs/route.ts
new file mode 100644
index 0000000..61d0f8a
--- /dev/null
+++ b/frontend/app/api/fs/route.ts
@@ -0,0 +1,19 @@
1import { NextResponse } from 'next/server'
2import { Drive_ls } from '@/lib/drive_server'
3
4// GET /api/fs - Get root directory listing
5export async function GET() {
6 try {
7 // Get root directory listing using Drive_ls (non-recursive)
8 const entries = await Drive_ls('/', false)
9
10 return NextResponse.json(entries)
11
12 } catch (error) {
13 console.error('GET fs root error:', error)
14 return NextResponse.json(
15 { error: error instanceof Error ? error.message : 'Internal server error' },
16 { status: 500 }
17 )
18 }
19} \ No newline at end of file
diff --git a/frontend/app/api/upload/route.ts b/frontend/app/api/upload/route.ts
deleted file mode 100644
index 164d86d..0000000
--- a/frontend/app/api/upload/route.ts
+++ /dev/null
@@ -1,127 +0,0 @@
1import { NextRequest, NextResponse } from 'next/server'
2import { writeFile, unlink } from 'fs/promises'
3import { tmpdir } from 'os'
4import { join } from 'path'
5import { randomUUID } from 'crypto'
6import { Auth_get_user, Auth_user_can_upload } from '@/lib/auth'
7import { Drive_import } from '@/lib/drive_server'
8import { UPLOAD_MAX_FILE_SIZE, UPLOAD_MAX_FILES } from '@/lib/constants'
9import { revalidatePath } from 'next/cache'
10
11export async function POST(request: NextRequest) {
12 try {
13 // Check user authentication and permissions
14 const user = await Auth_get_user()
15 if (!user.isLoggedIn) {
16 return NextResponse.json({ error: 'User not authenticated' }, { status: 401 })
17 }
18
19 if (!Auth_user_can_upload(user)) {
20 return NextResponse.json({ error: 'User does not have upload permissions' }, { status: 403 })
21 }
22
23 // Parse form data
24 const formData = await request.formData()
25 const files = formData.getAll('files') as File[]
26 const targetPath = formData.get('targetPath') as string || ''
27
28 // Validate files
29 if (!files || files.length === 0) {
30 return NextResponse.json({ error: 'No files provided' }, { status: 400 })
31 }
32
33 if (files.length > UPLOAD_MAX_FILES) {
34 return NextResponse.json({
35 error: `Too many files. Maximum ${UPLOAD_MAX_FILES} files allowed`
36 }, { status: 400 })
37 }
38
39 // Validate each file
40 for (const file of files) {
41 if (file.size > UPLOAD_MAX_FILE_SIZE) {
42 return NextResponse.json({
43 error: `File '${file.name}' exceeds maximum size of ${UPLOAD_MAX_FILE_SIZE / (1024 * 1024)}MB`
44 }, { status: 400 })
45 }
46 }
47
48 const uploadResults = []
49 const tempFiles: string[] = []
50
51 try {
52 // Process each file
53 for (const file of files) {
54 // Create temporary file
55 const tempFileName = `${randomUUID()}-${file.name}`
56 const tempFilePath = join(tmpdir(), tempFileName)
57 tempFiles.push(tempFilePath)
58
59 // Save file to temporary location
60 const bytes = await file.arrayBuffer()
61 const buffer = Buffer.from(bytes)
62 await writeFile(tempFilePath, buffer)
63
64 // Determine target drive path
65 const driveFilePath = targetPath ? `${targetPath}/${file.name}` : `/${file.name}`
66
67 try {
68 // Import file using Drive_import
69 await Drive_import(tempFilePath, driveFilePath, user.email)
70 uploadResults.push({
71 filename: file.name,
72 success: true,
73 message: 'File uploaded successfully'
74 })
75 } catch (error) {
76 console.error(`Failed to import file ${file.name}:`, error)
77 uploadResults.push({
78 filename: file.name,
79 success: false,
80 message: error instanceof Error ? error.message : 'Unknown error during import'
81 })
82 }
83 }
84
85 // Clean up temporary files
86 for (const tempFile of tempFiles) {
87 try {
88 await unlink(tempFile)
89 } catch (error) {
90 console.error(`Failed to delete temp file ${tempFile}:`, error)
91 }
92 }
93
94 // Revalidate the target path to refresh the directory listing
95 revalidatePath(`/drive${targetPath}`)
96 revalidatePath('/drive')
97
98 // Check if any uploads succeeded
99 const successfulUploads = uploadResults.filter(result => result.success)
100 const failedUploads = uploadResults.filter(result => !result.success)
101
102 return NextResponse.json({
103 success: true,
104 message: `${successfulUploads.length} files uploaded successfully${failedUploads.length > 0 ? `, ${failedUploads.length} failed` : ''}`,
105 results: uploadResults
106 })
107
108 } catch (error) {
109 // Clean up temporary files on error
110 for (const tempFile of tempFiles) {
111 try {
112 await unlink(tempFile)
113 } catch (cleanupError) {
114 console.error(`Failed to delete temp file during cleanup ${tempFile}:`, cleanupError)
115 }
116 }
117 throw error
118 }
119
120 } catch (error) {
121 console.error('Upload error:', error)
122 return NextResponse.json(
123 { error: error instanceof Error ? error.message : 'Internal server error' },
124 { status: 500 }
125 )
126 }
127} \ No newline at end of file