Merge pull request #376 from ente-io/master

release
This commit is contained in:
Vishnu Mohandas 2022-02-16 21:30:20 +05:30 committed by GitHub
commit 73283645a8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 935 additions and 267 deletions

View file

@ -20,6 +20,7 @@ import {
changeFileName, changeFileName,
downloadFile, downloadFile,
formatDateTime, formatDateTime,
isLivePhoto,
splitFilenameAndExtension, splitFilenameAndExtension,
updateExistingFilePubMetadata, updateExistingFilePubMetadata,
} from 'utils/file'; } from 'utils/file';
@ -46,6 +47,10 @@ import { MAX_EDITED_FILE_NAME_LENGTH } from 'constants/file';
import { sleep } from 'utils/common'; import { sleep } from 'utils/common';
import { PublicCollectionGalleryContext } from 'utils/publicCollectionGallery'; import { PublicCollectionGalleryContext } from 'utils/publicCollectionGallery';
import { GalleryContext } from 'pages/gallery'; import { GalleryContext } from 'pages/gallery';
import {
getLivePhotoInfoShownCount,
setLivePhotoInfoShownCount,
} from 'utils/storage';
const SmallLoadingSpinner = () => ( const SmallLoadingSpinner = () => (
<EnteSpinner <EnteSpinner
@ -525,6 +530,19 @@ function PhotoSwipe(props: Iprops) {
setIsFav(isInFav(this?.currItem)); setIsFav(isInFav(this?.currItem));
} }
function handleLivePhotoNotification() {
if (isLivePhoto(this?.currItem)) {
const infoShownCount = getLivePhotoInfoShownCount();
if (infoShownCount < 3) {
galleryContext.setNotificationAttributes({
message: constants.PLAYBACK_SUPPORT_COMING,
title: constants.LIVE_PHOTO,
});
setLivePhotoInfoShownCount(infoShownCount + 1);
}
}
}
const openPhotoSwipe = () => { const openPhotoSwipe = () => {
const { items, currentIndex } = props; const { items, currentIndex } = props;
const options = { const options = {
@ -587,6 +605,7 @@ function PhotoSwipe(props: Iprops) {
photoSwipe.listen('beforeChange', function () { photoSwipe.listen('beforeChange', function () {
updateInfo.call(this); updateInfo.call(this);
updateFavButton.call(this); updateFavButton.call(this);
handleLivePhotoNotification.call(this);
}); });
photoSwipe.listen('resize', checkExifAvailable); photoSwipe.listen('resize', checkExifAvailable);
photoSwipe.init(); photoSwipe.init();
@ -608,6 +627,8 @@ function PhotoSwipe(props: Iprops) {
videoTag.pause(); videoTag.pause();
} }
handleCloseInfo(); handleCloseInfo();
// BE_AWARE: this will clear any notification set, even if they were not set in/by the photoswipe component
galleryContext.setNotificationAttributes(null);
}; };
const isInFav = (file) => { const isInFav = (file) => {
const { favItemIds } = props; const { favItemIds } = props;

View file

@ -0,0 +1,59 @@
import React, { useEffect, useState } from 'react';
import { Toast } from 'react-bootstrap';
import styled from 'styled-components';
import { NotificationAttributes } from 'types/gallery';
const Wrapper = styled.div`
position: absolute;
top: 60px;
right: 10px;
z-index: 1501;
min-height: 100px;
`;
const AUTO_HIDE_TIME_IN_MILLISECONDS = 3000;
interface Iprops {
attributes: NotificationAttributes;
clearAttributes: () => void;
}
export default function ToastNotification({
attributes,
clearAttributes,
}: Iprops) {
const [show, setShow] = useState(false);
const closeToast = () => {
setShow(false);
clearAttributes();
};
useEffect(() => {
if (!attributes) {
setShow(false);
} else {
setShow(true);
}
}, [attributes]);
return (
<Wrapper>
<Toast
onClose={closeToast}
show={show}
delay={AUTO_HIDE_TIME_IN_MILLISECONDS}
autohide>
{attributes?.title && (
<Toast.Header
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}>
<h6 style={{ marginBottom: 0 }}>{attributes.title} </h6>
</Toast.Header>
)}
{attributes?.message && (
<Toast.Body>{attributes.message}</Toast.Body>
)}
</Toast>
</Wrapper>
);
}

View file

@ -0,0 +1,31 @@
import React from 'react';
import styled from 'styled-components';
const Wrapper = styled.div`
font-size: 10px;
position: absolute;
padding: 2px;
right: 5px;
top: 5px;
`;
export default function LivePhotoIndicatorOverlay(props) {
return (
<Wrapper>
<svg
xmlns="http://www.w3.org/2000/svg"
height={props.height}
viewBox={props.viewBox}
width={props.width}
fill="currentColor">
<path d="M0 0h24v24H0V0z" fill="none" />
<path d="M6.76 4.84l-1.8-1.79-1.41 1.41 1.79 1.79zM1 10.5h3v2H1zM11 .55h2V3.5h-2zm8.04 2.495l1.408 1.407-1.79 1.79-1.407-1.408zm-1.8 15.115l1.79 1.8 1.41-1.41-1.8-1.79zM20 10.5h3v2h-3zm-8-5c-3.31 0-6 2.69-6 6s2.69 6 6 6 6-2.69 6-6-2.69-6-6-6zm0 10c-2.21 0-4-1.79-4-4s1.79-4 4-4 4 1.79 4 4-1.79 4-4 4zm-1 4h2v2.95h-2zm-7.45-.96l1.41 1.41 1.79-1.8-1.41-1.41z" />
</svg>
</Wrapper>
);
}
LivePhotoIndicatorOverlay.defaultProps = {
height: 20,
width: 20,
viewBox: '0 0 24 24',
};

View file

@ -53,7 +53,7 @@ export default function CollectionNamer({ attributes, ...props }: Props) {
title: attributes?.title, title: attributes?.title,
}}> }}>
<Formik<formValues> <Formik<formValues>
initialValues={{ albumName: attributes.autoFilledName }} initialValues={{ albumName: attributes.autoFilledName ?? '' }}
validationSchema={Yup.object().shape({ validationSchema={Yup.object().shape({
albumName: Yup.string().required(constants.REQUIRED), albumName: Yup.string().required(constants.REQUIRED),
})} })}

View file

@ -11,6 +11,8 @@ import {
PublicCollectionGalleryContext, PublicCollectionGalleryContext,
} from 'utils/publicCollectionGallery'; } from 'utils/publicCollectionGallery';
import PublicCollectionDownloadManager from 'services/publicCollectionDownloadManager'; import PublicCollectionDownloadManager from 'services/publicCollectionDownloadManager';
import LivePhotoIndicatorOverlay from 'components/icons/LivePhotoIndicatorOverlay';
import { isLivePhoto } from 'utils/file';
interface IProps { interface IProps {
file: EnteFile; file: EnteFile;
@ -283,6 +285,7 @@ export default function PreviewCard(props: IProps) {
<InSelectRangeOverLay <InSelectRangeOverLay
active={isRangeSelectActive && isInsSelectRange} active={isRangeSelectActive && isInsSelectRange}
/> />
{isLivePhoto(file) && <LivePhotoIndicatorOverlay />}
</Cont> </Cont>
); );
} }

View file

@ -18,7 +18,7 @@ import { METADATA_FOLDER_NAME } from 'constants/export';
import { getUserFacingErrorMessage } from 'utils/error'; import { getUserFacingErrorMessage } from 'utils/error';
import { Collection } from 'types/collection'; import { Collection } from 'types/collection';
import { SetLoading, SetFiles } from 'types/gallery'; import { SetLoading, SetFiles } from 'types/gallery';
import { UPLOAD_STAGES } from 'constants/upload'; import { FileUploadResults, UPLOAD_STAGES } from 'constants/upload';
import { FileWithCollection } from 'types/upload'; import { FileWithCollection } from 'types/upload';
const FIRST_ALBUM_NAME = 'My First Album'; const FIRST_ALBUM_NAME = 'My First Album';
@ -54,10 +54,15 @@ export default function Upload(props: Props) {
const [uploadStage, setUploadStage] = useState<UPLOAD_STAGES>( const [uploadStage, setUploadStage] = useState<UPLOAD_STAGES>(
UPLOAD_STAGES.START UPLOAD_STAGES.START
); );
const [filenames, setFilenames] = useState(new Map<number, string>());
const [fileCounter, setFileCounter] = useState({ finished: 0, total: 0 }); const [fileCounter, setFileCounter] = useState({ finished: 0, total: 0 });
const [fileProgress, setFileProgress] = useState(new Map<string, number>()); const [fileProgress, setFileProgress] = useState(new Map<number, number>());
const [uploadResult, setUploadResult] = useState(new Map<string, number>()); const [uploadResult, setUploadResult] = useState(
new Map<number, FileUploadResults>()
);
const [percentComplete, setPercentComplete] = useState(0); const [percentComplete, setPercentComplete] = useState(0);
const [hasLivePhotos, setHasLivePhotos] = useState(false);
const [choiceModalView, setChoiceModalView] = useState(false); const [choiceModalView, setChoiceModalView] = useState(false);
const [analysisResult, setAnalysisResult] = useState<AnalysisResult>({ const [analysisResult, setAnalysisResult] = useState<AnalysisResult>({
suggestedCollectionName: '', suggestedCollectionName: '',
@ -74,6 +79,8 @@ export default function Upload(props: Props) {
setFileProgress, setFileProgress,
setUploadResult, setUploadResult,
setUploadStage, setUploadStage,
setFilenames,
setHasLivePhotos,
}, },
props.setFiles props.setFiles
); );
@ -107,8 +114,8 @@ export default function Upload(props: Props) {
const uploadInit = function () { const uploadInit = function () {
setUploadStage(UPLOAD_STAGES.START); setUploadStage(UPLOAD_STAGES.START);
setFileCounter({ finished: 0, total: 0 }); setFileCounter({ finished: 0, total: 0 });
setFileProgress(new Map<string, number>()); setFileProgress(new Map<number, number>());
setUploadResult(new Map<string, number>()); setUploadResult(new Map<number, number>());
setPercentComplete(0); setPercentComplete(0);
props.closeCollectionSelector(); props.closeCollectionSelector();
setProgressView(true); setProgressView(true);
@ -169,8 +176,9 @@ export default function Upload(props: Props) {
try { try {
uploadInit(); uploadInit();
const filesWithCollectionToUpload: FileWithCollection[] = const filesWithCollectionToUpload: FileWithCollection[] =
props.acceptedFiles.map((file) => ({ props.acceptedFiles.map((file, index) => ({
file, file,
localID: index,
collectionID: collection.id, collectionID: collection.id,
})); }));
await uploadFiles(filesWithCollectionToUpload); await uploadFiles(filesWithCollectionToUpload);
@ -196,18 +204,21 @@ export default function Upload(props: Props) {
} }
try { try {
const existingCollection = await syncCollections(); const existingCollection = await syncCollections();
let index = 0;
for (const [collectionName, files] of collectionWiseFiles) { for (const [collectionName, files] of collectionWiseFiles) {
const collection = await createAlbum( const collection = await createAlbum(
collectionName, collectionName,
existingCollection existingCollection
); );
collections.push(collection); collections.push(collection);
for (const file of files) {
filesWithCollectionToUpload.push({ filesWithCollectionToUpload.push(
...files.map((file) => ({
localID: index++,
collectionID: collection.id, collectionID: collection.id,
file, file,
}); }))
} );
} }
} catch (e) { } catch (e) {
setProgressView(false); setProgressView(false);
@ -336,9 +347,11 @@ export default function Upload(props: Props) {
/> />
<UploadProgress <UploadProgress
now={percentComplete} now={percentComplete}
filenames={filenames}
fileCounter={fileCounter} fileCounter={fileCounter}
uploadStage={uploadStage} uploadStage={uploadStage}
fileProgress={fileProgress} fileProgress={fileProgress}
hasLivePhotos={hasLivePhotos}
show={progressView} show={progressView}
closeModal={() => setProgressView(false)} closeModal={() => setProgressView(false)}
retryFailed={retryFailed} retryFailed={retryFailed}

View file

@ -17,13 +17,15 @@ interface Props {
now; now;
closeModal; closeModal;
retryFailed; retryFailed;
fileProgress: Map<string, number>; fileProgress: Map<number, number>;
filenames: Map<number, string>;
show; show;
fileRejections: FileRejection[]; fileRejections: FileRejection[];
uploadResult: Map<string, number>; uploadResult: Map<number, FileUploadResults>;
hasLivePhotos: boolean;
} }
interface FileProgresses { interface FileProgresses {
fileName: string; fileID: number;
progress: number; progress: number;
} }
@ -72,7 +74,8 @@ const NotUploadSectionHeader = styled.div`
`; `;
interface ResultSectionProps { interface ResultSectionProps {
fileUploadResultMap: Map<FileUploadResults, string[]>; filenames: Map<number, string>;
fileUploadResultMap: Map<FileUploadResults, number[]>;
fileUploadResult: FileUploadResults; fileUploadResult: FileUploadResults;
sectionTitle: any; sectionTitle: any;
sectionInfo?: any; sectionInfo?: any;
@ -95,8 +98,8 @@ const ResultSection = (props: ResultSectionProps) => {
<SectionInfo>{props.sectionInfo}</SectionInfo> <SectionInfo>{props.sectionInfo}</SectionInfo>
)} )}
<FileList> <FileList>
{fileList.map((fileName) => ( {fileList.map((fileID) => (
<li key={fileName}>{fileName}</li> <li key={fileID}>{props.filenames.get(fileID)}</li>
))} ))}
</FileList> </FileList>
</Content> </Content>
@ -106,8 +109,10 @@ const ResultSection = (props: ResultSectionProps) => {
}; };
interface InProgressProps { interface InProgressProps {
filenames: Map<number, string>;
sectionTitle: string; sectionTitle: string;
fileProgressStatuses: FileProgresses[]; fileProgressStatuses: FileProgresses[];
sectionInfo?: any;
} }
const InProgressSection = (props: InProgressProps) => { const InProgressSection = (props: InProgressProps) => {
const [listView, setListView] = useState(true); const [listView, setListView] = useState(true);
@ -126,10 +131,15 @@ const InProgressSection = (props: InProgressProps) => {
</SectionTitle> </SectionTitle>
<Collapse isOpened={listView}> <Collapse isOpened={listView}>
<Content> <Content>
{props.sectionInfo && (
<SectionInfo>{props.sectionInfo}</SectionInfo>
)}
<FileList> <FileList>
{fileList.map(({ fileName, progress }) => ( {fileList.map(({ fileID, progress }) => (
<li key={fileName}> <li key={fileID}>
{`${fileName} - ${progress}%`} {`${props.filenames.get(
fileID
)} - ${progress}%`}
</li> </li>
))} ))}
</FileList> </FileList>
@ -141,26 +151,33 @@ const InProgressSection = (props: InProgressProps) => {
export default function UploadProgress(props: Props) { export default function UploadProgress(props: Props) {
const fileProgressStatuses = [] as FileProgresses[]; const fileProgressStatuses = [] as FileProgresses[];
const fileUploadResultMap = new Map<FileUploadResults, string[]>(); const fileUploadResultMap = new Map<FileUploadResults, number[]>();
let filesNotUploaded = false; let filesNotUploaded = false;
let sectionInfo = null;
if (props.fileProgress) { if (props.fileProgress) {
for (const [fileName, progress] of props.fileProgress) { for (const [localID, progress] of props.fileProgress) {
fileProgressStatuses.push({ fileName, progress }); fileProgressStatuses.push({
fileID: localID,
progress,
});
} }
} }
if (props.uploadResult) { if (props.uploadResult) {
for (const [fileName, progress] of props.uploadResult) { for (const [localID, progress] of props.uploadResult) {
if (!fileUploadResultMap.has(progress)) { if (!fileUploadResultMap.has(progress)) {
fileUploadResultMap.set(progress, []); fileUploadResultMap.set(progress, []);
} }
if (progress < 0) { if (progress !== FileUploadResults.UPLOADED) {
filesNotUploaded = true; filesNotUploaded = true;
} }
const fileList = fileUploadResultMap.get(progress); const fileList = fileUploadResultMap.get(progress);
fileUploadResultMap.set(progress, [...fileList, fileName]);
fileUploadResultMap.set(progress, [...fileList, localID]);
} }
} }
if (props.hasLivePhotos) {
sectionInfo = constants.LIVE_PHOTOS_DETECTED();
}
return ( return (
<Modal <Modal
@ -200,11 +217,14 @@ export default function UploadProgress(props: Props) {
/> />
)} )}
<InProgressSection <InProgressSection
filenames={props.filenames}
fileProgressStatuses={fileProgressStatuses} fileProgressStatuses={fileProgressStatuses}
sectionTitle={constants.INPROGRESS_UPLOADS} sectionTitle={constants.INPROGRESS_UPLOADS}
sectionInfo={sectionInfo}
/> />
<ResultSection <ResultSection
filenames={props.filenames}
fileUploadResultMap={fileUploadResultMap} fileUploadResultMap={fileUploadResultMap}
fileUploadResult={FileUploadResults.UPLOADED} fileUploadResult={FileUploadResults.UPLOADED}
sectionTitle={constants.SUCCESSFUL_UPLOADS} sectionTitle={constants.SUCCESSFUL_UPLOADS}
@ -218,6 +238,7 @@ export default function UploadProgress(props: Props) {
)} )}
<ResultSection <ResultSection
filenames={props.filenames}
fileUploadResultMap={fileUploadResultMap} fileUploadResultMap={fileUploadResultMap}
fileUploadResult={FileUploadResults.BLOCKED} fileUploadResult={FileUploadResults.BLOCKED}
sectionTitle={constants.BLOCKED_UPLOADS} sectionTitle={constants.BLOCKED_UPLOADS}
@ -226,17 +247,20 @@ export default function UploadProgress(props: Props) {
)} )}
/> />
<ResultSection <ResultSection
filenames={props.filenames}
fileUploadResultMap={fileUploadResultMap} fileUploadResultMap={fileUploadResultMap}
fileUploadResult={FileUploadResults.FAILED} fileUploadResult={FileUploadResults.FAILED}
sectionTitle={constants.FAILED_UPLOADS} sectionTitle={constants.FAILED_UPLOADS}
/> />
<ResultSection <ResultSection
filenames={props.filenames}
fileUploadResultMap={fileUploadResultMap} fileUploadResultMap={fileUploadResultMap}
fileUploadResult={FileUploadResults.ALREADY_UPLOADED} fileUploadResult={FileUploadResults.ALREADY_UPLOADED}
sectionTitle={constants.SKIPPED_FILES} sectionTitle={constants.SKIPPED_FILES}
sectionInfo={constants.SKIPPED_INFO} sectionInfo={constants.SKIPPED_INFO}
/> />
<ResultSection <ResultSection
filenames={props.filenames}
fileUploadResultMap={fileUploadResultMap} fileUploadResultMap={fileUploadResultMap}
fileUploadResult={ fileUploadResult={
FileUploadResults.LARGER_THAN_AVAILABLE_STORAGE FileUploadResults.LARGER_THAN_AVAILABLE_STORAGE
@ -247,12 +271,14 @@ export default function UploadProgress(props: Props) {
sectionInfo={constants.LARGER_THAN_AVAILABLE_STORAGE_INFO} sectionInfo={constants.LARGER_THAN_AVAILABLE_STORAGE_INFO}
/> />
<ResultSection <ResultSection
filenames={props.filenames}
fileUploadResultMap={fileUploadResultMap} fileUploadResultMap={fileUploadResultMap}
fileUploadResult={FileUploadResults.UNSUPPORTED} fileUploadResult={FileUploadResults.UNSUPPORTED}
sectionTitle={constants.UNSUPPORTED_FILES} sectionTitle={constants.UNSUPPORTED_FILES}
sectionInfo={constants.UNSUPPORTED_INFO} sectionInfo={constants.UNSUPPORTED_INFO}
/> />
<ResultSection <ResultSection
filenames={props.filenames}
fileUploadResultMap={fileUploadResultMap} fileUploadResultMap={fileUploadResultMap}
fileUploadResult={FileUploadResults.TOO_LARGE} fileUploadResult={FileUploadResults.TOO_LARGE}
sectionTitle={constants.TOO_LARGE_UPLOADS} sectionTitle={constants.TOO_LARGE_UPLOADS}

View file

@ -25,6 +25,7 @@ export const NULL_LOCATION: Location = { latitude: null, longitude: null };
export enum UPLOAD_STAGES { export enum UPLOAD_STAGES {
START, START,
READING_GOOGLE_METADATA_FILES, READING_GOOGLE_METADATA_FILES,
EXTRACTING_METADATA,
UPLOADING, UPLOADING,
FINISH, FINISH,
} }
@ -38,3 +39,7 @@ export enum FileUploadResults {
LARGER_THAN_AVAILABLE_STORAGE, LARGER_THAN_AVAILABLE_STORAGE,
UPLOADED, UPLOADED,
} }
export const MAX_FILE_SIZE_SUPPORTED = 5 * 1024 * 1024 * 1024; // 5 GB
export const LIVE_PHOTO_ASSET_SIZE_LIMIT = 20 * 1024 * 1024; // 20MB

View file

@ -134,10 +134,10 @@ const GlobalStyles = createGlobalStyle`
min-height: -moz-calc(80% - 3.5rem); min-height: -moz-calc(80% - 3.5rem);
min-height: calc(80% - 3.5rem); min-height: calc(80% - 3.5rem);
} }
.modal .modal-header, .modal .modal-footer { .modal .modal-header, .modal .modal-footer , .toast-header{
border-color: #444 !important; border-color: #444 !important;
} }
.modal .modal-header .close { .modal .modal-header .close, .toast-header .close {
color: #aaa; color: #aaa;
text-shadow: none; text-shadow: none;
} }
@ -145,7 +145,11 @@ const GlobalStyles = createGlobalStyle`
z-index:2000; z-index:2000;
opacity:0.8 !important; opacity:0.8 !important;
} }
.modal .card , .table {
.toast-header{
border-radius:0px !important;
}
.modal .card , .table , .toast {
background-color: #202020; background-color: #202020;
border: none; border: none;
} }
@ -154,7 +158,7 @@ const GlobalStyles = createGlobalStyle`
overflow: hidden; overflow: hidden;
margin: 0 0 5px 0; margin: 0 0 5px 0;
} }
.modal-content { .modal-content ,.toast-header{
border-radius:15px; border-radius:15px;
background-color:#202020 !important; background-color:#202020 !important;
} }
@ -485,6 +489,7 @@ const GlobalStyles = createGlobalStyle`
.form-check-input:hover, .form-check-label :hover{ .form-check-input:hover, .form-check-label :hover{
cursor:pointer; cursor:pointer;
} }
`; `;
export const LogoImage = styled.img` export const LogoImage = styled.img`

View file

@ -96,9 +96,15 @@ import FixCreationTime, {
} from 'components/FixCreationTime'; } from 'components/FixCreationTime';
import { Collection, CollectionAndItsLatestFile } from 'types/collection'; import { Collection, CollectionAndItsLatestFile } from 'types/collection';
import { EnteFile } from 'types/file'; import { EnteFile } from 'types/file';
import { GalleryContextType, SelectedState, Search } from 'types/gallery'; import {
GalleryContextType,
SelectedState,
Search,
NotificationAttributes,
} from 'types/gallery';
import Collections from 'components/pages/gallery/Collections'; import Collections from 'components/pages/gallery/Collections';
import { VISIBILITY_STATE } from 'constants/file'; import { VISIBILITY_STATE } from 'constants/file';
import ToastNotification from 'components/ToastNotification';
export const DeadCenter = styled.div` export const DeadCenter = styled.div`
flex: 1; flex: 1;
@ -125,6 +131,7 @@ const defaultGalleryContext: GalleryContextType = {
setDialogMessage: () => null, setDialogMessage: () => null,
startLoading: () => null, startLoading: () => null,
finishLoading: () => null, finishLoading: () => null,
setNotificationAttributes: () => null,
}; };
export const GalleryContext = createContext<GalleryContextType>( export const GalleryContext = createContext<GalleryContextType>(
@ -191,9 +198,14 @@ export default function Gallery() {
const [fixCreationTimeAttributes, setFixCreationTimeAttributes] = const [fixCreationTimeAttributes, setFixCreationTimeAttributes] =
useState<FixCreationTimeAttributes>(null); useState<FixCreationTimeAttributes>(null);
const [notificationAttributes, setNotificationAttributes] =
useState<NotificationAttributes>(null);
const showPlanSelectorModal = () => setPlanModalView(true); const showPlanSelectorModal = () => setPlanModalView(true);
const closeMessageDialog = () => setMessageDialogView(false); const closeMessageDialog = () => setMessageDialogView(false);
const clearNotificationAttributes = () => setNotificationAttributes(null);
useEffect(() => { useEffect(() => {
const key = getKey(SESSION_KEYS.ENCRYPTION_KEY); const key = getKey(SESSION_KEYS.ENCRYPTION_KEY);
if (!key) { if (!key) {
@ -555,6 +567,7 @@ export default function Gallery() {
setDialogMessage, setDialogMessage,
startLoading, startLoading,
finishLoading, finishLoading,
setNotificationAttributes,
}}> }}>
<FullScreenDropZone <FullScreenDropZone
getRootProps={getRootProps} getRootProps={getRootProps}
@ -577,6 +590,10 @@ export default function Gallery() {
setLoading={setLoading} setLoading={setLoading}
/> />
<AlertBanner bannerMessage={bannerMessage} /> <AlertBanner bannerMessage={bannerMessage} />
<ToastNotification
attributes={notificationAttributes}
clearAttributes={clearNotificationAttributes}
/>
<MessageDialog <MessageDialog
size="lg" size="lg"
show={messageDialogView} show={messageDialogView}

View file

@ -79,7 +79,6 @@ export async function replaceThumbnail(
); );
const fileTypeInfo = await getFileType(reader, dummyImageFile); const fileTypeInfo = await getFileType(reader, dummyImageFile);
const { thumbnail: newThumbnail } = await generateThumbnail( const { thumbnail: newThumbnail } = await generateThumbnail(
worker,
reader, reader,
dummyImageFile, dummyImageFile,
fileTypeInfo fileTypeInfo

View file

@ -32,3 +32,16 @@ export const decodeMotionPhoto = async (
} }
return motionPhoto; return motionPhoto;
}; };
export const encodeMotionPhoto = async (motionPhoto: MotionPhoto) => {
const zip = new JSZip();
zip.file(
'image' + fileExtensionWithDot(motionPhoto.imageNameTitle),
motionPhoto.image
);
zip.file(
'video' + fileExtensionWithDot(motionPhoto.videoNameTitle),
motionPhoto.video
);
return await zip.generateAsync({ type: 'uint8array' });
};

View file

@ -0,0 +1,105 @@
import {
FileTypeInfo,
FileInMemory,
Metadata,
B64EncryptionResult,
EncryptedFile,
EncryptionResult,
FileWithMetadata,
ParsedMetadataJSONMap,
} from 'types/upload';
import { logError } from 'utils/sentry';
import { encryptFiledata } from './encryptionService';
import { extractMetadata, getMetadataJSONMapKey } from './metadataService';
import { getFileData, getFileOriginalName } from './readFileService';
import { generateThumbnail } from './thumbnailService';
export function getFileSize(file: File) {
return file.size;
}
export function getFilename(file: File) {
return file.name;
}
export async function readFile(
reader: FileReader,
fileTypeInfo: FileTypeInfo,
rawFile: File
): Promise<FileInMemory> {
const { thumbnail, hasStaticThumbnail } = await generateThumbnail(
reader,
rawFile,
fileTypeInfo
);
const filedata = await getFileData(reader, rawFile);
return {
filedata,
thumbnail,
hasStaticThumbnail,
};
}
export async function extractFileMetadata(
parsedMetadataJSONMap: ParsedMetadataJSONMap,
rawFile: File,
collectionID: number,
fileTypeInfo: FileTypeInfo
) {
const originalName = getFileOriginalName(rawFile);
const googleMetadata =
parsedMetadataJSONMap.get(
getMetadataJSONMapKey(collectionID, originalName)
) ?? {};
const extractedMetadata: Metadata = await extractMetadata(
rawFile,
fileTypeInfo
);
for (const [key, value] of Object.entries(googleMetadata)) {
if (!value) {
continue;
}
extractedMetadata[key] = value;
}
return extractedMetadata;
}
export async function encryptFile(
worker: any,
file: FileWithMetadata,
encryptionKey: string
): Promise<EncryptedFile> {
try {
const { key: fileKey, file: encryptedFiledata } = await encryptFiledata(
worker,
file.filedata
);
const { file: encryptedThumbnail }: EncryptionResult =
await worker.encryptThumbnail(file.thumbnail, fileKey);
const { file: encryptedMetadata }: EncryptionResult =
await worker.encryptMetadata(file.metadata, fileKey);
const encryptedKey: B64EncryptionResult = await worker.encryptToB64(
fileKey,
encryptionKey
);
const result: EncryptedFile = {
file: {
file: encryptedFiledata,
thumbnail: encryptedThumbnail,
metadata: encryptedMetadata,
localID: file.localID,
},
fileKey: encryptedKey,
};
return result;
} catch (e) {
logError(e, 'Error encrypting files');
throw e;
}
}

View file

@ -0,0 +1,242 @@
import { FILE_TYPE } from 'constants/file';
import { LIVE_PHOTO_ASSET_SIZE_LIMIT } from 'constants/upload';
import { encodeMotionPhoto } from 'services/motionPhotoService';
import {
FileTypeInfo,
FileWithCollection,
LivePhotoAssets,
Metadata,
} from 'types/upload';
import { CustomError } from 'utils/error';
import { isImageOrVideo, splitFilenameAndExtension } from 'utils/file';
import { logError } from 'utils/sentry';
import { getUint8ArrayView } from './readFileService';
import { generateThumbnail } from './thumbnailService';
import uploadService from './uploadService';
import UploadService from './uploadService';
interface LivePhotoIdentifier {
collectionID: number;
fileType: FILE_TYPE;
name: string;
size: number;
}
interface Asset {
file: File;
metadata: Metadata;
fileTypeInfo: FileTypeInfo;
}
const ENTE_LIVE_PHOTO_FORMAT = 'elp';
const UNDERSCORE_THREE = '_3';
const UNDERSCORE = '_';
export function getLivePhotoFileType(
imageFileTypeInfo: FileTypeInfo,
videoTypeInfo: FileTypeInfo
): FileTypeInfo {
return {
fileType: FILE_TYPE.LIVE_PHOTO,
exactType: `${imageFileTypeInfo.exactType}+${videoTypeInfo.exactType}`,
imageType: imageFileTypeInfo.exactType,
videoType: videoTypeInfo.exactType,
};
}
export function getLivePhotoMetadata(imageMetadata: Metadata) {
return {
...imageMetadata,
title: getLivePhotoName(imageMetadata.title),
fileType: FILE_TYPE.LIVE_PHOTO,
};
}
export function getLivePhotoSize(livePhotoAssets: LivePhotoAssets) {
return livePhotoAssets.image.size + livePhotoAssets.video.size;
}
export function getLivePhotoName(imageTitle: string) {
return `${
splitFilenameAndExtension(imageTitle)[0]
}.${ENTE_LIVE_PHOTO_FORMAT}`;
}
export async function readLivePhoto(
reader: FileReader,
fileTypeInfo: FileTypeInfo,
livePhotoAssets: LivePhotoAssets
) {
const { thumbnail, hasStaticThumbnail } = await generateThumbnail(
reader,
livePhotoAssets.image,
{
exactType: fileTypeInfo.imageType,
fileType: FILE_TYPE.IMAGE,
}
);
const image = await getUint8ArrayView(reader, livePhotoAssets.image);
const video = await getUint8ArrayView(reader, livePhotoAssets.video);
return {
filedata: await encodeMotionPhoto({
image,
video,
imageNameTitle: livePhotoAssets.image.name,
videoNameTitle: livePhotoAssets.video.name,
}),
thumbnail,
hasStaticThumbnail,
};
}
export function clusterLivePhotoFiles(mediaFiles: FileWithCollection[]) {
const analysedMediaFiles: FileWithCollection[] = [];
mediaFiles
.sort((firstMediaFile, secondMediaFile) =>
splitFilenameAndExtension(
firstMediaFile.file.name
)[0].localeCompare(
splitFilenameAndExtension(secondMediaFile.file.name)[0]
)
)
.sort(
(firstMediaFile, secondMediaFile) =>
firstMediaFile.collectionID - secondMediaFile.collectionID
);
let index = 0;
while (index < mediaFiles.length - 1) {
const firstMediaFile = mediaFiles[index];
const secondMediaFile = mediaFiles[index + 1];
const { fileTypeInfo: firstFileTypeInfo, metadata: firstFileMetadata } =
UploadService.getFileMetadataAndFileTypeInfo(
firstMediaFile.localID
);
const {
fileTypeInfo: secondFileFileInfo,
metadata: secondFileMetadata,
} = UploadService.getFileMetadataAndFileTypeInfo(
secondMediaFile.localID
);
const firstFileIdentifier: LivePhotoIdentifier = {
collectionID: firstMediaFile.collectionID,
fileType: firstFileTypeInfo.fileType,
name: firstMediaFile.file.name,
size: firstMediaFile.file.size,
};
const secondFileIdentifier: LivePhotoIdentifier = {
collectionID: secondMediaFile.collectionID,
fileType: secondFileFileInfo.fileType,
name: secondMediaFile.file.name,
size: secondMediaFile.file.size,
};
const firstAsset = {
file: firstMediaFile.file,
metadata: firstFileMetadata,
fileTypeInfo: firstFileTypeInfo,
};
const secondAsset = {
file: secondMediaFile.file,
metadata: secondFileMetadata,
fileTypeInfo: secondFileFileInfo,
};
if (
areFilesLivePhotoAssets(firstFileIdentifier, secondFileIdentifier)
) {
let imageAsset: Asset;
let videoAsset: Asset;
if (
firstFileTypeInfo.fileType === FILE_TYPE.IMAGE &&
secondFileFileInfo.fileType === FILE_TYPE.VIDEO
) {
imageAsset = firstAsset;
videoAsset = secondAsset;
} else {
videoAsset = firstAsset;
imageAsset = secondAsset;
}
const livePhotoLocalID = firstMediaFile.localID;
analysedMediaFiles.push({
localID: livePhotoLocalID,
collectionID: firstMediaFile.collectionID,
isLivePhoto: true,
livePhotoAssets: {
image: imageAsset.file,
video: videoAsset.file,
},
});
const livePhotoFileTypeInfo: FileTypeInfo = getLivePhotoFileType(
imageAsset.fileTypeInfo,
videoAsset.fileTypeInfo
);
const livePhotoMetadata: Metadata = getLivePhotoMetadata(
imageAsset.metadata
);
uploadService.setFileMetadataAndFileTypeInfo(livePhotoLocalID, {
fileTypeInfo: { ...livePhotoFileTypeInfo },
metadata: { ...livePhotoMetadata },
});
index += 2;
} else {
analysedMediaFiles.push({ ...firstMediaFile, isLivePhoto: false });
index += 1;
}
}
if (index === mediaFiles.length - 1) {
analysedMediaFiles.push({ ...mediaFiles[index], isLivePhoto: false });
}
return analysedMediaFiles;
}
function areFilesLivePhotoAssets(
firstFileIdentifier: LivePhotoIdentifier,
secondFileIdentifier: LivePhotoIdentifier
) {
if (
firstFileIdentifier.collectionID ===
secondFileIdentifier.collectionID &&
firstFileIdentifier.fileType !== secondFileIdentifier.fileType &&
isImageOrVideo(firstFileIdentifier.fileType) &&
isImageOrVideo(secondFileIdentifier.fileType) &&
removeUnderscoreThreeSuffix(
splitFilenameAndExtension(firstFileIdentifier.name)[0]
) ===
removeUnderscoreThreeSuffix(
splitFilenameAndExtension(secondFileIdentifier.name)[0]
)
) {
// checks size of live Photo assets are less than allowed limit
// I did that based on the assumption that live photo assets ideally would not be larger than LIVE_PHOTO_ASSET_SIZE_LIMIT
// also zipping library doesn't support stream as a input
if (
firstFileIdentifier.size <= LIVE_PHOTO_ASSET_SIZE_LIMIT &&
secondFileIdentifier.size <= LIVE_PHOTO_ASSET_SIZE_LIMIT
) {
return true;
} else {
logError(
new Error(CustomError.TOO_LARGE_LIVE_PHOTO_ASSETS),
CustomError.TOO_LARGE_LIVE_PHOTO_ASSETS,
{
fileSizes: [
firstFileIdentifier.size,
secondFileIdentifier.size,
],
}
);
}
}
return false;
}
function removeUnderscoreThreeSuffix(filename: string) {
if (filename.endsWith(UNDERSCORE_THREE)) {
return filename.slice(0, filename.lastIndexOf(UNDERSCORE));
} else {
return filename;
}
}

View file

@ -8,6 +8,7 @@ import {
FileTypeInfo, FileTypeInfo,
} from 'types/upload'; } from 'types/upload';
import { NULL_LOCATION } from 'constants/upload'; import { NULL_LOCATION } from 'constants/upload';
import { splitFilenameAndExtension } from 'utils/file';
interface ParsedMetadataJSONWithTitle { interface ParsedMetadataJSONWithTitle {
title: string; title: string;
@ -30,7 +31,9 @@ export async function extractMetadata(
} }
const extractedMetadata: Metadata = { const extractedMetadata: Metadata = {
title: receivedFile.name, title: `${splitFilenameAndExtension(receivedFile.name)[0]}.${
fileTypeInfo.exactType
}`,
creationTime: creationTime:
exifData?.creationTime ?? receivedFile.lastModified * 1000, exifData?.creationTime ?? receivedFile.lastModified * 1000,
modificationTime: receivedFile.lastModified * 1000, modificationTime: receivedFile.lastModified * 1000,
@ -41,8 +44,11 @@ export async function extractMetadata(
return extractedMetadata; return extractedMetadata;
} }
export const getMetadataMapKey = (collectionID: number, title: string) => export const getMetadataJSONMapKey = (
`${collectionID}_${title}`; collectionID: number,
title: string
) => `${collectionID}-${title}`;
export async function parseMetadataJSON( export async function parseMetadataJSON(
reader: FileReader, reader: FileReader,

View file

@ -20,7 +20,7 @@ function calculatePartCount(chunkCount: number) {
return partCount; return partCount;
} }
export async function uploadStreamUsingMultipart( export async function uploadStreamUsingMultipart(
filename: string, fileLocalID: number,
dataStream: DataStream dataStream: DataStream
) { ) {
const uploadPartCount = calculatePartCount(dataStream.chunkCount); const uploadPartCount = calculatePartCount(dataStream.chunkCount);
@ -30,7 +30,7 @@ export async function uploadStreamUsingMultipart(
const fileObjectKey = await uploadStreamInParts( const fileObjectKey = await uploadStreamInParts(
multipartUploadURLs, multipartUploadURLs,
dataStream.stream, dataStream.stream,
filename, fileLocalID,
uploadPartCount uploadPartCount
); );
return fileObjectKey; return fileObjectKey;
@ -39,7 +39,7 @@ export async function uploadStreamUsingMultipart(
export async function uploadStreamInParts( export async function uploadStreamInParts(
multipartUploadURLs: MultipartUploadURLs, multipartUploadURLs: MultipartUploadURLs,
dataStream: ReadableStream<Uint8Array>, dataStream: ReadableStream<Uint8Array>,
filename: string, fileLocalID: number,
uploadPartCount: number uploadPartCount: number
) { ) {
const streamReader = dataStream.getReader(); const streamReader = dataStream.getReader();
@ -52,7 +52,7 @@ export async function uploadStreamInParts(
] of multipartUploadURLs.partURLs.entries()) { ] of multipartUploadURLs.partURLs.entries()) {
const uploadChunk = await combineChunksToFormUploadPart(streamReader); const uploadChunk = await combineChunksToFormUploadPart(streamReader);
const progressTracker = UIService.trackUploadProgress( const progressTracker = UIService.trackUploadProgress(
filename, fileLocalID,
percentPerPart, percentPerPart,
index index
); );

View file

@ -29,12 +29,12 @@ export async function getFileType(
): Promise<FileTypeInfo> { ): Promise<FileTypeInfo> {
try { try {
let fileType: FILE_TYPE; let fileType: FILE_TYPE;
const mimeType = await getMimeType(reader, receivedFile); const typeResult = await extractFileType(reader, receivedFile);
const typeParts = mimeType?.split('/'); const mimTypeParts = typeResult.mime?.split('/');
if (typeParts?.length !== 2) { if (mimTypeParts?.length !== 2) {
throw Error(CustomError.TYPE_DETECTION_FAILED); throw Error(CustomError.TYPE_DETECTION_FAILED);
} }
switch (typeParts[0]) { switch (mimTypeParts[0]) {
case TYPE_IMAGE: case TYPE_IMAGE:
fileType = FILE_TYPE.IMAGE; fileType = FILE_TYPE.IMAGE;
break; break;
@ -44,7 +44,7 @@ export async function getFileType(
default: default:
fileType = FILE_TYPE.OTHERS; fileType = FILE_TYPE.OTHERS;
} }
return { fileType, exactType: typeParts[1] }; return { fileType, exactType: typeResult.ext };
} catch (e) { } catch (e) {
const fileFormat = getFileExtension(receivedFile.name); const fileFormat = getFileExtension(receivedFile.name);
const formatMissedByTypeDetection = FORMAT_MISSED_BY_FILE_TYPE_LIB.find( const formatMissedByTypeDetection = FORMAT_MISSED_BY_FILE_TYPE_LIB.find(
@ -85,16 +85,15 @@ export function getFileOriginalName(file: File) {
return originalName; return originalName;
} }
async function getMimeType(reader: FileReader, file: File) { async function extractFileType(reader: FileReader, file: File) {
const fileChunkBlob = file.slice(0, CHUNK_SIZE_FOR_TYPE_DETECTION); const fileChunkBlob = file.slice(0, CHUNK_SIZE_FOR_TYPE_DETECTION);
return getMimeTypeFromBlob(reader, fileChunkBlob); return getFileTypeFromBlob(reader, fileChunkBlob);
} }
export async function getMimeTypeFromBlob(reader: FileReader, fileBlob: Blob) { export async function getFileTypeFromBlob(reader: FileReader, fileBlob: Blob) {
try { try {
const initialFiledata = await getUint8ArrayView(reader, fileBlob); const initialFiledata = await getUint8ArrayView(reader, fileBlob);
const result = await FileType.fromBuffer(initialFiledata); return await FileType.fromBuffer(initialFiledata);
return result.mime;
} catch (e) { } catch (e) {
throw Error(CustomError.TYPE_DETECTION_FAILED); throw Error(CustomError.TYPE_DETECTION_FAILED);
} }

View file

@ -23,7 +23,6 @@ interface Dimension {
} }
export async function generateThumbnail( export async function generateThumbnail(
worker,
reader: FileReader, reader: FileReader,
file: File, file: File,
fileTypeInfo: FileTypeInfo fileTypeInfo: FileTypeInfo
@ -35,13 +34,12 @@ export async function generateThumbnail(
try { try {
if (fileTypeInfo.fileType === FILE_TYPE.IMAGE) { if (fileTypeInfo.fileType === FILE_TYPE.IMAGE) {
const isHEIC = isFileHEIC(fileTypeInfo.exactType); const isHEIC = isFileHEIC(fileTypeInfo.exactType);
canvas = await generateImageThumbnail(worker, file, isHEIC); canvas = await generateImageThumbnail(file, isHEIC);
} else { } else {
try { try {
const thumb = await FFmpegService.generateThumbnail(file); const thumb = await FFmpegService.generateThumbnail(file);
const dummyImageFile = new File([thumb], file.name); const dummyImageFile = new File([thumb], file.name);
canvas = await generateImageThumbnail( canvas = await generateImageThumbnail(
worker,
dummyImageFile, dummyImageFile,
false false
); );
@ -73,11 +71,7 @@ export async function generateThumbnail(
} }
} }
export async function generateImageThumbnail( export async function generateImageThumbnail(file: File, isHEIC: boolean) {
worker,
file: File,
isHEIC: boolean
) {
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
const canvasCTX = canvas.getContext('2d'); const canvasCTX = canvas.getContext('2d');

View file

@ -9,8 +9,8 @@ class UIService {
private perFileProgress: number; private perFileProgress: number;
private filesUploaded: number; private filesUploaded: number;
private totalFileCount: number; private totalFileCount: number;
private fileProgress: Map<string, number>; private fileProgress: Map<number, number>;
private uploadResult: Map<string, FileUploadResults>; private uploadResult: Map<number, FileUploadResults>;
private progressUpdater: ProgressUpdater; private progressUpdater: ProgressUpdater;
init(progressUpdater: ProgressUpdater) { init(progressUpdater: ProgressUpdater) {
@ -20,8 +20,8 @@ class UIService {
reset(count: number) { reset(count: number) {
this.setTotalFileCount(count); this.setTotalFileCount(count);
this.filesUploaded = 0; this.filesUploaded = 0;
this.fileProgress = new Map<string, number>(); this.fileProgress = new Map<number, number>();
this.uploadResult = new Map<string, number>(); this.uploadResult = new Map<number, FileUploadResults>();
this.updateProgressBarUI(); this.updateProgressBarUI();
} }
@ -30,8 +30,8 @@ class UIService {
this.perFileProgress = 100 / this.totalFileCount; this.perFileProgress = 100 / this.totalFileCount;
} }
setFileProgress(filename: string, progress: number) { setFileProgress(key: number, progress: number) {
this.fileProgress.set(filename, progress); this.fileProgress.set(key, progress);
this.updateProgressBarUI(); this.updateProgressBarUI();
} }
@ -43,14 +43,22 @@ class UIService {
this.progressUpdater.setPercentComplete(percent); this.progressUpdater.setPercentComplete(percent);
} }
setFilenames(filenames: Map<number, string>) {
this.progressUpdater.setFilenames(filenames);
}
setHasLivePhoto(hasLivePhoto: boolean) {
this.progressUpdater.setHasLivePhotos(hasLivePhoto);
}
increaseFileUploaded() { increaseFileUploaded() {
this.filesUploaded++; this.filesUploaded++;
this.updateProgressBarUI(); this.updateProgressBarUI();
} }
moveFileToResultList(filename: string, uploadResult: FileUploadResults) { moveFileToResultList(key: number, uploadResult: FileUploadResults) {
this.uploadResult.set(filename, uploadResult); this.uploadResult.set(key, uploadResult);
this.fileProgress.delete(filename); this.fileProgress.delete(key);
this.updateProgressBarUI(); this.updateProgressBarUI();
} }
@ -82,7 +90,7 @@ class UIService {
} }
trackUploadProgress( trackUploadProgress(
filename: string, fileLocalID: number,
percentPerPart = RANDOM_PERCENTAGE_PROGRESS_FOR_PUT(), percentPerPart = RANDOM_PERCENTAGE_PROGRESS_FOR_PUT(),
index = 0 index = 0
) { ) {
@ -97,14 +105,12 @@ class UIService {
return { return {
cancel, cancel,
onUploadProgress: (event) => { onUploadProgress: (event) => {
filename &&
this.fileProgress.set( this.fileProgress.set(
filename, fileLocalID,
Math.min( Math.min(
Math.round( Math.round(
percentPerPart * index + percentPerPart * index +
(percentPerPart * event.loaded) / (percentPerPart * event.loaded) / event.total
event.total
), ),
98 98
) )

View file

@ -8,8 +8,8 @@ import {
removeUnnecessaryFileProps, removeUnnecessaryFileProps,
} from 'utils/file'; } from 'utils/file';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
import { getMetadataMapKey, parseMetadataJSON } from './metadataService'; import { getMetadataJSONMapKey, parseMetadataJSON } from './metadataService';
import { segregateFiles } from 'utils/upload'; import { segregateMetadataAndMediaFiles } from 'utils/upload';
import uploader from './uploader'; import uploader from './uploader';
import UIService from './uiService'; import UIService from './uiService';
import UploadService from './uploadService'; import UploadService from './uploadService';
@ -18,19 +18,28 @@ import { Collection } from 'types/collection';
import { EnteFile } from 'types/file'; import { EnteFile } from 'types/file';
import { import {
FileWithCollection, FileWithCollection,
MetadataMap, MetadataAndFileTypeInfo,
MetadataAndFileTypeInfoMap,
ParsedMetadataJSON, ParsedMetadataJSON,
ParsedMetadataJSONMap,
ProgressUpdater, ProgressUpdater,
} from 'types/upload'; } from 'types/upload';
import { UPLOAD_STAGES, FileUploadResults } from 'constants/upload'; import {
UPLOAD_STAGES,
FileUploadResults,
MAX_FILE_SIZE_SUPPORTED,
} from 'constants/upload';
import { ComlinkWorker } from 'utils/comlink'; import { ComlinkWorker } from 'utils/comlink';
import { FILE_TYPE } from 'constants/file';
import uiService from './uiService';
const MAX_CONCURRENT_UPLOADS = 4; const MAX_CONCURRENT_UPLOADS = 4;
const FILE_UPLOAD_COMPLETED = 100; const FILE_UPLOAD_COMPLETED = 100;
class UploadManager { class UploadManager {
private cryptoWorkers = new Array<ComlinkWorker>(MAX_CONCURRENT_UPLOADS); private cryptoWorkers = new Array<ComlinkWorker>(MAX_CONCURRENT_UPLOADS);
private metadataMap: MetadataMap; private parsedMetadataJSONMap: ParsedMetadataJSONMap;
private metadataAndFileTypeInfoMap: MetadataAndFileTypeInfoMap;
private filesToBeUploaded: FileWithCollection[]; private filesToBeUploaded: FileWithCollection[];
private failedFiles: FileWithCollection[]; private failedFiles: FileWithCollection[];
private existingFilesCollectionWise: Map<number, EnteFile[]>; private existingFilesCollectionWise: Map<number, EnteFile[]>;
@ -45,7 +54,11 @@ class UploadManager {
private async init(newCollections?: Collection[]) { private async init(newCollections?: Collection[]) {
this.filesToBeUploaded = []; this.filesToBeUploaded = [];
this.failedFiles = []; this.failedFiles = [];
this.metadataMap = new Map<string, ParsedMetadataJSON>(); this.parsedMetadataJSONMap = new Map<string, ParsedMetadataJSON>();
this.metadataAndFileTypeInfoMap = new Map<
number,
MetadataAndFileTypeInfo
>();
this.existingFiles = await getLocalFiles(); this.existingFiles = await getLocalFiles();
this.existingFilesCollectionWise = sortFilesIntoCollections( this.existingFilesCollectionWise = sortFilesIntoCollections(
this.existingFiles this.existingFiles
@ -65,18 +78,38 @@ class UploadManager {
) { ) {
try { try {
await this.init(newCreatedCollections); await this.init(newCreatedCollections);
const { metadataFiles, mediaFiles } = segregateFiles( const { metadataJSONFiles, mediaFiles } =
fileWithCollectionToBeUploaded segregateMetadataAndMediaFiles(fileWithCollectionToBeUploaded);
); if (metadataJSONFiles.length) {
if (metadataFiles.length) {
UIService.setUploadStage( UIService.setUploadStage(
UPLOAD_STAGES.READING_GOOGLE_METADATA_FILES UPLOAD_STAGES.READING_GOOGLE_METADATA_FILES
); );
await this.seedMetadataMap(metadataFiles); await this.parseMetadataJSONFiles(metadataJSONFiles);
UploadService.setParsedMetadataJSONMap(
this.parsedMetadataJSONMap
);
} }
if (mediaFiles.length) { if (mediaFiles.length) {
UIService.setUploadStage(UPLOAD_STAGES.EXTRACTING_METADATA);
await this.extractMetadataFromFiles(mediaFiles);
UploadService.setMetadataAndFileTypeInfoMap(
this.metadataAndFileTypeInfoMap
);
UIService.setUploadStage(UPLOAD_STAGES.START); UIService.setUploadStage(UPLOAD_STAGES.START);
await this.uploadMediaFiles(mediaFiles); const analysedMediaFiles =
UploadService.clusterLivePhotoFiles(mediaFiles);
uiService.setFilenames(
new Map<number, string>(
analysedMediaFiles.map((mediaFile) => [
mediaFile.localID,
UploadService.getAssetName(mediaFile),
])
)
);
UIService.setHasLivePhoto(
mediaFiles.length !== analysedMediaFiles.length
);
await this.uploadMediaFiles(analysedMediaFiles);
} }
UIService.setUploadStage(UPLOAD_STAGES.FINISH); UIService.setUploadStage(UPLOAD_STAGES.FINISH);
UIService.setPercentComplete(FILE_UPLOAD_COMPLETED); UIService.setPercentComplete(FILE_UPLOAD_COMPLETED);
@ -90,27 +123,28 @@ class UploadManager {
} }
} }
private async seedMetadataMap(metadataFiles: FileWithCollection[]) { private async parseMetadataJSONFiles(metadataFiles: FileWithCollection[]) {
try { try {
UIService.reset(metadataFiles.length); UIService.reset(metadataFiles.length);
const reader = new FileReader(); const reader = new FileReader();
for (const fileWithCollection of metadataFiles) { for (const { file, collectionID } of metadataFiles) {
try {
const parsedMetadataJSONWithTitle = await parseMetadataJSON( const parsedMetadataJSONWithTitle = await parseMetadataJSON(
reader, reader,
fileWithCollection.file file
); );
if (parsedMetadataJSONWithTitle) { if (parsedMetadataJSONWithTitle) {
const { title, parsedMetadataJSON } = const { title, parsedMetadataJSON } =
parsedMetadataJSONWithTitle; parsedMetadataJSONWithTitle;
this.metadataMap.set( this.parsedMetadataJSONMap.set(
getMetadataMapKey( getMetadataJSONMapKey(collectionID, title),
fileWithCollection.collectionID, parsedMetadataJSON && { ...parsedMetadataJSON }
title
),
{ ...parsedMetadataJSON }
); );
UIService.increaseFileUploaded(); UIService.increaseFileUploaded();
} }
} catch (e) {
logError(e, 'parsing failed for a file');
}
} }
} catch (e) { } catch (e) {
logError(e, 'error seeding MetadataMap'); logError(e, 'error seeding MetadataMap');
@ -118,11 +152,52 @@ class UploadManager {
} }
} }
private async extractMetadataFromFiles(mediaFiles: FileWithCollection[]) {
try {
UIService.reset(mediaFiles.length);
const reader = new FileReader();
for (const { file, localID, collectionID } of mediaFiles) {
try {
const { fileTypeInfo, metadata } = await (async () => {
if (file.size >= MAX_FILE_SIZE_SUPPORTED) {
return { fileTypeInfo: null, metadata: null };
}
const fileTypeInfo = await UploadService.getFileType(
reader,
file
);
if (fileTypeInfo.fileType === FILE_TYPE.OTHERS) {
return { fileTypeInfo, metadata: null };
}
const metadata =
(await UploadService.extractFileMetadata(
file,
collectionID,
fileTypeInfo
)) || null;
return { fileTypeInfo, metadata };
})();
this.metadataAndFileTypeInfoMap.set(localID, {
fileTypeInfo: fileTypeInfo && { ...fileTypeInfo },
metadata: metadata && { ...metadata },
});
UIService.increaseFileUploaded();
} catch (e) {
logError(e, 'metadata extraction failed for a file');
}
}
} catch (e) {
logError(e, 'error extracting metadata');
// silently ignore the error
}
}
private async uploadMediaFiles(mediaFiles: FileWithCollection[]) { private async uploadMediaFiles(mediaFiles: FileWithCollection[]) {
this.filesToBeUploaded.push(...mediaFiles); this.filesToBeUploaded.push(...mediaFiles);
UIService.reset(mediaFiles.length); UIService.reset(mediaFiles.length);
await UploadService.init(mediaFiles.length, this.metadataMap); await UploadService.setFileCount(mediaFiles.length);
UIService.setUploadStage(UPLOAD_STAGES.UPLOADING); UIService.setUploadStage(UPLOAD_STAGES.UPLOADING);
@ -150,19 +225,15 @@ class UploadManager {
private async uploadNextFileInQueue(worker: any, reader: FileReader) { private async uploadNextFileInQueue(worker: any, reader: FileReader) {
while (this.filesToBeUploaded.length > 0) { while (this.filesToBeUploaded.length > 0) {
const fileWithCollection = this.filesToBeUploaded.pop(); const fileWithCollection = this.filesToBeUploaded.pop();
const { collectionID } = fileWithCollection;
const existingFilesInCollection = const existingFilesInCollection =
this.existingFilesCollectionWise.get( this.existingFilesCollectionWise.get(collectionID) ?? [];
fileWithCollection.collectionID const collection = this.collections.get(collectionID);
) ?? [];
const collection = this.collections.get(
fileWithCollection.collectionID
);
fileWithCollection.collection = collection;
const { fileUploadResult, file } = await uploader( const { fileUploadResult, file } = await uploader(
worker, worker,
reader, reader,
existingFilesInCollection, existingFilesInCollection,
fileWithCollection { ...fileWithCollection, collection }
); );
if (fileUploadResult === FileUploadResults.UPLOADED) { if (fileUploadResult === FileUploadResults.UPLOADED) {
@ -187,7 +258,7 @@ class UploadManager {
} }
UIService.moveFileToResultList( UIService.moveFileToResultList(
fileWithCollection.file.name, fileWithCollection.localID,
fileUploadResult fileUploadResult
); );
UploadService.reducePendingUploadCount(); UploadService.reducePendingUploadCount();

View file

@ -1,124 +1,131 @@
import { Collection } from 'types/collection'; import { Collection } from 'types/collection';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
import UploadHttpClient from './uploadHttpClient'; import UploadHttpClient from './uploadHttpClient';
import { extractMetadata, getMetadataMapKey } from './metadataService'; import { extractFileMetadata, getFilename } from './fileService';
import { generateThumbnail } from './thumbnailService'; import { getFileType } from './readFileService';
import { getFileOriginalName, getFileData } from './readFileService';
import { encryptFiledata } from './encryptionService';
import { uploadStreamUsingMultipart } from './multiPartUploadService';
import UIService from './uiService';
import { handleUploadError } from 'utils/error'; import { handleUploadError } from 'utils/error';
import { import {
B64EncryptionResult, B64EncryptionResult,
BackupedFile, BackupedFile,
EncryptedFile, EncryptedFile,
EncryptionResult,
FileInMemory,
FileTypeInfo, FileTypeInfo,
FileWithCollection,
FileWithMetadata, FileWithMetadata,
isDataStream, isDataStream,
MetadataMap,
Metadata, Metadata,
MetadataAndFileTypeInfo,
MetadataAndFileTypeInfoMap,
ParsedMetadataJSON, ParsedMetadataJSON,
ParsedMetadataJSONMap,
ProcessedFile, ProcessedFile,
UploadAsset,
UploadFile, UploadFile,
UploadURL, UploadURL,
} from 'types/upload'; } from 'types/upload';
import {
clusterLivePhotoFiles,
getLivePhotoName,
getLivePhotoSize,
readLivePhoto,
} from './livePhotoService';
import { encryptFile, getFileSize, readFile } from './fileService';
import { uploadStreamUsingMultipart } from './multiPartUploadService';
import UIService from './uiService';
class UploadService { class UploadService {
private uploadURLs: UploadURL[] = []; private uploadURLs: UploadURL[] = [];
private metadataMap: Map<string, ParsedMetadataJSON>; private parsedMetadataJSONMap: ParsedMetadataJSONMap = new Map<
string,
ParsedMetadataJSON
>();
private metadataAndFileTypeInfoMap: MetadataAndFileTypeInfoMap = new Map<
number,
MetadataAndFileTypeInfo
>();
private pendingUploadCount: number = 0; private pendingUploadCount: number = 0;
async init(fileCount: number, metadataMap: MetadataMap) { async setFileCount(fileCount: number) {
this.pendingUploadCount = fileCount; this.pendingUploadCount = fileCount;
this.metadataMap = metadataMap;
await this.preFetchUploadURLs(); await this.preFetchUploadURLs();
} }
setParsedMetadataJSONMap(parsedMetadataJSONMap: ParsedMetadataJSONMap) {
this.parsedMetadataJSONMap = parsedMetadataJSONMap;
}
setMetadataAndFileTypeInfoMap(
metadataAndFileTypeInfoMap: MetadataAndFileTypeInfoMap
) {
this.metadataAndFileTypeInfoMap = metadataAndFileTypeInfoMap;
}
reducePendingUploadCount() { reducePendingUploadCount() {
this.pendingUploadCount--; this.pendingUploadCount--;
} }
async readFile( getAssetSize({ isLivePhoto, file, livePhotoAssets }: UploadAsset) {
worker: any, return isLivePhoto
reader: FileReader, ? getLivePhotoSize(livePhotoAssets)
rawFile: File, : getFileSize(file);
fileTypeInfo: FileTypeInfo
): Promise<FileInMemory> {
const { thumbnail, hasStaticThumbnail } = await generateThumbnail(
worker,
reader,
rawFile,
fileTypeInfo
);
const filedata = await getFileData(reader, rawFile);
return {
filedata,
thumbnail,
hasStaticThumbnail,
};
} }
async getFileMetadata( getAssetName({ isLivePhoto, file, livePhotoAssets }: FileWithCollection) {
rawFile: File, return isLivePhoto
collection: Collection, ? getLivePhotoName(livePhotoAssets.image.name)
: getFilename(file);
}
async getFileType(reader: FileReader, file: File) {
return getFileType(reader, file);
}
async readAsset(
reader: FileReader,
fileTypeInfo: FileTypeInfo,
{ isLivePhoto, file, livePhotoAssets }: UploadAsset
) {
return isLivePhoto
? await readLivePhoto(reader, fileTypeInfo, livePhotoAssets)
: await readFile(reader, fileTypeInfo, file);
}
async extractFileMetadata(
file: File,
collectionID: number,
fileTypeInfo: FileTypeInfo fileTypeInfo: FileTypeInfo
): Promise<Metadata> { ): Promise<Metadata> {
const originalName = getFileOriginalName(rawFile); return extractFileMetadata(
const googleMetadata = this.parsedMetadataJSONMap,
this.metadataMap.get( file,
getMetadataMapKey(collection.id, originalName) collectionID,
) ?? {};
const extractedMetadata: Metadata = await extractMetadata(
rawFile,
fileTypeInfo fileTypeInfo
); );
for (const [key, value] of Object.entries(googleMetadata)) {
if (!value) {
continue;
}
extractedMetadata[key] = value;
}
return extractedMetadata;
} }
async encryptFile( getFileMetadataAndFileTypeInfo(localID: number) {
return this.metadataAndFileTypeInfoMap.get(localID);
}
setFileMetadataAndFileTypeInfo(
localID: number,
metadataAndFileTypeInfo: MetadataAndFileTypeInfo
) {
return this.metadataAndFileTypeInfoMap.set(
localID,
metadataAndFileTypeInfo
);
}
clusterLivePhotoFiles(mediaFiles: FileWithCollection[]) {
return clusterLivePhotoFiles(mediaFiles);
}
async encryptAsset(
worker: any, worker: any,
file: FileWithMetadata, file: FileWithMetadata,
encryptionKey: string encryptionKey: string
): Promise<EncryptedFile> { ): Promise<EncryptedFile> {
try { return encryptFile(worker, file, encryptionKey);
const { key: fileKey, file: encryptedFiledata } =
await encryptFiledata(worker, file.filedata);
const { file: encryptedThumbnail }: EncryptionResult =
await worker.encryptThumbnail(file.thumbnail, fileKey);
const { file: encryptedMetadata }: EncryptionResult =
await worker.encryptMetadata(file.metadata, fileKey);
const encryptedKey: B64EncryptionResult = await worker.encryptToB64(
fileKey,
encryptionKey
);
const result: EncryptedFile = {
file: {
file: encryptedFiledata,
thumbnail: encryptedThumbnail,
metadata: encryptedMetadata,
filename: file.metadata.title,
},
fileKey: encryptedKey,
};
return result;
} catch (e) {
logError(e, 'Error encrypting files');
throw e;
}
} }
async uploadToBucket(file: ProcessedFile): Promise<BackupedFile> { async uploadToBucket(file: ProcessedFile): Promise<BackupedFile> {
@ -126,12 +133,12 @@ class UploadService {
let fileObjectKey: string = null; let fileObjectKey: string = null;
if (isDataStream(file.file.encryptedData)) { if (isDataStream(file.file.encryptedData)) {
fileObjectKey = await uploadStreamUsingMultipart( fileObjectKey = await uploadStreamUsingMultipart(
file.filename, file.localID,
file.file.encryptedData file.file.encryptedData
); );
} else { } else {
const progressTracker = UIService.trackUploadProgress( const progressTracker = UIService.trackUploadProgress(
file.filename file.localID
); );
const fileUploadURL = await this.getUploadURL(); const fileUploadURL = await this.getUploadURL();
fileObjectKey = await UploadHttpClient.putFile( fileObjectKey = await UploadHttpClient.putFile(

View file

@ -6,22 +6,10 @@ import { fileAlreadyInCollection } from 'utils/upload';
import UploadHttpClient from './uploadHttpClient'; import UploadHttpClient from './uploadHttpClient';
import UIService from './uiService'; import UIService from './uiService';
import UploadService from './uploadService'; import UploadService from './uploadService';
import uploadService from './uploadService';
import { getFileType } from './readFileService';
import {
BackupedFile,
EncryptedFile,
FileInMemory,
FileTypeInfo,
FileWithCollection,
FileWithMetadata,
Metadata,
UploadFile,
} from 'types/upload';
import { FILE_TYPE } from 'constants/file'; import { FILE_TYPE } from 'constants/file';
import { FileUploadResults } from 'constants/upload'; import { FileUploadResults, MAX_FILE_SIZE_SUPPORTED } from 'constants/upload';
import { FileWithCollection, BackupedFile, UploadFile } from 'types/upload';
const FIVE_GB_IN_BYTES = 5 * 1024 * 1024 * 1024;
interface UploadResponse { interface UploadResponse {
fileUploadResult: FileUploadResults; fileUploadResult: FileUploadResults;
file?: EnteFile; file?: EnteFile;
@ -32,50 +20,44 @@ export default async function uploader(
existingFilesInCollection: EnteFile[], existingFilesInCollection: EnteFile[],
fileWithCollection: FileWithCollection fileWithCollection: FileWithCollection
): Promise<UploadResponse> { ): Promise<UploadResponse> {
const { file: rawFile, collection } = fileWithCollection; const { collection, localID, ...uploadAsset } = fileWithCollection;
UIService.setFileProgress(rawFile.name, 0);
let file: FileInMemory = null;
let encryptedFile: EncryptedFile = null;
let metadata: Metadata = null;
let fileTypeInfo: FileTypeInfo = null;
let fileWithMetadata: FileWithMetadata = null;
UIService.setFileProgress(localID, 0);
const { fileTypeInfo, metadata } =
UploadService.getFileMetadataAndFileTypeInfo(localID);
try { try {
if (rawFile.size >= FIVE_GB_IN_BYTES) { const fileSize = UploadService.getAssetSize(uploadAsset);
if (fileSize >= MAX_FILE_SIZE_SUPPORTED) {
return { fileUploadResult: FileUploadResults.TOO_LARGE }; return { fileUploadResult: FileUploadResults.TOO_LARGE };
} }
fileTypeInfo = await getFileType(reader, rawFile);
if (fileTypeInfo.fileType === FILE_TYPE.OTHERS) { if (fileTypeInfo.fileType === FILE_TYPE.OTHERS) {
throw Error(CustomError.UNSUPPORTED_FILE_FORMAT); throw Error(CustomError.UNSUPPORTED_FILE_FORMAT);
} }
metadata = await uploadService.getFileMetadata( if (!metadata) {
rawFile, throw Error(CustomError.NO_METADATA);
collection, }
fileTypeInfo
);
if (fileAlreadyInCollection(existingFilesInCollection, metadata)) { if (fileAlreadyInCollection(existingFilesInCollection, metadata)) {
return { fileUploadResult: FileUploadResults.ALREADY_UPLOADED }; return { fileUploadResult: FileUploadResults.ALREADY_UPLOADED };
} }
file = await UploadService.readFile( const file = await UploadService.readAsset(
worker,
reader, reader,
rawFile, fileTypeInfo,
fileTypeInfo uploadAsset
); );
if (file.hasStaticThumbnail) { if (file.hasStaticThumbnail) {
metadata.hasStaticThumbnail = true; metadata.hasStaticThumbnail = true;
} }
fileWithMetadata = { const fileWithMetadata = {
localID,
filedata: file.filedata, filedata: file.filedata,
thumbnail: file.thumbnail, thumbnail: file.thumbnail,
metadata, metadata,
}; };
encryptedFile = await UploadService.encryptFile( const encryptedFile = await UploadService.encryptAsset(
worker, worker,
fileWithMetadata, fileWithMetadata,
collection.key collection.key
@ -117,9 +99,5 @@ export default async function uploader(
default: default:
return { fileUploadResult: FileUploadResults.FAILED }; return { fileUploadResult: FileUploadResults.FAILED };
} }
} finally {
file = null;
fileWithMetadata = null;
encryptedFile = null;
} }
} }

View file

@ -33,4 +33,10 @@ export type GalleryContextType = {
setDialogMessage: SetDialogMessage; setDialogMessage: SetDialogMessage;
startLoading: () => void; startLoading: () => void;
finishLoading: () => void; finishLoading: () => void;
setNotificationAttributes: (attributes: NotificationAttributes) => void;
}; };
export interface NotificationAttributes {
message: string;
title: string;
}

View file

@ -48,6 +48,8 @@ export interface MultipartUploadURLs {
export interface FileTypeInfo { export interface FileTypeInfo {
fileType: FILE_TYPE; fileType: FILE_TYPE;
exactType: string; exactType: string;
imageType?: string;
videoType?: string;
} }
export interface ProgressUpdater { export interface ProgressUpdater {
@ -59,17 +61,34 @@ export interface ProgressUpdater {
}> }>
>; >;
setUploadStage: React.Dispatch<React.SetStateAction<UPLOAD_STAGES>>; setUploadStage: React.Dispatch<React.SetStateAction<UPLOAD_STAGES>>;
setFileProgress: React.Dispatch<React.SetStateAction<Map<string, number>>>; setFileProgress: React.Dispatch<React.SetStateAction<Map<number, number>>>;
setUploadResult: React.Dispatch<React.SetStateAction<Map<string, number>>>; setUploadResult: React.Dispatch<React.SetStateAction<Map<number, number>>>;
setFilenames: React.Dispatch<React.SetStateAction<Map<number, string>>>;
setHasLivePhotos: React.Dispatch<React.SetStateAction<boolean>>;
} }
export interface FileWithCollection { export interface UploadAsset {
file: File; isLivePhoto?: boolean;
collectionID?: number; file?: File;
livePhotoAssets?: LivePhotoAssets;
}
export interface LivePhotoAssets {
image: globalThis.File;
video: globalThis.File;
}
export interface FileWithCollection extends UploadAsset {
localID: number;
collection?: Collection; collection?: Collection;
collectionID?: number;
}
export interface MetadataAndFileTypeInfo {
metadata: Metadata;
fileTypeInfo: FileTypeInfo;
} }
export type MetadataMap = Map<string, ParsedMetadataJSON>; export type MetadataAndFileTypeInfoMap = Map<number, MetadataAndFileTypeInfo>;
export type ParsedMetadataJSONMap = Map<string, ParsedMetadataJSON>;
export interface UploadURL { export interface UploadURL {
url: string; url: string;
@ -91,6 +110,7 @@ export interface FileInMemory {
export interface FileWithMetadata export interface FileWithMetadata
extends Omit<FileInMemory, 'hasStaticThumbnail'> { extends Omit<FileInMemory, 'hasStaticThumbnail'> {
metadata: Metadata; metadata: Metadata;
localID: number;
} }
export interface EncryptedFile { export interface EncryptedFile {
@ -101,9 +121,9 @@ export interface ProcessedFile {
file: fileAttribute; file: fileAttribute;
thumbnail: fileAttribute; thumbnail: fileAttribute;
metadata: fileAttribute; metadata: fileAttribute;
filename: string; localID: number;
} }
export interface BackupedFile extends Omit<ProcessedFile, 'filename'> {} export interface BackupedFile extends Omit<ProcessedFile, 'localID'> {}
export interface UploadFile extends BackupedFile { export interface UploadFile extends BackupedFile {
collectionID: number; collectionID: number;

View file

@ -38,6 +38,8 @@ export enum CustomError {
BAD_REQUEST = 'bad request', BAD_REQUEST = 'bad request',
SUBSCRIPTION_NEEDED = 'subscription not present', SUBSCRIPTION_NEEDED = 'subscription not present',
NOT_FOUND = 'not found ', NOT_FOUND = 'not found ',
NO_METADATA = 'no metadata',
TOO_LARGE_LIVE_PHOTO_ASSETS = 'too large live photo assets',
} }
function parseUploadErrorCodes(error) { function parseUploadErrorCodes(error) {

View file

@ -7,7 +7,7 @@ import {
PublicMagicMetadataProps, PublicMagicMetadataProps,
} from 'types/file'; } from 'types/file';
import { decodeMotionPhoto } from 'services/motionPhotoService'; import { decodeMotionPhoto } from 'services/motionPhotoService';
import { getMimeTypeFromBlob } from 'services/upload/readFileService'; import { getFileTypeFromBlob } from 'services/upload/readFileService';
import DownloadManager from 'services/downloadManager'; import DownloadManager from 'services/downloadManager';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
import { User } from 'types/user'; import { User } from 'types/user';
@ -48,8 +48,6 @@ export async function downloadFile(
) { ) {
let fileURL: string; let fileURL: string;
let tempURL: string; let tempURL: string;
const a = document.createElement('a');
a.style.display = 'none';
if (accessedThroughSharedURL) { if (accessedThroughSharedURL) {
fileURL = await PublicCollectionDownloadManager.getCachedOriginalFile( fileURL = await PublicCollectionDownloadManager.getCachedOriginalFile(
file file
@ -95,19 +93,35 @@ export async function downloadFile(
tempEditedFileURL = URL.createObjectURL(fileBlob); tempEditedFileURL = URL.createObjectURL(fileBlob);
fileURL = tempEditedFileURL; fileURL = tempEditedFileURL;
} }
let tempImageURL: string;
a.href = fileURL; let tempVideoURL: string;
if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) { if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) {
a.download = fileNameWithoutExtension(file.metadata.title) + '.zip'; const fileBlob = await (await fetch(fileURL)).blob();
const originalName = fileNameWithoutExtension(file.metadata.title);
const motionPhoto = await decodeMotionPhoto(fileBlob, originalName);
tempImageURL = URL.createObjectURL(new Blob([motionPhoto.image]));
tempVideoURL = URL.createObjectURL(new Blob([motionPhoto.video]));
downloadUsingAnchor(motionPhoto.imageNameTitle, tempImageURL);
downloadUsingAnchor(motionPhoto.videoNameTitle, tempVideoURL);
} else { } else {
a.download = file.metadata.title; downloadUsingAnchor(file.metadata.title, fileURL);
} }
tempURL && URL.revokeObjectURL(tempURL);
tempEditedFileURL && URL.revokeObjectURL(tempEditedFileURL);
tempImageURL && URL.revokeObjectURL(tempImageURL);
tempVideoURL && URL.revokeObjectURL(tempVideoURL);
}
function downloadUsingAnchor(name: string, link: string) {
const a = document.createElement('a');
a.style.display = 'none';
a.href = link;
a.download = name;
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 isFileHEIC(mimeType: string) { export function isFileHEIC(mimeType: string) {
@ -326,7 +340,8 @@ export async function convertForPreview(file: EnteFile, fileBlob: Blob) {
const reader = new FileReader(); const reader = new FileReader();
const mimeType = const mimeType =
(await getMimeTypeFromBlob(reader, fileBlob)) ?? typeFromExtension; (await getFileTypeFromBlob(reader, fileBlob))?.mime ??
typeFromExtension;
if (isFileHEIC(mimeType)) { if (isFileHEIC(mimeType)) {
fileBlob = await HEICConverter.convert(fileBlob); fileBlob = await HEICConverter.convert(fileBlob);
} }
@ -548,3 +563,9 @@ export function needsConversionForPreview(file: EnteFile) {
return false; return false;
} }
} }
export const isLivePhoto = (file: EnteFile) =>
file.metadata.fileType === FILE_TYPE.LIVE_PHOTO;
export const isImageOrVideo = (fileType: FILE_TYPE) =>
fileType in [FILE_TYPE.IMAGE, FILE_TYPE.VIDEO];

View file

@ -13,3 +13,11 @@ export const justSignedUp = () =>
export function setJustSignedUp(status) { export function setJustSignedUp(status) {
setData(LS_KEYS.JUST_SIGNED_UP, { status }); setData(LS_KEYS.JUST_SIGNED_UP, { status });
} }
export function getLivePhotoInfoShownCount() {
return getData(LS_KEYS.LIVE_PHOTO_INFO_SHOWN_COUNT)?.count ?? 0;
}
export function setLivePhotoInfoShownCount(count) {
setData(LS_KEYS.LIVE_PHOTO_INFO_SHOWN_COUNT, { count });
}

View file

@ -13,6 +13,7 @@ export enum LS_KEYS {
EXPORT = 'export', EXPORT = 'export',
AnonymizeUserID = 'anonymizedUserID', AnonymizeUserID = 'anonymizedUserID',
THUMBNAIL_FIX_STATE = 'thumbnailFixState', THUMBNAIL_FIX_STATE = 'thumbnailFixState',
LIVE_PHOTO_INFO_SHOWN_COUNT = 'livePhotoInfoShownCount',
} }
export const setData = (key: LS_KEYS, value: object) => { export const setData = (key: LS_KEYS, value: object) => {

View file

@ -99,13 +99,14 @@ const englishConstants = {
ENTER_ALBUM_NAME: 'album name', ENTER_ALBUM_NAME: 'album name',
CLOSE: 'close', CLOSE: 'close',
NO: 'no', NO: 'no',
NOTHING_HERE: 'nothing to see here eyes 👀', NOTHING_HERE: 'nothing to see here yet 👀',
UPLOAD: { UPLOAD: {
0: 'preparing to upload', 0: 'preparing to upload',
1: 'reading google metadata files', 1: 'reading google metadata files',
2: (fileCounter) => 2: 'reading file metadata to organize file',
3: (fileCounter) =>
`${fileCounter.finished} / ${fileCounter.total} files backed up`, `${fileCounter.finished} / ${fileCounter.total} files backed up`,
3: 'backup complete', 4: 'backup complete',
}, },
UPLOADING_FILES: 'file upload', UPLOADING_FILES: 'file upload',
FILE_NOT_UPLOADED_LIST: 'the following files were not uploaded', FILE_NOT_UPLOADED_LIST: 'the following files were not uploaded',
@ -516,6 +517,13 @@ const englishConstants = {
</> </>
), ),
LIVE_PHOTOS_DETECTED: () => (
<p>
the photo and video files from your Live Photos have been merged
into a single ELP file
</p>
),
RETRY_FAILED: 'retry failed uploads', RETRY_FAILED: 'retry failed uploads',
FAILED_UPLOADS: 'failed uploads ', FAILED_UPLOADS: 'failed uploads ',
SKIPPED_FILES: 'ignored uploads', SKIPPED_FILES: 'ignored uploads',
@ -652,6 +660,8 @@ const englishConstants = {
TERM_3: 'I acknowledge that any person who knowingly materially misrepresents that material or activity is infringing may be subject to liability for damages. ', TERM_3: 'I acknowledge that any person who knowingly materially misrepresents that material or activity is infringing may be subject to liability for damages. ',
PRESERVED_BY: 'preserved by', PRESERVED_BY: 'preserved by',
ENTE_IO: 'ente.io', ENTE_IO: 'ente.io',
PLAYBACK_SUPPORT_COMING: 'playback support coming soon...',
LIVE_PHOTO: 'this is a live photo',
}; };
export default englishConstants; export default englishConstants;

View file

@ -30,10 +30,10 @@ export function areFilesSame(
} }
} }
export function segregateFiles( export function segregateMetadataAndMediaFiles(
filesWithCollectionToUpload: FileWithCollection[] filesWithCollectionToUpload: FileWithCollection[]
) { ) {
const metadataFiles: FileWithCollection[] = []; const metadataJSONFiles: FileWithCollection[] = [];
const mediaFiles: FileWithCollection[] = []; const mediaFiles: FileWithCollection[] = [];
filesWithCollectionToUpload.forEach((fileWithCollection) => { filesWithCollectionToUpload.forEach((fileWithCollection) => {
const file = fileWithCollection.file; const file = fileWithCollection.file;
@ -42,10 +42,10 @@ export function segregateFiles(
return; return;
} }
if (file.name.toLowerCase().endsWith(TYPE_JSON)) { if (file.name.toLowerCase().endsWith(TYPE_JSON)) {
metadataFiles.push(fileWithCollection); metadataJSONFiles.push(fileWithCollection);
} else { } else {
mediaFiles.push(fileWithCollection); mediaFiles.push(fileWithCollection);
} }
}); });
return { mediaFiles, metadataFiles }; return { mediaFiles, metadataJSONFiles };
} }

View file

@ -3472,9 +3472,9 @@ fn-name@~3.0.0:
integrity sha512-eNMNr5exLoavuAMhIUVsOKF79SWd/zG104ef6sxBTSw+cZc6BXdQXDvYcGvp0VbxVVSp1XDUNoz7mg1xMtSznA== integrity sha512-eNMNr5exLoavuAMhIUVsOKF79SWd/zG104ef6sxBTSw+cZc6BXdQXDvYcGvp0VbxVVSp1XDUNoz7mg1xMtSznA==
follow-redirects@^1.0.0, follow-redirects@^1.14.0: follow-redirects@^1.0.0, follow-redirects@^1.14.0:
version "1.14.7" version "1.14.8"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.7.tgz#2004c02eb9436eee9a21446a6477debf17e81685" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.8.tgz#016996fb9a11a100566398b1c6839337d7bfa8fc"
integrity sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ== integrity sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==
foreach@^2.0.5: foreach@^2.0.5:
version "2.0.5" version "2.0.5"