diff --git a/Dockerfile b/Dockerfile
index 365b9d2..5992f13 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -25,6 +25,8 @@ RUN mkdir built && \
FROM node:16.14.2-alpine
+ENV NODE_ENV=production
+
COPY --from=client-builder --chown=node:node /usr/src/app/picture-gallery-client/built /usr/src/app/picture-gallery-client/
COPY --from=server-builder --chown=node:node /usr/src/app/picture-gallery-server/built /usr/src/app/picture-gallery-server/
diff --git a/picture-gallery-client/src/ImageGallery/FolderGallery.tsx b/picture-gallery-client/src/ImageGallery/FolderGallery.tsx
new file mode 100644
index 0000000..bff407a
--- /dev/null
+++ b/picture-gallery-client/src/ImageGallery/FolderGallery.tsx
@@ -0,0 +1,49 @@
+import React from "react";
+import { FolderPreview } from "./models";
+import { useColumns } from "../util/responsive";
+import {
+ Chip,
+ ImageList,
+ ImageListItem,
+ ImageListItemBar,
+} from "@mui/material";
+import { Link } from "react-router-dom";
+
+export const FolderGallery = ({ folders }: { folders: FolderPreview[] }) => {
+ const columns = useColumns();
+
+ return (
+
+ {folders.map((folder) => (
+
+ {/* Link and image styling taken from https://github.com/mui/material-ui/issues/22597 */}
+
+
+
+ }
+ />
+
+
+ ))}
+
+ );
+};
diff --git a/picture-gallery-client/src/ImageGallery/ImageGallery.tsx b/picture-gallery-client/src/ImageGallery/ImageGallery.tsx
index c925ae2..2eb838a 100644
--- a/picture-gallery-client/src/ImageGallery/ImageGallery.tsx
+++ b/picture-gallery-client/src/ImageGallery/ImageGallery.tsx
@@ -9,21 +9,15 @@ import Thumbnails from "yet-another-react-lightbox/plugins/thumbnails";
import "yet-another-react-lightbox/plugins/thumbnails.css";
import Zoom from "yet-another-react-lightbox/plugins/zoom";
import { ImageList, ImageListItem } from "@mui/material";
+import { useColumns } from "../util/responsive";
-function ImageGallery({ images }: { images: ImageWithThumbnail[] }) {
+export const ImageGallery = ({ images }: { images: ImageWithThumbnail[] }) => {
const [index, setIndex] = useState(-1);
-
- if (images.length === 0) {
- return (
-
- No images available. You may want to add images in your root directory.
-
- );
- }
+ const columns = useColumns();
return (
<>
-
+
{images.map((item, index) => (
>
);
-}
-
-export default ImageGallery;
+};
diff --git a/picture-gallery-client/src/ImageGallery/models.ts b/picture-gallery-client/src/ImageGallery/models.ts
index 09d8e09..5921a09 100644
--- a/picture-gallery-client/src/ImageGallery/models.ts
+++ b/picture-gallery-client/src/ImageGallery/models.ts
@@ -7,6 +7,13 @@ export interface Folders {
children: Folders[];
}
+export interface FolderPreview {
+ name: string;
+ fullPath: string;
+ numberOfFiles: number;
+ imagePreviewSrc: string | undefined;
+}
+
export interface ImageWithThumbnail extends Slide {
thumbnail: string;
}
diff --git a/picture-gallery-client/src/ImageGalleryLayout.tsx b/picture-gallery-client/src/ImageGalleryLayout.tsx
index ad23cde..e195afd 100644
--- a/picture-gallery-client/src/ImageGalleryLayout.tsx
+++ b/picture-gallery-client/src/ImageGalleryLayout.tsx
@@ -2,12 +2,18 @@ import React, { useEffect, useState } from "react";
import CssBaseline from "@mui/material/CssBaseline";
import Box from "@mui/material/Box";
import { useLocation, useNavigate } from "react-router-dom";
-import { Folders, ImageWithThumbnail } from "./ImageGallery/models";
+import {
+ FolderPreview,
+ Folders,
+ ImageWithThumbnail,
+} from "./ImageGallery/models";
import { ImageGalleryAppBar } from "./ImageGallery/ImageGalleryAppBar";
import { ImageGalleryDrawer } from "./ImageGallery/ImageGalleryDrawer";
-import ImageGallery from "./ImageGallery/ImageGallery";
+import { ImageGallery } from "./ImageGallery/ImageGallery";
import { Spinner } from "./ImageGallery/Spinner";
import Toolbar from "@mui/material/Toolbar";
+import { Chip, Divider } from "@mui/material";
+import { FolderGallery } from "./ImageGallery/FolderGallery";
const drawerWidth = 240;
export const smallScreenMediaQuery = `(min-width:${drawerWidth * 3}px)`;
@@ -19,7 +25,9 @@ function ImageGalleryLayout() {
const [images, setImages] = useState([]);
const [folders, setFolders] = useState(undefined);
-
+ const [foldersPreview, setFoldersPreview] = useState<
+ FolderPreview[] | undefined
+ >([]);
const location = useLocation();
const navigate = useNavigate();
@@ -28,6 +36,7 @@ function ImageGalleryLayout() {
}
useEffect(() => {
+ setFoldersPreview(undefined);
setImages([]);
setError(false);
setImagesLoaded(false);
@@ -49,6 +58,15 @@ function ImageGalleryLayout() {
setImagesLoaded(true);
}
});
+ fetch(`/folderspreview${location.pathname}`, {
+ headers: {
+ Accept: "application/json",
+ },
+ })
+ .then((res) => res.json())
+ .then((data) => {
+ setFoldersPreview(data);
+ });
}, [location.pathname]);
useEffect(() => {
@@ -85,7 +103,32 @@ function ImageGalleryLayout() {
/>
- {imagesLoaded ? : }
+ {!imagesLoaded || !foldersPreview ? (
+
+ ) : (
+ <>
+ {foldersPreview.length > 0 && (
+ <>
+
+
+
+
+ >
+ )}
+ {images.length > 0 && foldersPreview.length > 0 && (
+
+
+
+ )}
+ {images.length > 0 && }
+ {images.length == 0 && foldersPreview.length == 0 && (
+
+ No images available. You may want to add images in your root
+ directory.
+
+ )}
+ >
+ )}
);
diff --git a/picture-gallery-client/src/util/responsive.ts b/picture-gallery-client/src/util/responsive.ts
new file mode 100644
index 0000000..7f66927
--- /dev/null
+++ b/picture-gallery-client/src/util/responsive.ts
@@ -0,0 +1,12 @@
+import useMediaQuery from "@mui/material/useMediaQuery";
+
+const breakpoints = Object.freeze([1200, 600, 300, 0]);
+
+// TODO: never can be one column at the moment
+export function useColumns(): number {
+ const values = [5, 4, 3, 2];
+ const index = breakpoints
+ .map((b) => useMediaQuery(`(min-width:${b}px)`))
+ .findIndex((a) => a);
+ return Math.max(values[Math.max(index, 0)], 1);
+}
diff --git a/picture-gallery-client/vite.config.ts b/picture-gallery-client/vite.config.ts
index ef943f1..2ea3d51 100644
--- a/picture-gallery-client/vite.config.ts
+++ b/picture-gallery-client/vite.config.ts
@@ -31,6 +31,11 @@ export default defineConfig(() => {
changeOrigin: true,
secure: false,
},
+ "/folderspreview": {
+ target: "http://localhost:3001",
+ changeOrigin: true,
+ secure: false,
+ },
"/staticImages": {
target: "http://localhost:3001",
changeOrigin: true,
diff --git a/picture-gallery-server/src/controller/common.ts b/picture-gallery-server/src/controller/common.ts
new file mode 100644
index 0000000..a470d39
--- /dev/null
+++ b/picture-gallery-server/src/controller/common.ts
@@ -0,0 +1,29 @@
+import express from "express";
+import path from "path";
+import fs, { Dirent } from "fs";
+import { thumbnailPath, thumbnailPublicPath } from "../paths";
+
+export const getRequestedPath = (req: express.Request): string =>
+ req.params[1] === undefined || req.params[1] === "/" ? "" : req.params[1];
+
+export const readThumbnails = (requestedPath: string): string[] => {
+ const requestedThumbnailPath = path.posix.join(
+ thumbnailPublicPath,
+ requestedPath,
+ );
+ return fs.existsSync(requestedThumbnailPath)
+ ? fs.readdirSync(requestedThumbnailPath)
+ : [];
+};
+
+export const getSrc = (requestedPath: string, f: Dirent): string =>
+ path.posix.join("/staticImages", requestedPath, f.name);
+
+export const getThumbnail = (
+ thumbnailExists: boolean,
+ requestedPath: string,
+ f: Dirent,
+): string =>
+ thumbnailExists
+ ? path.posix.join("/staticImages", thumbnailPath, requestedPath, f.name)
+ : getSrc(requestedPath, f);
diff --git a/picture-gallery-server/src/controller/directories.ts b/picture-gallery-server/src/controller/directories.ts
new file mode 100644
index 0000000..72e8d2b
--- /dev/null
+++ b/picture-gallery-server/src/controller/directories.ts
@@ -0,0 +1,95 @@
+import fs from "fs";
+import * as path from "path";
+import express from "express";
+import { a, FolderPreview, Folders } from "../models";
+import { publicPath, thumbnailPath, thumbnailPublicPath } from "../paths";
+import { securityValidation } from "./securityChecks";
+import { getRequestedPath, getThumbnail } from "./common";
+import { consoleLogger } from "../logging";
+
+export const walk = async (dirPath: string): Promise => {
+ const dirEnts = fs.readdirSync(path.posix.join(publicPath, dirPath), {
+ withFileTypes: true,
+ });
+
+ const numberOfFiles = dirEnts.filter((f) => f.isFile()).length;
+
+ const children = await Promise.all(
+ dirEnts
+ .filter((d) => d.isDirectory())
+ .filter((d) => !d.name.includes(thumbnailPath.substring(1)))
+ .map((d) => walk(path.posix.join(dirPath, d.name))),
+ );
+
+ return {
+ name: path.basename(dirPath) || "Home",
+ fullPath: dirPath,
+ numberOfFiles,
+ children,
+ };
+};
+
+const getFirstImageInFolder = (dirPath: string): string | undefined => {
+ const dirs = [dirPath];
+ while (dirs.length > 0) {
+ const curPath = dirs.shift();
+ const dirContent = fs.readdirSync(path.posix.join(publicPath, curPath), {
+ withFileTypes: true,
+ });
+ for (let i = 0; i < dirContent.length; i += 1) {
+ const content = dirContent[i];
+ const filePath = path.posix.join(curPath, content.name);
+ if (content.isFile()) {
+ return getThumbnail(
+ fs.existsSync(path.posix.join(thumbnailPublicPath, filePath)),
+ curPath,
+ content,
+ );
+ }
+ if (content.isDirectory()) {
+ dirs.push(filePath);
+ }
+ }
+ }
+ return undefined;
+};
+
+const getNumberOfFiles = (dirPath: string): number =>
+ fs
+ .readdirSync(path.posix.join(publicPath, dirPath), {
+ withFileTypes: true,
+ })
+ .filter((d) => d.isFile()).length;
+
+export const getFolderPreview = (
+ req: express.Request,
+ res: express.Response,
+) => {
+ const requestedPath = getRequestedPath(req);
+
+ try {
+ securityValidation(requestedPath);
+
+ const dirents = fs
+ .readdirSync(path.posix.join(publicPath, requestedPath), {
+ withFileTypes: true,
+ })
+ .filter((d) => d.isDirectory())
+ .filter((d) => !d.name.includes(thumbnailPath.substring(1)));
+
+ res.json(
+ dirents.map((dir) => {
+ const fullPath = path.posix.join(requestedPath, dir.name);
+ return a({
+ name: dir.name,
+ fullPath,
+ numberOfFiles: getNumberOfFiles(fullPath),
+ imagePreviewSrc: getFirstImageInFolder(fullPath),
+ });
+ }),
+ );
+ } catch (e) {
+ consoleLogger.warn(`Error when trying to access ${req.path}: ${e}`);
+ res.status(400).json({ message: `Path ${req.path} not accessible.` });
+ }
+};
diff --git a/picture-gallery-server/src/controller/images.ts b/picture-gallery-server/src/controller/images.ts
index f1dc909..614ef5a 100644
--- a/picture-gallery-server/src/controller/images.ts
+++ b/picture-gallery-server/src/controller/images.ts
@@ -3,48 +3,29 @@ import express from "express";
import sharp from "sharp";
import path from "path";
import natsort from "natsort";
-import { publicPath, thumbnailPath, thumbnailPublicPath } from "../paths";
+import { publicPath } from "../paths";
import { a, Folder, Image } from "../models";
import { createThumbnailAsyncForImage } from "../thumbnails";
import { consoleLogger } from "../logging";
import { securityValidation } from "./securityChecks";
+import {
+ getRequestedPath,
+ getSrc,
+ getThumbnail,
+ readThumbnails,
+} from "./common";
const notEmpty = (
- value: TValue | void | null | undefined
+ value: TValue | void | null | undefined,
): value is TValue => {
return value !== null && value !== undefined;
};
-const getRequestedPath = (req: express.Request): string =>
- req.params[1] === undefined || req.params[1] === "/" ? "" : req.params[1];
-
-const readThumbnails = (requestedPath: string): string[] => {
- const requestedThumbnailPath = path.posix.join(
- thumbnailPublicPath,
- requestedPath
- );
- return fs.existsSync(requestedThumbnailPath)
- ? fs.readdirSync(requestedThumbnailPath)
- : [];
-};
-
-const getSrc = (requestedPath: string, f: Dirent): string =>
- path.posix.join("/staticImages", requestedPath, f.name);
-
-const getThumbnail = (
- thumbnailExists: boolean,
- requestedPath: string,
- f: Dirent
-): string =>
- thumbnailExists
- ? path.posix.join("/staticImages", thumbnailPath, requestedPath, f.name)
- : getSrc(requestedPath, f);
-
const toImage = (
metadata: sharp.Metadata,
thumbnailExists: boolean,
requestedPath: string,
- f: Dirent
+ f: Dirent,
): Image => {
const widthAndHeightSwap = metadata.orientation > 4; // see https://exiftool.org/TagNames/EXIF.html
return a({
@@ -58,7 +39,7 @@ const toImage = (
const getImagesToBeLoaded = (
dirents: Dirent[],
thumbnails: string[],
- requestedPath: string
+ requestedPath: string,
): Promise[] =>
dirents
.filter((f) => f.isFile())
@@ -73,22 +54,22 @@ const getImagesToBeLoaded = (
return sharp(path.posix.join(publicPath, requestedPath, f.name))
.metadata()
.then((metadata) =>
- toImage(metadata, thumbnailExists, requestedPath, f)
+ toImage(metadata, thumbnailExists, requestedPath, f),
)
.catch((err) => {
consoleLogger.error(
`Reading metadata from ${path.posix.join(
publicPath,
requestedPath,
- f.name
- )} produced the following error: ${err.message}`
+ f.name,
+ )} produced the following error: ${err.message}`,
);
});
});
export const getImages = async (
req: express.Request,
- res: express.Response
+ res: express.Response,
) => {
const requestedPath = getRequestedPath(req);
@@ -103,7 +84,7 @@ export const getImages = async (
const imagesToBeLoaded = getImagesToBeLoaded(
dirents,
thumbnails,
- requestedPath
+ requestedPath,
);
const images = (await Promise.all(imagesToBeLoaded)).filter(notEmpty);
res.json(a({ images }));
diff --git a/picture-gallery-server/src/fsExtension.ts b/picture-gallery-server/src/fsExtension.ts
deleted file mode 100644
index 69f6fd0..0000000
--- a/picture-gallery-server/src/fsExtension.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import fs from "fs";
-import * as path from "path";
-import { Folders } from "./models";
-import { publicPath, thumbnailPath } from "./paths";
-
-export const walk = async (dirPath: string): Promise => {
- const dirEnts = fs.readdirSync(path.posix.join(publicPath, dirPath), {
- withFileTypes: true,
- });
-
- const numberOfFiles = dirEnts.filter((f) => f.isFile()).length;
-
- const children = await Promise.all(
- dirEnts
- .filter((d) => d.isDirectory())
- .filter((d) => !d.name.includes(thumbnailPath.substring(1)))
- .map((d) => walk(path.posix.join(dirPath, d.name))),
- );
-
- return {
- name: path.basename(dirPath) || "Home",
- fullPath: dirPath,
- numberOfFiles,
- children,
- };
-};
diff --git a/picture-gallery-server/src/models.ts b/picture-gallery-server/src/models.ts
index 92b7e45..802aee5 100644
--- a/picture-gallery-server/src/models.ts
+++ b/picture-gallery-server/src/models.ts
@@ -16,6 +16,13 @@ export interface Folders {
children: Folders[];
}
+export interface FolderPreview {
+ name: string;
+ fullPath: string;
+ numberOfFiles: number;
+ imagePreviewSrc: string | undefined;
+}
+
export const a = (v: T): T => {
return v;
};
diff --git a/picture-gallery-server/src/router/routerApi.ts b/picture-gallery-server/src/router/routerApi.ts
index 828f6c6..fcfb526 100644
--- a/picture-gallery-server/src/router/routerApi.ts
+++ b/picture-gallery-server/src/router/routerApi.ts
@@ -1,6 +1,6 @@
import express from "express";
import { getImages } from "../controller/images";
-import { walk } from "../fsExtension";
+import { getFolderPreview, walk } from "../controller/directories";
export const routerApi = express.Router();
@@ -9,3 +9,5 @@ routerApi.get(`/images(/*)?`, getImages);
routerApi.get("/directories", async (req, res) => {
res.json(await walk(""));
});
+
+routerApi.get("/folderspreview(/*)?", getFolderPreview);