improved thumbnail loading

This commit is contained in:
subnub
2024-12-30 04:57:56 -05:00
parent e65ac93edb
commit 33a83d14df
10 changed files with 61 additions and 123 deletions
+1 -3
View File
@@ -49,9 +49,7 @@ class FileController {
const user = req.user;
const id = req.params.id;
const bufferData = await this.chunkService.getThumbnail(user, id);
res.send(bufferData);
await this.chunkService.getThumbnail(user, id, res);
} catch (e: unknown) {
next(e);
}
@@ -348,9 +348,8 @@ class StorageService {
return { archive };
};
getThumbnail = async (user: UserInterface, id: string) => {
const bufferData = await getThumbnailData(id, user);
return bufferData;
getThumbnail = async (user: UserInterface, id: string, res: Response) => {
await getThumbnailData(res, id, user);
};
getFullThumbnail = async (
@@ -1,26 +1,27 @@
import { EventEmitter } from "stream";
import { UserInterface } from "../../../models/user-model";
import { Response } from "express";
import ForbiddenError from "../../../utils/ForbiddenError";
import ThumbnailDB from "../../../db/mongoDB/thumbnailDB";
import NotFoundError from "../../../utils/NotFoundError";
import crypto from "crypto";
import { createGenericParams } from "./storageHelper";
import { getStorageActions } from "../actions/helper-actions";
import streamToBuffer from "../../../utils/streamToBuffer";
import ThumbnailDB from "../../../db/mongoDB/thumbnailDB";
const thumbnailDB = new ThumbnailDB();
const storageActions = getStorageActions();
const processData = (thumbnailID: string, user: UserInterface) => {
const proccessData = (
res: Response,
thumbnailID: string,
user: UserInterface
) => {
const eventEmitter = new EventEmitter();
const processThumbnail = async () => {
const processFile = async () => {
try {
const password = user.getEncryptionKey();
if (!password) throw new ForbiddenError("Invalid Encryption Key");
const thumbnail = await thumbnailDB.getThumbnailInfo(
user._id.toString(),
thumbnailID
@@ -28,11 +29,11 @@ const processData = (thumbnailID: string, user: UserInterface) => {
if (!thumbnail) throw new NotFoundError("Thumbnail Not Found");
const iv = thumbnail.IV;
const password = user.getEncryptionKey();
const CIPHER_KEY = crypto.createHash("sha256").update(password).digest();
if (!password) throw new ForbiddenError("Invalid Encryption Key");
const decipher = crypto.createDecipheriv("aes256", CIPHER_KEY, iv);
const IV = thumbnail.IV;
const readStreamParams = createGenericParams({
filePath: thumbnail.path,
@@ -41,6 +42,10 @@ const processData = (thumbnailID: string, user: UserInterface) => {
const readStream = storageActions.createReadStream(readStreamParams);
const CIPHER_KEY = crypto.createHash("sha256").update(password).digest();
const decipher = crypto.createDecipheriv("aes256", CIPHER_KEY, IV);
decipher.on("error", (e: Error) => {
eventEmitter.emit("error", e);
});
@@ -49,22 +54,33 @@ const processData = (thumbnailID: string, user: UserInterface) => {
eventEmitter.emit("error", e);
});
const bufferData = await streamToBuffer(readStream.pipe(decipher));
res.on("error", (e: Error) => {
eventEmitter.emit("error", e);
});
eventEmitter.emit("finish", bufferData);
readStream
.pipe(decipher)
.pipe(res)
.on("finish", () => {
eventEmitter.emit("finish");
});
} catch (e) {
eventEmitter.emit("error", e);
}
};
processThumbnail();
processFile();
return eventEmitter;
};
const getThumbnailData = (thumbnailID: string, user: UserInterface) => {
const getThumbnailData = (
res: Response,
thumbnailID: string,
user: UserInterface
) => {
return new Promise((resolve, reject) => {
const eventEmitter = processData(thumbnailID, user);
const eventEmitter = proccessData(res, thumbnailID, user);
eventEmitter.on("finish", (data) => {
resolve(data);
});
-26
View File
@@ -79,32 +79,6 @@ export const downloadFileAPI = async (fileID: string) => {
link.click();
};
export const getFileThumbnailAPI = async (thumbnailID: string) => {
const url = `${getBackendURL()}/file-service/thumbnail/${thumbnailID}`;
const response = await axios.get(url, {
responseType: "arraybuffer",
});
const imgFile = new Blob([response.data]);
const imgUrl = URL.createObjectURL(imgFile);
return imgUrl;
};
export const getFileFullThumbnailAPI = async (fileID: string) => {
const url = `${getBackendURL()}/file-service/full-thumbnail/${fileID}`;
const response = await axios.get(url, {
responseType: "arraybuffer",
});
const imgFile = new Blob([response.data]);
const imgUrl = URL.createObjectURL(imgFile);
return imgUrl;
};
export const getVideoTokenAPI = async () => {
const response = await axios.get(
"/file-service/download/access-token-stream-video"
+5 -4
View File
@@ -3,9 +3,7 @@ import moment from "moment";
import React, { memo, useMemo, useRef } from "react";
import ContextMenu from "../ContextMenu/ContextMenu";
import { useContextMenu } from "../../hooks/contextMenu";
import { useSelector } from "react-redux";
import classNames from "classnames";
import { useThumbnail } from "../../hooks/files";
import { getFileColor, getFileExtension } from "../../utils/files";
import bytes from "bytes";
import { useAppDispatch, useAppSelector } from "../../hooks/store";
@@ -14,6 +12,7 @@ import PlayButtonIcon from "../../icons/PlayIcon";
import { setPopupSelect } from "../../reducers/selected";
import ActionsIcon from "../../icons/ActionsIcon";
import { FileInterface } from "../../types/file";
import getBackendURL from "../../utils/getBackendURL";
interface FileItemProps {
file: FileInterface;
@@ -34,7 +33,9 @@ const FileItem: React.FC<FileItemProps> = memo((props) => {
(state) => state.selected.multiSelectMode
);
const listView = useAppSelector((state) => state.general.listView);
const { data: thumbnail } = useThumbnail(file.metadata.thumbnailID);
const thumbnail = `${getBackendURL()}/file-service/thumbnail/${
file.metadata.thumbnailID
}`;
const dispatch = useAppDispatch();
const lastSelected = useRef(0);
const {
@@ -203,7 +204,7 @@ const FileItem: React.FC<FileItemProps> = memo((props) => {
}
)}
>
{!!thumbnail ? (
{file.metadata.hasThumbnail ? (
<div className="w-full min-h-[88px] max-h-[88px] h-full flex">
<img
className="object-cover w-full disable-force-touch"
+4 -2
View File
@@ -1,6 +1,5 @@
import React, { memo, useRef } from "react";
import { FileInterface } from "../../types/file";
import { useThumbnail } from "../../hooks/files";
import { useAppDispatch, useAppSelector } from "../../hooks/store";
import {
setMainSelect,
@@ -12,6 +11,7 @@ import classNames from "classnames";
import { useContextMenu } from "../../hooks/contextMenu";
import ContextMenu from "../ContextMenu/ContextMenu";
import PlayButtonIcon from "../../icons/PlayIcon";
import getBackendURL from "../../utils/getBackendURL";
type MediaItemType = {
file: FileInterface;
@@ -41,7 +41,9 @@ const MediaItem: React.FC<MediaItemType> = memo(({ file }) => {
} = useContextMenu();
const lastSelected = useRef(0);
const dispatch = useAppDispatch();
const { data: thumbnail } = useThumbnail(file.metadata.thumbnailID);
const thumbnail = `${getBackendURL()}/file-service/thumbnail/${
file.metadata.thumbnailID
}`;
// TODO: See if we can memoize this and remove any
const mediaItemClick = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
@@ -1,10 +1,6 @@
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useAppDispatch, useAppSelector } from "../../hooks/store";
import {
deleteVideoTokenAPI,
getFileFullThumbnailAPI,
getVideoTokenAPI,
} from "../../api/filesAPI";
import { deleteVideoTokenAPI, getVideoTokenAPI } from "../../api/filesAPI";
import CloseIcon from "../../icons/CloseIcon";
import ActionsIcon from "../../icons/ActionsIcon";
import { useContextMenu } from "../../hooks/contextMenu";
@@ -12,7 +8,7 @@ import ContextMenu from "../ContextMenu/ContextMenu";
import { resetPopupSelect, setPopupSelect } from "../../reducers/selected";
import CircleLeftIcon from "../../icons/CircleLeftIcon";
import CircleRightIcon from "../../icons/CircleRightIcon";
import { useFiles, useFullThumbnail, useQuickFiles } from "../../hooks/files";
import { useFiles, useQuickFiles } from "../../hooks/files";
import { FileInterface } from "../../types/file";
import { InfiniteData } from "react-query";
import { getFileColor, getFileExtension } from "../../utils/files";
@@ -28,12 +24,14 @@ const PhotoViewerPopup: React.FC<PhotoViewerPopupProps> = memo((props) => {
const { file } = props;
const [video, setVideo] = useState("");
const [isVideoLoading, setIsVideoLoading] = useState(false);
const [isThumbnailLoading, setIsThumbnailLoading] = useState(
file.metadata.hasThumbnail && !file.metadata.isVideo
);
const videoRef = useRef<HTMLVideoElement | null>(null);
const type = useAppSelector((state) => state.selected.popupModal.type)!;
const { data: thumbnail, isLoading: isThumbnailLoading } = useFullThumbnail(
file._id,
file.metadata.isVideo
);
const thumbnailURL = `${getBackendURL()}/file-service/full-thumbnail/${
file._id
}`;
const finalLastPageLoaded = useRef(false);
const loadingNextPage = useRef(false);
const { data: quickFiles } = useQuickFiles(false);
@@ -287,11 +285,13 @@ const PhotoViewerPopup: React.FC<PhotoViewerPopupProps> = memo((props) => {
/>
</div>
<div className="max-w-[80vw] max-h-[80vh] flex justify-center items-center z-10">
{(isVideoLoading || isThumbnailLoading) && <Spinner />}
{!file.metadata.isVideo && !isThumbnailLoading && (
{isThumbnailLoading && <Spinner />}
{!file.metadata.isVideo && (
<img
src={thumbnail}
src={thumbnailURL}
className="max-w-full max-h-full object-contain select-none"
onLoad={() => setIsThumbnailLoading(false)}
onError={() => setIsThumbnailLoading(false)}
/>
)}
{file.metadata.isVideo && !isVideoLoading && (
@@ -4,7 +4,6 @@ import React, { memo, useMemo, useRef } from "react";
import ContextMenu from "../ContextMenu/ContextMenu";
import classNames from "classnames";
import { getFileColor, getFileExtension } from "../../utils/files";
import { useThumbnail } from "../../hooks/files";
import { useContextMenu } from "../../hooks/contextMenu";
import mobilecheck from "../../utils/mobileCheck";
import { FileInterface } from "../../types/file";
@@ -15,6 +14,7 @@ import {
setPopupSelect,
} from "../../reducers/selected";
import PlayButtonIcon from "../../icons/PlayIcon";
import getBackendURL from "../../utils/getBackendURL";
interface QuickAccessItemProps {
file: FileInterface;
@@ -34,7 +34,9 @@ const QuickAccessItem = memo((props: QuickAccessItemProps) => {
const multiSelectMode = useAppSelector(
(state) => state.selected.multiSelectMode
);
const { data: thumbnail } = useThumbnail(file.metadata.thumbnailID, true);
const thumbnail = `${getBackendURL()}/file-service/thumbnail/${
file.metadata.thumbnailID
}`;
const dispatch = useAppDispatch();
const lastSelected = useRef(0);
@@ -10,7 +10,6 @@ import { useNavigate } from "react-router-dom";
import { useAppSelector } from "../../hooks/store";
import { resetSelected, setPopupSelect } from "../../reducers/selected";
import { useUtils } from "../../hooks/utils";
import { useThumbnail } from "../../hooks/files";
import CloseIcon from "../../icons/CloseIcon";
import FileDetailsIcon from "../../icons/FileDetailsIcon";
import ActionsIcon from "../../icons/ActionsIcon";
-53
View File
@@ -8,8 +8,6 @@ import {
} from "react-query";
import { useParams } from "react-router-dom";
import {
getFileFullThumbnailAPI,
getFileThumbnailAPI,
getFilesListAPI,
getQuickFilesListAPI,
getSuggestedListAPI,
@@ -137,57 +135,6 @@ export const useQuickFilesClient = () => {
return { ...quickFilesReactClientQuery, invalidateQuickFilesCache };
};
export const useThumbnail = (
thumbnailID: string | undefined,
isQuickFile?: boolean
) => {
const listView = useAppSelector((state) => state.general.listView);
const loadThumbnailsDisabled = useAppSelector(
(state) => state.general.loadThumbnailsDisabled
);
const { isMedia } = useUtils();
const disabled =
(listView && !isMedia && !isQuickFile) ||
(loadThumbnailsDisabled && !isMedia);
const thumbnailQuery = useQuery(
["thumbnail", { thumbnailID }],
() => {
if (thumbnailID) {
return getFileThumbnailAPI(thumbnailID);
} else {
return undefined;
}
},
{
enabled: !!thumbnailID && !disabled,
refetchOnWindowFocus: false,
refetchOnMount: false,
keepPreviousData: true,
}
);
return thumbnailQuery;
};
export const useFullThumbnail = (fileID: string, isVideo: boolean) => {
console.log("usefullthumbnail", fileID, isVideo);
const thumbnailQuery = useQuery(
["full-thumbnail", { fileID }],
() => {
if (!isVideo) {
return getFileFullThumbnailAPI(fileID);
} else {
return undefined;
}
},
{
enabled: !isVideo,
refetchOnWindowFocus: false,
refetchOnMount: false,
}
);
return thumbnailQuery;
};
export const useSearchSuggestions = (searchText: string) => {
const { isTrash, isMedia } = useUtils();
const searchQuery = useQuery(