Merge branch 'master' into export-v2

This commit is contained in:
Abhinav 2021-11-29 14:56:22 +05:30
commit cff0959cf3
9 changed files with 148 additions and 60 deletions

View file

@ -354,7 +354,7 @@ 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.getPreview(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);

View file

@ -0,0 +1,25 @@
import React from 'react';
export default function DownloadIcon(props) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
height={props.height}
viewBox={props.viewBox}
width={props.width}
fill="currentColor">
<g>
<rect fill="none" height="24" width="24" />
</g>
<g>
<path d="M5,20h14v-2H5V20z M19,9h-4V3H9v6H5l7,7L19,9z" />
</g>
</svg>
);
}
DownloadIcon.defaultProps = {
height: 24,
width: 24,
viewBox: '0 0 24 24',
};

View file

@ -177,7 +177,7 @@ export default function PreviewCard(props: IProps) {
if (file && !file.msrc) { if (file && !file.msrc) {
const main = async () => { const main = async () => {
try { try {
const url = await DownloadManager.getPreview(file); const 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

@ -23,6 +23,7 @@ import {
FIX_CREATION_TIME_VISIBLE_TO_USER_IDS, FIX_CREATION_TIME_VISIBLE_TO_USER_IDS,
User, User,
} from 'services/userService'; } from 'services/userService';
import DownloadIcon from 'components/icons/DownloadIcon';
interface Props { interface Props {
addToCollectionHelper: (collection: Collection) => void; addToCollectionHelper: (collection: Collection) => void;
@ -34,6 +35,7 @@ interface Props {
deleteFileHelper: (permanent?: boolean) => void; deleteFileHelper: (permanent?: boolean) => void;
removeFromCollectionHelper: () => void; removeFromCollectionHelper: () => void;
fixTimeHelper: () => void; fixTimeHelper: () => void;
downloadHelper: () => void;
count: number; count: number;
clearSelection: () => void; clearSelection: () => void;
archiveFilesHelper: () => void; archiveFilesHelper: () => void;
@ -79,6 +81,7 @@ const SelectedFileOptions = ({
setDialogMessage, setDialogMessage,
setCollectionSelectorAttributes, setCollectionSelectorAttributes,
deleteFileHelper, deleteFileHelper,
downloadHelper,
count, count,
clearSelection, clearSelection,
archiveFilesHelper, archiveFilesHelper,
@ -190,6 +193,11 @@ const SelectedFileOptions = ({
</IconButton> </IconButton>
</IconWithMessage> </IconWithMessage>
)} )}
<IconWithMessage message={constants.DOWNLOAD}>
<IconButton onClick={downloadHelper}>
<DownloadIcon />
</IconButton>
</IconWithMessage>
<IconWithMessage message={constants.ADD}> <IconWithMessage message={constants.ADD}>
<IconButton onClick={addToCollection}> <IconButton onClick={addToCollection}>
<AddIcon /> <AddIcon />

View file

@ -50,6 +50,7 @@ import { LoadingOverlay } from 'components/LoadingOverlay';
import PhotoFrame from 'components/PhotoFrame'; import PhotoFrame from 'components/PhotoFrame';
import { import {
changeFilesVisibility, changeFilesVisibility,
downloadFiles,
getNonTrashedUniqueUserFiles, getNonTrashedUniqueUserFiles,
getSelectedFiles, getSelectedFiles,
mergeMetadata, mergeMetadata,
@ -545,6 +546,14 @@ export default function Gallery() {
clearSelection(); clearSelection();
}; };
const downloadHelper = async () => {
const selectedFiles = getSelectedFiles(selected, files);
clearSelection();
!syncInProgress.current && loadingBar.current?.continuousStart();
await downloadFiles(selectedFiles);
!syncInProgress.current && loadingBar.current.complete();
};
return ( return (
<GalleryContext.Provider <GalleryContext.Provider
value={{ value={{
@ -714,6 +723,7 @@ export default function Gallery() {
) )
} }
fixTimeHelper={fixTimeHelper} fixTimeHelper={fixTimeHelper}
downloadHelper={downloadHelper}
count={selected.count} count={selected.count}
clearSelection={clearSelection} clearSelection={clearSelection}
activeCollection={activeCollection} activeCollection={activeCollection}

View file

@ -1,7 +1,11 @@
import { getToken } from 'utils/common/key'; import { getToken } from 'utils/common/key';
import { getFileUrl, getThumbnailUrl } from 'utils/common/apiUtil'; import { getFileUrl, getThumbnailUrl } from 'utils/common/apiUtil';
import CryptoWorker from 'utils/crypto'; import CryptoWorker from 'utils/crypto';
import { generateStreamFromArrayBuffer, convertForPreview } from 'utils/file'; import {
generateStreamFromArrayBuffer,
convertForPreview,
needsConversionForPreview,
} from 'utils/file';
import HTTPService from './HTTPService'; import HTTPService from './HTTPService';
import { File, FILE_TYPE } from './fileService'; import { File, FILE_TYPE } from './fileService';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
@ -10,12 +14,14 @@ class DownloadManager {
private fileObjectUrlPromise = new Map<string, Promise<string>>(); private fileObjectUrlPromise = new Map<string, Promise<string>>();
private thumbnailObjectUrlPromise = new Map<number, Promise<string>>(); private thumbnailObjectUrlPromise = new Map<number, Promise<string>>();
public async getPreview(file: File) { public async getThumbnail(file: File) {
try { try {
const token = getToken(); const token = getToken();
if (!token) { if (!token) {
return null; return null;
} }
if (!this.thumbnailObjectUrlPromise.get(file.id)) {
const downloadPromise = async () => {
const thumbnailCache = await caches.open('thumbs'); const thumbnailCache = await caches.open('thumbs');
const cacheResp: Response = await thumbnailCache.match( const cacheResp: Response = await thumbnailCache.match(
file.id.toString() file.id.toString()
@ -23,14 +29,21 @@ class DownloadManager {
if (cacheResp) { if (cacheResp) {
return URL.createObjectURL(await cacheResp.blob()); return URL.createObjectURL(await cacheResp.blob());
} }
if (!this.thumbnailObjectUrlPromise.get(file.id)) { const thumb = await this.downloadThumb(token, file);
const downloadPromise = this.downloadThumb( const thumbBlob = new Blob([thumb]);
token, try {
thumbnailCache, await thumbnailCache.put(
file file.id.toString(),
new Response(thumbBlob)
); );
this.thumbnailObjectUrlPromise.set(file.id, downloadPromise); } catch (e) {
// TODO: handle storage full exception.
} }
return URL.createObjectURL(thumbBlob);
};
this.thumbnailObjectUrlPromise.set(file.id, downloadPromise());
}
return await this.thumbnailObjectUrlPromise.get(file.id); return await this.thumbnailObjectUrlPromise.get(file.id);
} catch (e) { } catch (e) {
this.thumbnailObjectUrlPromise.delete(file.id); this.thumbnailObjectUrlPromise.delete(file.id);
@ -39,24 +52,7 @@ class DownloadManager {
} }
} }
private downloadThumb = async ( downloadThumb = async (token: string, file: File) => {
token: string,
thumbnailCache: Cache,
file: File
) => {
const thumb = await this.getThumbnail(token, file);
try {
await thumbnailCache.put(
file.id.toString(),
new Response(new Blob([thumb]))
);
} catch (e) {
// TODO: handle storage full exception.
}
return URL.createObjectURL(new Blob([thumb]));
};
getThumbnail = async (token: string, file: File) => {
const resp = await HTTPService.get( const resp = await HTTPService.get(
getThumbnailUrl(file.id), getThumbnailUrl(file.id),
null, null,
@ -73,32 +69,38 @@ class DownloadManager {
}; };
getFile = async (file: File, forPreview = false) => { getFile = async (file: File, forPreview = false) => {
let fileUID: string; const shouldBeConverted = forPreview && needsConversionForPreview(file);
if (file.metadata.fileType === FILE_TYPE.VIDEO) { const fileKey = shouldBeConverted
fileUID = file.id.toString(); ? `${file.id}_converted`
} else { : `${file.id}`;
fileUID = `${file.id}_forPreview=${forPreview}`;
}
try { try {
const getFilePromise = async () => { const getFilePromise = async (convert: boolean) => {
const fileStream = await this.downloadFile(file); const fileStream = await this.downloadFile(file);
let fileBlob = await new Response(fileStream).blob(); let fileBlob = await new Response(fileStream).blob();
if (forPreview) { if (convert) {
fileBlob = await convertForPreview(file, fileBlob); fileBlob = await convertForPreview(file, fileBlob);
} }
return URL.createObjectURL(fileBlob); return URL.createObjectURL(fileBlob);
}; };
if (!this.fileObjectUrlPromise.get(fileUID)) { if (!this.fileObjectUrlPromise.get(fileKey)) {
this.fileObjectUrlPromise.set(fileUID, getFilePromise()); this.fileObjectUrlPromise.set(
fileKey,
getFilePromise(shouldBeConverted)
);
} }
return await this.fileObjectUrlPromise.get(fileUID); const fileURL = await this.fileObjectUrlPromise.get(fileKey);
return fileURL;
} catch (e) { } catch (e) {
this.fileObjectUrlPromise.delete(fileUID); this.fileObjectUrlPromise.delete(fileKey);
logError(e, 'Failed to get File'); logError(e, 'Failed to get File');
throw e; throw e;
} }
}; };
public async getCachedOriginalFile(file: File) {
return await this.fileObjectUrlPromise.get(file.id.toString());
}
async downloadFile(file: File) { async downloadFile(file: File) {
const worker = await new CryptoWorker(); const worker = await new CryptoWorker();
const token = getToken(); const token = getToken();

View file

@ -67,7 +67,7 @@ export async function replaceThumbnail(
current: idx, current: idx,
total: largeThumbnailFiles.length, total: largeThumbnailFiles.length,
}); });
const originalThumbnail = await downloadManager.getThumbnail( const originalThumbnail = await downloadManager.downloadThumb(
token, token,
file file
); );

View file

@ -4,7 +4,7 @@ import { logError } from 'utils/sentry';
import { BLACK_THUMBNAIL_BASE64 } from '../../../public/images/black-thumbnail-b64'; import { BLACK_THUMBNAIL_BASE64 } from '../../../public/images/black-thumbnail-b64';
import FFmpegService from 'services/ffmpegService'; import FFmpegService from 'services/ffmpegService';
import { convertToHumanReadable } from 'utils/billingUtil'; import { convertToHumanReadable } from 'utils/billingUtil';
import { fileIsHEIC } from 'utils/file'; import { isFileHEIC } from 'utils/file';
import { FileTypeInfo } from './readFileService'; import { FileTypeInfo } from './readFileService';
const MAX_THUMBNAIL_DIMENSION = 720; const MAX_THUMBNAIL_DIMENSION = 720;
@ -31,7 +31,7 @@ export async function generateThumbnail(
let thumbnail: Uint8Array; let thumbnail: Uint8Array;
try { try {
if (fileTypeInfo.fileType === FILE_TYPE.IMAGE) { if (fileTypeInfo.fileType === FILE_TYPE.IMAGE) {
const isHEIC = fileIsHEIC(fileTypeInfo.exactType); const isHEIC = isFileHEIC(fileTypeInfo.exactType);
canvas = await generateImageThumbnail(worker, file, isHEIC); canvas = await generateImageThumbnail(worker, file, isHEIC);
} else { } else {
try { try {

View file

@ -11,7 +11,7 @@ import {
} from 'services/fileService'; } from 'services/fileService';
import { decodeMotionPhoto } from 'services/motionPhotoService'; import { decodeMotionPhoto } from 'services/motionPhotoService';
import { getMimeTypeFromBlob } from 'services/upload/readFileService'; import { getMimeTypeFromBlob } from 'services/upload/readFileService';
import DownloadManger from 'services/downloadManager'; import DownloadManager from 'services/downloadManager';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
import { User } from 'services/userService'; import { User } from 'services/userService';
import CryptoWorker from 'utils/crypto'; import CryptoWorker from 'utils/crypto';
@ -40,18 +40,35 @@ export function downloadAsFile(filename: string, content: string) {
a.remove(); a.remove();
} }
export async function downloadFile(file) { export async function downloadFile(file: File) {
const a = document.createElement('a'); const a = document.createElement('a');
a.style.display = 'none'; a.style.display = 'none';
const fileURL = await DownloadManger.getFile(file); let fileURL = await DownloadManager.getCachedOriginalFile(file);
let tempURL;
if (!fileURL) {
tempURL = URL.createObjectURL(
await new Response(await DownloadManager.downloadFile(file)).blob()
);
fileURL = tempURL;
}
const fileType = getFileExtension(file.metadata.title);
let tempEditedFileURL;
if (
file.pubMagicMetadata?.data.editedTime &&
(fileType === TYPE_JPEG || fileType === TYPE_JPG)
) {
let fileBlob = await (await fetch(fileURL)).blob(); let fileBlob = await (await fetch(fileURL)).blob();
if (file.pubMagicMetadata?.data.editedTime) {
fileBlob = await updateFileCreationDateInEXIF( fileBlob = await updateFileCreationDateInEXIF(
fileBlob, fileBlob,
new Date(file.pubMagicMetadata.data.editedTime / 1000) new Date(file.pubMagicMetadata.data.editedTime / 1000)
); );
tempEditedFileURL = URL.createObjectURL(fileBlob);
fileURL = tempEditedFileURL;
} }
a.href = URL.createObjectURL(fileBlob);
a.href = fileURL;
if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) { if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) {
a.download = fileNameWithoutExtension(file.metadata.title) + '.zip'; a.download = fileNameWithoutExtension(file.metadata.title) + '.zip';
} else { } else {
@ -60,12 +77,15 @@ export async function downloadFile(file) {
document.body.appendChild(a); document.body.appendChild(a);
a.click(); a.click();
a.remove(); a.remove();
tempURL && URL.revokeObjectURL(tempURL);
tempEditedFileURL && URL.revokeObjectURL(tempEditedFileURL);
} }
export function fileIsHEIC(mimeType: string) { export function isFileHEIC(mimeType: string) {
return ( return (
mimeType.toLowerCase().endsWith(TYPE_HEIC) || mimeType &&
mimeType.toLowerCase().endsWith(TYPE_HEIF) (mimeType.toLowerCase().endsWith(TYPE_HEIC) ||
mimeType.toLowerCase().endsWith(TYPE_HEIF))
); );
} }
@ -286,7 +306,7 @@ export async function convertForPreview(file: File, fileBlob: Blob) {
const mimeType = const mimeType =
(await getMimeTypeFromBlob(worker, fileBlob)) ?? typeFromExtension; (await getMimeTypeFromBlob(worker, fileBlob)) ?? typeFromExtension;
if (fileIsHEIC(mimeType)) { if (isFileHEIC(mimeType)) {
fileBlob = await worker.convertHEIC2JPEG(fileBlob); fileBlob = await worker.convertHEIC2JPEG(fileBlob);
} }
return fileBlob; return fileBlob;
@ -481,3 +501,26 @@ export function getNonTrashedUniqueUserFiles(files: File[]) {
) )
); );
} }
export async function downloadFiles(files: File[]) {
for (const file of files) {
try {
await downloadFile(file);
} catch (e) {
logError(e, 'download fail for file');
}
}
}
export function needsConversionForPreview(file: File) {
const fileExtension = splitFilenameAndExtension(file.metadata.title)[1];
if (
file.metadata.fileType === FILE_TYPE.LIVE_PHOTO ||
(file.metadata.fileType === FILE_TYPE.IMAGE &&
isFileHEIC(fileExtension))
) {
return true;
} else {
return false;
}
}