commit
73283645a8
|
@ -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;
|
||||||
|
|
59
src/components/ToastNotification.tsx
Normal file
59
src/components/ToastNotification.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
31
src/components/icons/LivePhotoIndicatorOverlay.tsx
Normal file
31
src/components/icons/LivePhotoIndicatorOverlay.tsx
Normal 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',
|
||||||
|
};
|
|
@ -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),
|
||||||
})}
|
})}
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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`
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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' });
|
||||||
|
};
|
||||||
|
|
105
src/services/upload/fileService.ts
Normal file
105
src/services/upload/fileService.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
242
src/services/upload/livePhotoService.ts
Normal file
242
src/services/upload/livePhotoService.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
);
|
);
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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');
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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];
|
||||||
|
|
|
@ -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 });
|
||||||
|
}
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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 };
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in a new issue