add logic and apis for viewing sharedAlbum Thumbnails and files
This commit is contained in:
parent
d7832a2e08
commit
ecc2ce7093
|
@ -20,6 +20,11 @@ import { isPlaybackPossible } from 'utils/photoFrame';
|
|||
import { PhotoList } from './PhotoList';
|
||||
import { SetFiles, SelectedState, Search, setSearchStats } from 'types/gallery';
|
||||
import { FILE_TYPE } from 'constants/file';
|
||||
import SharedCollectionDownloadManager from 'services/sharedCollectionDownloadManager';
|
||||
import {
|
||||
defaultSharedAlbumContext,
|
||||
SharedAlbumContext,
|
||||
} from 'pages/shared-album';
|
||||
|
||||
const Container = styled.div`
|
||||
display: block;
|
||||
|
@ -89,6 +94,8 @@ const PhotoFrame = ({
|
|||
const [fetching, setFetching] = useState<{ [k: number]: boolean }>({});
|
||||
const startTime = Date.now();
|
||||
const galleryContext = useContext(GalleryContext);
|
||||
const sharedAlbumContext =
|
||||
useContext(SharedAlbumContext) ?? defaultSharedAlbumContext;
|
||||
const [rangeStart, setRangeStart] = useState(null);
|
||||
const [currentHover, setCurrentHover] = useState(null);
|
||||
const [isShiftKeyPressed, setIsShiftKeyPressed] = useState(false);
|
||||
|
@ -364,8 +371,16 @@ const PhotoFrame = ({
|
|||
let url: string;
|
||||
if (galleryContext.thumbs.has(item.id)) {
|
||||
url = galleryContext.thumbs.get(item.id);
|
||||
} else {
|
||||
if (sharedAlbumContext.accessedThroughSharedURL) {
|
||||
url =
|
||||
await SharedCollectionDownloadManager.getThumbnail(
|
||||
item,
|
||||
sharedAlbumContext.token
|
||||
);
|
||||
} else {
|
||||
url = await DownloadManager.getThumbnail(item);
|
||||
}
|
||||
galleryContext.thumbs.set(item.id, url);
|
||||
}
|
||||
updateUrl(item.dataIndex)(url);
|
||||
|
@ -391,8 +406,16 @@ const PhotoFrame = ({
|
|||
let url: string;
|
||||
if (galleryContext.files.has(item.id)) {
|
||||
url = galleryContext.files.get(item.id);
|
||||
} else {
|
||||
if (sharedAlbumContext.accessedThroughSharedURL) {
|
||||
url = await SharedCollectionDownloadManager.getFile(
|
||||
item,
|
||||
sharedAlbumContext.token,
|
||||
true
|
||||
);
|
||||
} else {
|
||||
url = await DownloadManager.getFile(item, true);
|
||||
}
|
||||
galleryContext.files.set(item.id, url);
|
||||
}
|
||||
await updateSrcUrl(item.dataIndex, url);
|
||||
|
|
|
@ -6,6 +6,11 @@ import DownloadManager from 'services/downloadManager';
|
|||
import useLongPress from 'utils/common/useLongPress';
|
||||
import { GalleryContext } from 'pages/gallery';
|
||||
import { GAP_BTW_TILES } from 'constants/gallery';
|
||||
import {
|
||||
defaultSharedAlbumContext,
|
||||
SharedAlbumContext,
|
||||
} from 'pages/shared-album';
|
||||
import SharedCollectionDownloadManager from 'services/sharedCollectionDownloadManager';
|
||||
|
||||
interface IProps {
|
||||
file: EnteFile;
|
||||
|
@ -173,11 +178,22 @@ export default function PreviewCard(props: IProps) {
|
|||
isInsSelectRange,
|
||||
} = props;
|
||||
const isMounted = useRef(true);
|
||||
const sharedAlbumContext =
|
||||
useContext(SharedAlbumContext) ?? defaultSharedAlbumContext;
|
||||
useLayoutEffect(() => {
|
||||
if (file && !file.msrc) {
|
||||
const main = async () => {
|
||||
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) {
|
||||
setImgSrc(url);
|
||||
thumbs.set(file.id, url);
|
||||
|
|
|
@ -1,10 +1,20 @@
|
|||
import { ALL_SECTION } from 'constants/collection';
|
||||
import PhotoFrame from 'components/PhotoFrame';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { createContext, useEffect, useState } from 'react';
|
||||
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() {
|
||||
const [token, setToken] = useState(null);
|
||||
const [token, setToken] = useState<string>(null);
|
||||
const [collectionKey, setCollectionKey] = useState(null);
|
||||
const [files, setFiles] = useState([]);
|
||||
useEffect(() => {
|
||||
|
@ -28,6 +38,12 @@ export default function sharedAlbum() {
|
|||
};
|
||||
|
||||
return (
|
||||
<SharedAlbumContext.Provider
|
||||
value={{
|
||||
...defaultSharedAlbumContext,
|
||||
token,
|
||||
accessedThroughSharedURL: true,
|
||||
}}>
|
||||
<PhotoFrame
|
||||
files={files}
|
||||
setFiles={setFiles}
|
||||
|
@ -43,7 +59,8 @@ export default function sharedAlbum() {
|
|||
setSearchStats={() => null}
|
||||
deleted={[]}
|
||||
activeCollection={ALL_SECTION}
|
||||
isSharedCollection={true}
|
||||
isSharedCollection
|
||||
/>
|
||||
</SharedAlbumContext.Provider>
|
||||
);
|
||||
}
|
||||
|
|
191
src/services/sharedCollectionDownloadManager.ts
Normal file
191
src/services/sharedCollectionDownloadManager.ts
Normal 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();
|
4
src/types/sharedAlbum/index.ts
Normal file
4
src/types/sharedAlbum/index.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export type SharedAlbumContextType = {
|
||||
token: string;
|
||||
accessedThroughSharedURL: boolean;
|
||||
};
|
|
@ -14,6 +14,16 @@ export const getFileUrl = (id: number) => {
|
|||
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) => {
|
||||
if (process.env.NEXT_PUBLIC_ENTE_ENDPOINT !== undefined) {
|
||||
return (
|
||||
|
@ -24,6 +34,16 @@ export const getThumbnailUrl = (id: number) => {
|
|||
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 = () => {
|
||||
return `https://sentry-reporter.ente.io`;
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue