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 { 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);
|
||||||
|
@ -364,8 +371,16 @@ const PhotoFrame = ({
|
||||||
let url: string;
|
let url: string;
|
||||||
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 {
|
||||||
|
if (sharedAlbumContext.accessedThroughSharedURL) {
|
||||||
|
url =
|
||||||
|
await SharedCollectionDownloadManager.getThumbnail(
|
||||||
|
item,
|
||||||
|
sharedAlbumContext.token
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
url = await DownloadManager.getThumbnail(item);
|
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);
|
||||||
|
@ -391,8 +406,16 @@ const PhotoFrame = ({
|
||||||
let url: string;
|
let url: string;
|
||||||
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 {
|
||||||
|
if (sharedAlbumContext.accessedThroughSharedURL) {
|
||||||
|
url = await SharedCollectionDownloadManager.getFile(
|
||||||
|
item,
|
||||||
|
sharedAlbumContext.token,
|
||||||
|
true
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
url = await DownloadManager.getFile(item, true);
|
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);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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,6 +38,12 @@ export default function sharedAlbum() {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<SharedAlbumContext.Provider
|
||||||
|
value={{
|
||||||
|
...defaultSharedAlbumContext,
|
||||||
|
token,
|
||||||
|
accessedThroughSharedURL: true,
|
||||||
|
}}>
|
||||||
<PhotoFrame
|
<PhotoFrame
|
||||||
files={files}
|
files={files}
|
||||||
setFiles={setFiles}
|
setFiles={setFiles}
|
||||||
|
@ -43,7 +59,8 @@ export default function sharedAlbum() {
|
||||||
setSearchStats={() => null}
|
setSearchStats={() => null}
|
||||||
deleted={[]}
|
deleted={[]}
|
||||||
activeCollection={ALL_SECTION}
|
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}`;
|
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`;
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue