Improve the loading and transfer speed of the dataset file lists
This commit is contained in:
@@ -1,8 +1,13 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import zlib from 'zlib';
|
||||||
|
import { promisify } from 'util';
|
||||||
import { getDatasetsRoot } from '@/server/settings';
|
import { getDatasetsRoot } from '@/server/settings';
|
||||||
|
|
||||||
|
const brotliCompress = promisify(zlib.brotliCompress);
|
||||||
|
const gzipCompress = promisify(zlib.gzip);
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
const datasetsPath = await getDatasetsRoot();
|
const datasetsPath = await getDatasetsRoot();
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
@@ -21,12 +26,33 @@ export async function POST(request: Request) {
|
|||||||
// Sort server-side so the client doesn't have to sort large lists
|
// Sort server-side so the client doesn't have to sort large lists
|
||||||
imageFiles.sort((a, b) => a.localeCompare(b));
|
imageFiles.sort((a, b) => a.localeCompare(b));
|
||||||
|
|
||||||
// Format response
|
// Send a single shared root plus each file's sub-path, rather than repeating the full
|
||||||
const result = imageFiles.map(imgPath => ({
|
// absolute path (and an "img_path" key) on every entry. The root carries the trailing
|
||||||
img_path: imgPath,
|
// OS separator so the client rebuilds the native path with a plain concat (root + subPath),
|
||||||
}));
|
// keeping correct paths on Windows, macOS, and Linux without any separator logic client-side.
|
||||||
|
const root = datasetFolder + path.sep;
|
||||||
|
const result = imageFiles.map(imgPath => imgPath.slice(root.length));
|
||||||
|
|
||||||
return NextResponse.json({ images: result });
|
// Compress the payload explicitly. Even after stripping the shared prefix these lists
|
||||||
|
// still gzip/brotli down substantially — a big win on slow connections and huge datasets.
|
||||||
|
// The browser decompresses transparently via Content-Encoding, so the client just parses JSON.
|
||||||
|
const json = JSON.stringify({ root, images: result });
|
||||||
|
const acceptEncoding = request.headers.get('accept-encoding') ?? '';
|
||||||
|
|
||||||
|
if (/\bbr\b/.test(acceptEncoding)) {
|
||||||
|
const body = await brotliCompress(json);
|
||||||
|
return new NextResponse(body as any, {
|
||||||
|
headers: { 'Content-Type': 'application/json', 'Content-Encoding': 'br' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (/\bgzip\b/.test(acceptEncoding)) {
|
||||||
|
const body = await gzipCompress(json);
|
||||||
|
return new NextResponse(body as any, {
|
||||||
|
headers: { 'Content-Type': 'application/json', 'Content-Encoding': 'gzip' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return new NextResponse(json, { headers: { 'Content-Type': 'application/json' } });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error finding images:', error);
|
console.error('Error finding images:', error);
|
||||||
return NextResponse.json({ error: 'Failed to process request' }, { status: 500 });
|
return NextResponse.json({ error: 'Failed to process request' }, { status: 500 });
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useState, use, useMemo, useCallback } from 'react';
|
import { useEffect, useState, use, useMemo, useCallback, useRef } from 'react';
|
||||||
import { LuImageOff, LuLoader, LuBan } from 'react-icons/lu';
|
import { LuImageOff, LuLoader, LuBan } from 'react-icons/lu';
|
||||||
import { FaChevronLeft } from 'react-icons/fa';
|
import { FaChevronLeft } from 'react-icons/fa';
|
||||||
import { VirtuosoGrid } from 'react-virtuoso';
|
import { VirtuosoGrid } from 'react-virtuoso';
|
||||||
@@ -29,20 +29,30 @@ export default function DatasetPage({ params }: { params: { datasetName: string
|
|||||||
const [scrollParent, setScrollParent] = useState<HTMLDivElement | null>(null);
|
const [scrollParent, setScrollParent] = useState<HTMLDivElement | null>(null);
|
||||||
const [captionBarHeight, setCaptionBarHeight] = useState(0);
|
const [captionBarHeight, setCaptionBarHeight] = useState(0);
|
||||||
const scrollParentCallback = useCallback((el: HTMLDivElement | null) => setScrollParent(el), []);
|
const scrollParentCallback = useCallback((el: HTMLDivElement | null) => setScrollParent(el), []);
|
||||||
|
const isRefreshingRef = useRef(false);
|
||||||
|
|
||||||
const refreshImageList = (dbName: string) => {
|
const refreshImageList = (dbName: string) => {
|
||||||
|
// Only allow one listImages request in flight at a time.
|
||||||
|
if (isRefreshingRef.current) return;
|
||||||
|
isRefreshingRef.current = true;
|
||||||
setStatus('loading');
|
setStatus('loading');
|
||||||
apiClient
|
apiClient
|
||||||
.post('/api/datasets/listImages', { datasetName: dbName })
|
.post('/api/datasets/listImages', { datasetName: dbName })
|
||||||
.then((res: any) => {
|
.then((res: any) => {
|
||||||
const data = res.data;
|
const data = res.data;
|
||||||
// Server already sorts; avoid the client-side sort that's expensive on large lists.
|
// Server sends a shared root (with trailing OS separator) + each file's sub-path to
|
||||||
setImgList(data.images);
|
// keep the payload small. Plain concat rebuilds the native absolute path on any OS.
|
||||||
|
// Server already sorts; avoid a client-side sort on large lists.
|
||||||
|
const root = data.root;
|
||||||
|
setImgList(data.images.map((subPath: string) => ({ img_path: root + subPath })));
|
||||||
setStatus('success');
|
setStatus('success');
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error('Error fetching images:', error);
|
console.error('Error fetching images:', error);
|
||||||
setStatus('error');
|
setStatus('error');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
isRefreshingRef.current = false;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
useOpenImagesModalOnDrag(datasetName, () => refreshImageList(datasetName));
|
useOpenImagesModalOnDrag(datasetName, () => refreshImageList(datasetName));
|
||||||
|
|||||||
Reference in New Issue
Block a user