diff --git a/.gitignore b/.gitignore index 39d8187c..62105eac 100644 --- a/.gitignore +++ b/.gitignore @@ -161,6 +161,7 @@ cython_debug/ /env.sh /models +/datasets /custom/* !/custom/.gitkeep /.tmp diff --git a/ui/src/app/api/caption/[...imagePath]/route.ts b/ui/src/app/api/caption/[...imagePath]/route.ts new file mode 100644 index 00000000..3a35b88b --- /dev/null +++ b/ui/src/app/api/caption/[...imagePath]/route.ts @@ -0,0 +1,42 @@ +// src/app/api/img/[imagePath]/route.ts +import { NextRequest, NextResponse } from 'next/server'; +import fs from 'fs'; +import path from 'path'; +import { getDatasetsRoot } from '@/app/api/datasets/utils'; + +export async function GET(request: NextRequest, { params }: { params: { imagePath: string } }) { + const { imagePath } = await params; + try { + // Decode the path + const filepath = decodeURIComponent(imagePath); + + // caption name is the filepath without extension but with .txt + const captionPath = filepath.replace(/\.[^/.]+$/, '') + '.txt'; + + // Get allowed directories + const allowedDir = await getDatasetsRoot(); + + // Security check: Ensure path is in allowed directory + const isAllowed = filepath.startsWith(allowedDir) && !filepath.includes('..'); + + if (!isAllowed) { + console.warn(`Access denied: ${filepath} not in ${allowedDir}`); + return new NextResponse('Access denied', { status: 403 }); + } + + // Check if file exists + if (!fs.existsSync(captionPath)) { + // send back blank string if caption file does not exist + return new NextResponse(''); + } + + // Read caption file + const caption = fs.readFileSync(captionPath, 'utf-8'); + + // Return caption + return new NextResponse(caption); + } catch (error) { + console.error('Error getting caption:', error); + return new NextResponse('Error getting caption', { status: 500 }); + } +} diff --git a/ui/src/app/api/datasets/create/route.tsx b/ui/src/app/api/datasets/create/route.tsx new file mode 100644 index 00000000..f552b57f --- /dev/null +++ b/ui/src/app/api/datasets/create/route.tsx @@ -0,0 +1,22 @@ +import { NextResponse } from 'next/server'; +import fs from 'fs'; +import path from 'path'; +import { getDatasetsRoot } from '@/app/api/datasets/utils'; + +export async function POST(request: Request) { + try { + const body = await request.json(); + const { name } = body; + let datasetsPath = await getDatasetsRoot(); + let datasetPath = path.join(datasetsPath, name); + + // if folder doesnt exist, create it + if (!fs.existsSync(datasetPath)) { + fs.mkdirSync(datasetPath); + } + + return NextResponse.json({ success: true }); + } catch (error) { + return NextResponse.json({ error: 'Failed to create dataset' }, { status: 500 }); + } +} \ No newline at end of file diff --git a/ui/src/app/api/datasets/list/route.ts b/ui/src/app/api/datasets/list/route.ts new file mode 100644 index 00000000..1d841681 --- /dev/null +++ b/ui/src/app/api/datasets/list/route.ts @@ -0,0 +1,25 @@ +import { NextResponse } from 'next/server'; +import fs from 'fs'; +import { getDatasetsRoot } from '@/app/api/datasets/utils'; + +export async function GET() { + try { + let datasetsPath = await getDatasetsRoot(); + + // if folder doesnt exist, create it + if (!fs.existsSync(datasetsPath)) { + fs.mkdirSync(datasetsPath); + } + + // find all the folders in the datasets folder + let folders = fs + .readdirSync(datasetsPath, { withFileTypes: true }) + .filter(dirent => dirent.isDirectory()) + .filter(dirent => !dirent.name.startsWith('.')) + .map(dirent => dirent.name); + + return NextResponse.json(folders); + } catch (error) { + return NextResponse.json({ error: 'Failed to fetch datasets' }, { status: 500 }); + } +} diff --git a/ui/src/app/api/datasets/listImages/route.ts b/ui/src/app/api/datasets/listImages/route.ts new file mode 100644 index 00000000..e563d65f --- /dev/null +++ b/ui/src/app/api/datasets/listImages/route.ts @@ -0,0 +1,67 @@ +import { NextResponse } from 'next/server'; +import fs from 'fs'; +import path from 'path'; +import { getDatasetsRoot } from '@/app/api/datasets/utils'; + +export async function POST(request: Request) { + const datasetsPath = await getDatasetsRoot(); + const body = await request.json(); + const { datasetName } = body; + const datasetFolder = path.join(datasetsPath, datasetName); + + try { + // Check if folder exists + if (!fs.existsSync(datasetFolder)) { + return NextResponse.json( + { error: `Folder '${datasetName}' not found` }, + { status: 404 } + ); + } + + // Find all images recursively + const imageFiles = findImagesRecursively(datasetFolder); + + // Format response + const result = imageFiles.map(imgPath => ({ + img_path: imgPath + })); + + return NextResponse.json({ images: result }); + } catch (error) { + console.error('Error finding images:', error); + return NextResponse.json( + { error: 'Failed to process request' }, + { status: 500 } + ); + } +} + +/** + * Recursively finds all image files in a directory and its subdirectories + * @param dir Directory to search + * @returns Array of absolute paths to image files + */ +function findImagesRecursively(dir: string): string[] { + const imageExtensions = ['.png', '.jpg', '.jpeg', '.webp']; + let results: string[] = []; + + const items = fs.readdirSync(dir); + + for (const item of items) { + const itemPath = path.join(dir, item); + const stat = fs.statSync(itemPath); + + if (stat.isDirectory()) { + // If it's a directory, recursively search it + results = results.concat(findImagesRecursively(itemPath)); + } else { + // If it's a file, check if it's an image + const ext = path.extname(itemPath).toLowerCase(); + if (imageExtensions.includes(ext)) { + results.push(itemPath); + } + } + } + + return results; +} \ No newline at end of file diff --git a/ui/src/app/api/datasets/utils.ts b/ui/src/app/api/datasets/utils.ts new file mode 100644 index 00000000..88361636 --- /dev/null +++ b/ui/src/app/api/datasets/utils.ts @@ -0,0 +1,17 @@ +import { PrismaClient } from '@prisma/client'; +import { defaultDatasetsFolder } from '@/paths'; + +const prisma = new PrismaClient(); + +export const getDatasetsRoot = async () => { + let row = await prisma.settings.findFirst({ + where: { + key: 'DATASETS_FOLDER', + }, + }); + let datasetsPath = defaultDatasetsFolder; + if (row?.value && row.value !== '') { + datasetsPath = row.value; + } + return datasetsPath; +}; \ No newline at end of file diff --git a/ui/src/app/api/img/[...imagePath]/route.ts b/ui/src/app/api/img/[...imagePath]/route.ts new file mode 100644 index 00000000..875cdd1a --- /dev/null +++ b/ui/src/app/api/img/[...imagePath]/route.ts @@ -0,0 +1,66 @@ +// src/app/api/img/[imagePath]/route.ts +import { NextRequest, NextResponse } from 'next/server'; +import fs from 'fs'; +import path from 'path'; +import { getDatasetsRoot } from '@/app/api/datasets/utils'; + +export async function GET(request: NextRequest, { params }: { params: { imagePath: string } }) { + const { imagePath } = await params; + try { + // Decode the path + const filepath = decodeURIComponent(imagePath); + console.log('Serving image:', filepath); + + // Get allowed directories + const allowedDir = await getDatasetsRoot(); + + // Security check: Ensure path is in allowed directory + const isAllowed = filepath.startsWith(allowedDir) && !filepath.includes('..'); + + if (!isAllowed) { + console.warn(`Access denied: ${filepath} not in ${allowedDir}`); + return new NextResponse('Access denied', { status: 403 }); + } + + // Check if file exists + if (!fs.existsSync(filepath)) { + console.warn(`File not found: ${filepath}`); + return new NextResponse('File not found', { status: 404 }); + } + + // Get file info + const stat = fs.statSync(filepath); + if (!stat.isFile()) { + return new NextResponse('Not a file', { status: 400 }); + } + + // Determine content type + const ext = path.extname(filepath).toLowerCase(); + const contentTypeMap: { [key: string]: string } = { + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.png': 'image/png', + '.gif': 'image/gif', + '.webp': 'image/webp', + '.svg': 'image/svg+xml', + '.bmp': 'image/bmp', + }; + + const contentType = contentTypeMap[ext] || 'application/octet-stream'; + + // Read file as buffer + const fileBuffer = fs.readFileSync(filepath); + + // Return file with appropriate headers + return new NextResponse(fileBuffer, { + headers: { + 'Content-Type': contentType, + 'Content-Length': String(stat.size), + 'Cache-Control': 'public, max-age=86400', + }, + }); + } catch (error) { + console.error('Error serving image:', error); + return new NextResponse('Internal Server Error', { status: 500 }); + } +} diff --git a/ui/src/app/api/settings/route.ts b/ui/src/app/api/settings/route.ts index 8062afde..458c53c5 100644 --- a/ui/src/app/api/settings/route.ts +++ b/ui/src/app/api/settings/route.ts @@ -1,6 +1,6 @@ import { NextResponse } from 'next/server'; import { PrismaClient } from '@prisma/client'; -import { defaultTrainFolder } from '@/paths'; +import { defaultTrainFolder, defaultDatasetsFolder } from '@/paths'; const prisma = new PrismaClient(); @@ -15,6 +15,10 @@ export async function GET() { if (!settingsObject.TRAINING_FOLDER || settingsObject.TRAINING_FOLDER === '') { settingsObject.TRAINING_FOLDER = defaultTrainFolder; } + // if DATASETS_FOLDER is not set, use default + if (!settingsObject.DATASETS_FOLDER || settingsObject.DATASETS_FOLDER === '') { + settingsObject.DATASETS_FOLDER = defaultDatasetsFolder; + } return NextResponse.json(settingsObject); } catch (error) { return NextResponse.json({ error: 'Failed to fetch settings' }, { status: 500 }); @@ -24,7 +28,7 @@ export async function GET() { export async function POST(request: Request) { try { const body = await request.json(); - const { HF_TOKEN, TRAINING_FOLDER } = body; + const { HF_TOKEN, TRAINING_FOLDER, DATASETS_FOLDER } = body; // Upsert both settings await Promise.all([ @@ -38,6 +42,11 @@ export async function POST(request: Request) { update: { value: TRAINING_FOLDER }, create: { key: 'TRAINING_FOLDER', value: TRAINING_FOLDER }, }), + prisma.settings.upsert({ + where: { key: 'DATASETS_FOLDER' }, + update: { value: DATASETS_FOLDER }, + create: { key: 'DATASETS_FOLDER', value: DATASETS_FOLDER }, + }), ]); return NextResponse.json({ success: true }); diff --git a/ui/src/app/dashboard/page.tsx b/ui/src/app/dashboard/page.tsx index fd05095f..94394919 100644 --- a/ui/src/app/dashboard/page.tsx +++ b/ui/src/app/dashboard/page.tsx @@ -5,7 +5,7 @@ import GpuMonitor from '@/components/GPUMonitor'; export default function Dashboard() { return (
Loading...
} + {status === 'error' &&Error fetching images
} + {status === 'success' && ( +No images found
} + {imgList.map(img => ( +Loading...
} + {status === 'error' &&Error fetching datasets
} + {status === 'success' && ( +No datasets found
} + {datasets.map((dataset: string) => ( + + {dataset} + + ))} +