add logic and apis for viewing sharedAlbum Thumbnails and files

This commit is contained in:
Abhinav 2022-01-17 09:46:12 +05:30
parent d7832a2e08
commit ecc2ce7093
6 changed files with 293 additions and 22 deletions

View file

@ -20,6 +20,11 @@ import { isPlaybackPossible } from 'utils/photoFrame';
import { PhotoList } from './PhotoList'; import { PhotoList } from './PhotoList';
import { SetFiles, SelectedState, Search, setSearchStats } from 'types/gallery'; import { SetFiles, SelectedState, Search, setSearchStats } from 'types/gallery';
import { FILE_TYPE } from 'constants/file'; import { FILE_TYPE } from 'constants/file';
import SharedCollectionDownloadManager from 'services/sharedCollectionDownloadManager';
import {
defaultSharedAlbumContext,
SharedAlbumContext,
} from 'pages/shared-album';
const Container = styled.div` const Container = styled.div`
display: block; display: block;
@ -89,6 +94,8 @@ const PhotoFrame = ({
const [fetching, setFetching] = useState<{ [k: number]: boolean }>({}); const [fetching, setFetching] = useState<{ [k: number]: boolean }>({});
const startTime = Date.now(); const startTime = Date.now();
const galleryContext = useContext(GalleryContext); const galleryContext = useContext(GalleryContext);
const sharedAlbumContext =
useContext(SharedAlbumContext) ?? defaultSharedAlbumContext;
const [rangeStart, setRangeStart] = useState(null); const [rangeStart, setRangeStart] = useState(null);
const [currentHover, setCurrentHover] = useState(null); const [currentHover, setCurrentHover] = useState(null);
const [isShiftKeyPressed, setIsShiftKeyPressed] = useState(false); const [isShiftKeyPressed, setIsShiftKeyPressed] = useState(false);
@ -365,7 +372,15 @@ const PhotoFrame = ({
if (galleryContext.thumbs.has(item.id)) { if (galleryContext.thumbs.has(item.id)) {
url = galleryContext.thumbs.get(item.id); url = galleryContext.thumbs.get(item.id);
} else { } else {
url = await DownloadManager.getThumbnail(item); if (sharedAlbumContext.accessedThroughSharedURL) {
url =
await SharedCollectionDownloadManager.getThumbnail(
item,
sharedAlbumContext.token
);
} else {
url = await DownloadManager.getThumbnail(item);
}
galleryContext.thumbs.set(item.id, url); galleryContext.thumbs.set(item.id, url);
} }
updateUrl(item.dataIndex)(url); updateUrl(item.dataIndex)(url);
@ -392,7 +407,15 @@ const PhotoFrame = ({
if (galleryContext.files.has(item.id)) { if (galleryContext.files.has(item.id)) {
url = galleryContext.files.get(item.id); url = galleryContext.files.get(item.id);
} else { } else {
url = await DownloadManager.getFile(item, true); if (sharedAlbumContext.accessedThroughSharedURL) {
url = await SharedCollectionDownloadManager.getFile(
item,
sharedAlbumContext.token,
true
);
} else {
url = await DownloadManager.getFile(item, true);
}
galleryContext.files.set(item.id, url); galleryContext.files.set(item.id, url);
} }
await updateSrcUrl(item.dataIndex, url); await updateSrcUrl(item.dataIndex, url);

View file

@ -6,6 +6,11 @@ import DownloadManager from 'services/downloadManager';
import useLongPress from 'utils/common/useLongPress'; import useLongPress from 'utils/common/useLongPress';
import { GalleryContext } from 'pages/gallery'; import { GalleryContext } from 'pages/gallery';
import { GAP_BTW_TILES } from 'constants/gallery'; import { GAP_BTW_TILES } from 'constants/gallery';
import {
defaultSharedAlbumContext,
SharedAlbumContext,
} from 'pages/shared-album';
import SharedCollectionDownloadManager from 'services/sharedCollectionDownloadManager';
interface IProps { interface IProps {
file: EnteFile; file: EnteFile;
@ -173,11 +178,22 @@ export default function PreviewCard(props: IProps) {
isInsSelectRange, isInsSelectRange,
} = props; } = props;
const isMounted = useRef(true); const isMounted = useRef(true);
const sharedAlbumContext =
useContext(SharedAlbumContext) ?? defaultSharedAlbumContext;
useLayoutEffect(() => { useLayoutEffect(() => {
if (file && !file.msrc) { if (file && !file.msrc) {
const main = async () => { const main = async () => {
try { try {
const url = await DownloadManager.getThumbnail(file); let url;
if (sharedAlbumContext.accessedThroughSharedURL) {
url =
await SharedCollectionDownloadManager.getThumbnail(
file,
sharedAlbumContext.token
);
} else {
url = await DownloadManager.getThumbnail(file);
}
if (isMounted.current) { if (isMounted.current) {
setImgSrc(url); setImgSrc(url);
thumbs.set(file.id, url); thumbs.set(file.id, url);

View file

@ -1,10 +1,20 @@
import { ALL_SECTION } from 'constants/collection'; import { ALL_SECTION } from 'constants/collection';
import PhotoFrame from 'components/PhotoFrame'; import PhotoFrame from 'components/PhotoFrame';
import React, { useEffect, useState } from 'react'; import React, { createContext, useEffect, useState } from 'react';
import { getSharedCollectionFiles } from 'services/sharedCollectionService'; import { getSharedCollectionFiles } from 'services/sharedCollectionService';
import { SharedAlbumContextType } from 'types/sharedAlbum';
export const defaultSharedAlbumContext: SharedAlbumContextType = {
token: null,
accessedThroughSharedURL: false,
};
export const SharedAlbumContext = createContext<SharedAlbumContextType>(
defaultSharedAlbumContext
);
export default function sharedAlbum() { export default function sharedAlbum() {
const [token, setToken] = useState(null); const [token, setToken] = useState<string>(null);
const [collectionKey, setCollectionKey] = useState(null); const [collectionKey, setCollectionKey] = useState(null);
const [files, setFiles] = useState([]); const [files, setFiles] = useState([]);
useEffect(() => { useEffect(() => {
@ -28,22 +38,29 @@ export default function sharedAlbum() {
}; };
return ( return (
<PhotoFrame <SharedAlbumContext.Provider
files={files} value={{
setFiles={setFiles} ...defaultSharedAlbumContext,
syncWithRemote={syncWithRemote} token,
favItemIds={null} accessedThroughSharedURL: true,
setSelected={() => null} }}>
selected={{ count: 0, collectionID: null }} <PhotoFrame
isFirstLoad={false} files={files}
openFileUploader={() => null} setFiles={setFiles}
loadingBar={null} syncWithRemote={syncWithRemote}
isInSearchMode={false} favItemIds={null}
search={{}} setSelected={() => null}
setSearchStats={() => null} selected={{ count: 0, collectionID: null }}
deleted={[]} isFirstLoad={false}
activeCollection={ALL_SECTION} openFileUploader={() => null}
isSharedCollection={true} loadingBar={null}
/> isInSearchMode={false}
search={{}}
setSearchStats={() => null}
deleted={[]}
activeCollection={ALL_SECTION}
isSharedCollection
/>
</SharedAlbumContext.Provider>
); );
} }

View file

@ -0,0 +1,191 @@
import {
getFileUrl,
getSharedAlbumFileUrl,
getSharedAlbumThumbnailUrl,
} from 'utils/common/apiUtil';
import CryptoWorker from 'utils/crypto';
import {
generateStreamFromArrayBuffer,
convertForPreview,
needsConversionForPreview,
} from 'utils/file';
import HTTPService from './HTTPService';
import { EnteFile } from 'types/file';
import { logError } from 'utils/sentry';
import { FILE_TYPE } from 'constants/file';
class SharedCollectionDownloadManager {
private fileObjectUrlPromise = new Map<string, Promise<string>>();
private thumbnailObjectUrlPromise = new Map<number, Promise<string>>();
public async getThumbnail(file: EnteFile, token: string) {
try {
if (!token) {
return null;
}
if (!this.thumbnailObjectUrlPromise.get(file.id)) {
const downloadPromise = async () => {
const thumbnailCache = await caches.open('thumbs');
const cacheResp: Response = await thumbnailCache.match(
file.id.toString()
);
if (cacheResp) {
return URL.createObjectURL(await cacheResp.blob());
}
const thumb = await this.downloadThumb(token, file);
const thumbBlob = new Blob([thumb]);
try {
await thumbnailCache.put(
file.id.toString(),
new Response(thumbBlob)
);
} catch (e) {
// TODO: handle storage full exception.
}
return URL.createObjectURL(thumbBlob);
};
this.thumbnailObjectUrlPromise.set(file.id, downloadPromise());
}
return await this.thumbnailObjectUrlPromise.get(file.id);
} catch (e) {
this.thumbnailObjectUrlPromise.delete(file.id);
logError(e, 'get preview Failed');
throw e;
}
}
downloadThumb = async (token: string, file: EnteFile) => {
const resp = await HTTPService.get(
getSharedAlbumThumbnailUrl(file.id),
null,
{ 'X-Auth-Access-Token': token },
{ responseType: 'arraybuffer' }
);
const worker = await new CryptoWorker();
const decrypted: Uint8Array = await worker.decryptThumbnail(
new Uint8Array(resp.data),
await worker.fromB64(file.thumbnail.decryptionHeader),
file.key
);
return decrypted;
};
getFile = async (file: EnteFile, token: string, forPreview = false) => {
const shouldBeConverted = forPreview && needsConversionForPreview(file);
const fileKey = shouldBeConverted
? `${file.id}_converted`
: `${file.id}`;
try {
const getFilePromise = async (convert: boolean) => {
const fileStream = await this.downloadFile(token, file);
let fileBlob = await new Response(fileStream).blob();
if (convert) {
fileBlob = await convertForPreview(file, fileBlob);
}
return URL.createObjectURL(fileBlob);
};
if (!this.fileObjectUrlPromise.get(fileKey)) {
this.fileObjectUrlPromise.set(
fileKey,
getFilePromise(shouldBeConverted)
);
}
const fileURL = await this.fileObjectUrlPromise.get(fileKey);
return fileURL;
} catch (e) {
this.fileObjectUrlPromise.delete(fileKey);
logError(e, 'Failed to get File');
throw e;
}
};
public async getCachedOriginalFile(file: EnteFile) {
return await this.fileObjectUrlPromise.get(file.id.toString());
}
async downloadFile(token: string, file: EnteFile) {
const worker = await new CryptoWorker();
if (!token) {
return null;
}
if (
file.metadata.fileType === FILE_TYPE.IMAGE ||
file.metadata.fileType === FILE_TYPE.LIVE_PHOTO
) {
const resp = await HTTPService.get(
getSharedAlbumFileUrl(file.id),
null,
{ 'X-Auth-Access-Token': token },
{ responseType: 'arraybuffer' }
);
const decrypted: any = await worker.decryptFile(
new Uint8Array(resp.data),
await worker.fromB64(file.file.decryptionHeader),
file.key
);
return generateStreamFromArrayBuffer(decrypted);
}
const resp = await fetch(getFileUrl(file.id), {
headers: {
'X-Auth-Token': token,
},
});
const reader = resp.body.getReader();
const stream = new ReadableStream({
async start(controller) {
const decryptionHeader = await worker.fromB64(
file.file.decryptionHeader
);
const fileKey = await worker.fromB64(file.key);
const { pullState, decryptionChunkSize } =
await worker.initDecryption(decryptionHeader, fileKey);
let data = new Uint8Array();
// The following function handles each data chunk
function push() {
// "done" is a Boolean and value a "Uint8Array"
reader.read().then(async ({ done, value }) => {
// Is there more data to read?
if (!done) {
const buffer = new Uint8Array(
data.byteLength + value.byteLength
);
buffer.set(new Uint8Array(data), 0);
buffer.set(new Uint8Array(value), data.byteLength);
if (buffer.length > decryptionChunkSize) {
const fileData = buffer.slice(
0,
decryptionChunkSize
);
const { decryptedData } =
await worker.decryptChunk(
fileData,
pullState
);
controller.enqueue(decryptedData);
data = buffer.slice(decryptionChunkSize);
} else {
data = buffer;
}
push();
} else {
if (data) {
const { decryptedData } =
await worker.decryptChunk(data, pullState);
controller.enqueue(decryptedData);
data = null;
}
controller.close();
}
});
}
push();
},
});
return stream;
}
}
export default new SharedCollectionDownloadManager();

View file

@ -0,0 +1,4 @@
export type SharedAlbumContextType = {
token: string;
accessedThroughSharedURL: boolean;
};

View file

@ -14,6 +14,16 @@ export const getFileUrl = (id: number) => {
return `https://files.ente.io/?fileID=${id}`; return `https://files.ente.io/?fileID=${id}`;
}; };
export const getSharedAlbumFileUrl = (id: number) => {
if (process.env.NEXT_PUBLIC_ENTE_ENDPOINT !== undefined) {
return (
`${process.env.NEXT_PUBLIC_ENTE_ENDPOINT}/public-collection/files/download/${id}` ??
'https://api.ente.io'
);
}
return `https://files.ente.io/?fileID=${id}`;
};
export const getThumbnailUrl = (id: number) => { export const getThumbnailUrl = (id: number) => {
if (process.env.NEXT_PUBLIC_ENTE_ENDPOINT !== undefined) { if (process.env.NEXT_PUBLIC_ENTE_ENDPOINT !== undefined) {
return ( return (
@ -24,6 +34,16 @@ export const getThumbnailUrl = (id: number) => {
return `https://thumbnails.ente.io/?fileID=${id}`; return `https://thumbnails.ente.io/?fileID=${id}`;
}; };
export const getSharedAlbumThumbnailUrl = (id: number) => {
if (process.env.NEXT_PUBLIC_ENTE_ENDPOINT !== undefined) {
return (
`${process.env.NEXT_PUBLIC_ENTE_ENDPOINT}/public-collection/files/preview/${id}` ??
'https://api.ente.io'
);
}
return `https://thumbnails.ente.io/?fileID=${id}`;
};
export const getSentryTunnelUrl = () => { export const getSentryTunnelUrl = () => {
return `https://sentry-reporter.ente.io`; return `https://sentry-reporter.ente.io`;
}; };