diff --git a/backend/controllers/file-controller.ts b/backend/controllers/file-controller.ts index 6ab7e8e..3409658 100644 --- a/backend/controllers/file-controller.ts +++ b/backend/controllers/file-controller.ts @@ -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); } diff --git a/backend/services/chunk-service/chunk-service.ts b/backend/services/chunk-service/chunk-service.ts index 733ab79..1740081 100644 --- a/backend/services/chunk-service/chunk-service.ts +++ b/backend/services/chunk-service/chunk-service.ts @@ -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 ( diff --git a/backend/services/chunk-service/utils/getThumbnailData.ts b/backend/services/chunk-service/utils/getThumbnailData.ts index c2fe9a6..d288d0b 100644 --- a/backend/services/chunk-service/utils/getThumbnailData.ts +++ b/backend/services/chunk-service/utils/getThumbnailData.ts @@ -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); }); diff --git a/src/api/filesAPI.ts b/src/api/filesAPI.ts index 82ad65e..d56d0c4 100644 --- a/src/api/filesAPI.ts +++ b/src/api/filesAPI.ts @@ -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" diff --git a/src/components/FileItem/FileItem.tsx b/src/components/FileItem/FileItem.tsx index 217235f..2c5d0fd 100644 --- a/src/components/FileItem/FileItem.tsx +++ b/src/components/FileItem/FileItem.tsx @@ -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 = 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 = memo((props) => { } )} > - {!!thumbnail ? ( + {file.metadata.hasThumbnail ? (
= 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) => { diff --git a/src/components/PhotoViewerPopup/PhotoViewerPopup.tsx b/src/components/PhotoViewerPopup/PhotoViewerPopup.tsx index cb6d8ef..db7f152 100644 --- a/src/components/PhotoViewerPopup/PhotoViewerPopup.tsx +++ b/src/components/PhotoViewerPopup/PhotoViewerPopup.tsx @@ -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 = 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(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 = memo((props) => { />
- {(isVideoLoading || isThumbnailLoading) && } - {!file.metadata.isVideo && !isThumbnailLoading && ( + {isThumbnailLoading && } + {!file.metadata.isVideo && ( setIsThumbnailLoading(false)} + onError={() => setIsThumbnailLoading(false)} /> )} {file.metadata.isVideo && !isVideoLoading && ( diff --git a/src/components/QuickAccessItem/QuickAccessItem.tsx b/src/components/QuickAccessItem/QuickAccessItem.tsx index 6e93f7d..9f7d12a 100644 --- a/src/components/QuickAccessItem/QuickAccessItem.tsx +++ b/src/components/QuickAccessItem/QuickAccessItem.tsx @@ -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); diff --git a/src/components/RightSection/RightSection.tsx b/src/components/RightSection/RightSection.tsx index 5c8700f..72598cf 100644 --- a/src/components/RightSection/RightSection.tsx +++ b/src/components/RightSection/RightSection.tsx @@ -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"; diff --git a/src/hooks/files.ts b/src/hooks/files.ts index 1204173..ffb0a68 100644 --- a/src/hooks/files.ts +++ b/src/hooks/files.ts @@ -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(