improved thumbnail loading
This commit is contained in:
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user