commit
73283645a8
|
@ -20,6 +20,7 @@ import {
|
|||
changeFileName,
|
||||
downloadFile,
|
||||
formatDateTime,
|
||||
isLivePhoto,
|
||||
splitFilenameAndExtension,
|
||||
updateExistingFilePubMetadata,
|
||||
} from 'utils/file';
|
||||
|
@ -46,6 +47,10 @@ import { MAX_EDITED_FILE_NAME_LENGTH } from 'constants/file';
|
|||
import { sleep } from 'utils/common';
|
||||
import { PublicCollectionGalleryContext } from 'utils/publicCollectionGallery';
|
||||
import { GalleryContext } from 'pages/gallery';
|
||||
import {
|
||||
getLivePhotoInfoShownCount,
|
||||
setLivePhotoInfoShownCount,
|
||||
} from 'utils/storage';
|
||||
|
||||
const SmallLoadingSpinner = () => (
|
||||
<EnteSpinner
|
||||
|
@ -525,6 +530,19 @@ function PhotoSwipe(props: Iprops) {
|
|||
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 { items, currentIndex } = props;
|
||||
const options = {
|
||||
|
@ -587,6 +605,7 @@ function PhotoSwipe(props: Iprops) {
|
|||
photoSwipe.listen('beforeChange', function () {
|
||||
updateInfo.call(this);
|
||||
updateFavButton.call(this);
|
||||
handleLivePhotoNotification.call(this);
|
||||
});
|
||||
photoSwipe.listen('resize', checkExifAvailable);
|
||||
photoSwipe.init();
|
||||
|
@ -608,6 +627,8 @@ function PhotoSwipe(props: Iprops) {
|
|||
videoTag.pause();
|
||||
}
|
||||
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 { 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,
|
||||
}}>
|
||||
<Formik<formValues>
|
||||
initialValues={{ albumName: attributes.autoFilledName }}
|
||||
initialValues={{ albumName: attributes.autoFilledName ?? '' }}
|
||||
validationSchema={Yup.object().shape({
|
||||
albumName: Yup.string().required(constants.REQUIRED),
|
||||
})}
|
||||
|
|
|
@ -11,6 +11,8 @@ import {
|
|||
PublicCollectionGalleryContext,
|
||||
} from 'utils/publicCollectionGallery';
|
||||
import PublicCollectionDownloadManager from 'services/publicCollectionDownloadManager';
|
||||
import LivePhotoIndicatorOverlay from 'components/icons/LivePhotoIndicatorOverlay';
|
||||
import { isLivePhoto } from 'utils/file';
|
||||
|
||||
interface IProps {
|
||||
file: EnteFile;
|
||||
|
@ -283,6 +285,7 @@ export default function PreviewCard(props: IProps) {
|
|||
<InSelectRangeOverLay
|
||||
active={isRangeSelectActive && isInsSelectRange}
|
||||
/>
|
||||
{isLivePhoto(file) && <LivePhotoIndicatorOverlay />}
|
||||
</Cont>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ import { METADATA_FOLDER_NAME } from 'constants/export';
|
|||
import { getUserFacingErrorMessage } from 'utils/error';
|
||||
import { Collection } from 'types/collection';
|
||||
import { SetLoading, SetFiles } from 'types/gallery';
|
||||
import { UPLOAD_STAGES } from 'constants/upload';
|
||||
import { FileUploadResults, UPLOAD_STAGES } from 'constants/upload';
|
||||
import { FileWithCollection } from 'types/upload';
|
||||
|
||||
const FIRST_ALBUM_NAME = 'My First Album';
|
||||
|
@ -54,10 +54,15 @@ export default function Upload(props: Props) {
|
|||
const [uploadStage, setUploadStage] = useState<UPLOAD_STAGES>(
|
||||
UPLOAD_STAGES.START
|
||||
);
|
||||
const [filenames, setFilenames] = useState(new Map<number, string>());
|
||||
const [fileCounter, setFileCounter] = useState({ finished: 0, total: 0 });
|
||||
const [fileProgress, setFileProgress] = useState(new Map<string, number>());
|
||||
const [uploadResult, setUploadResult] = useState(new Map<string, number>());
|
||||
const [fileProgress, setFileProgress] = useState(new Map<number, number>());
|
||||
const [uploadResult, setUploadResult] = useState(
|
||||
new Map<number, FileUploadResults>()
|
||||
);
|
||||
const [percentComplete, setPercentComplete] = useState(0);
|
||||
const [hasLivePhotos, setHasLivePhotos] = useState(false);
|
||||
|
||||
const [choiceModalView, setChoiceModalView] = useState(false);
|
||||
const [analysisResult, setAnalysisResult] = useState<AnalysisResult>({
|
||||
suggestedCollectionName: '',
|
||||
|
@ -74,6 +79,8 @@ export default function Upload(props: Props) {
|
|||
setFileProgress,
|
||||
setUploadResult,
|
||||
setUploadStage,
|
||||
setFilenames,
|
||||
setHasLivePhotos,
|
||||
},
|
||||
props.setFiles
|
||||
);
|
||||
|
@ -107,8 +114,8 @@ export default function Upload(props: Props) {
|
|||
const uploadInit = function () {
|
||||
setUploadStage(UPLOAD_STAGES.START);
|
||||
setFileCounter({ finished: 0, total: 0 });
|
||||
setFileProgress(new Map<string, number>());
|
||||
setUploadResult(new Map<string, number>());
|
||||
setFileProgress(new Map<number, number>());
|
||||
setUploadResult(new Map<number, number>());
|
||||
setPercentComplete(0);
|
||||
props.closeCollectionSelector();
|
||||
setProgressView(true);
|
||||
|
@ -169,8 +176,9 @@ export default function Upload(props: Props) {
|
|||
try {
|
||||
uploadInit();
|
||||
const filesWithCollectionToUpload: FileWithCollection[] =
|
||||
props.acceptedFiles.map((file) => ({
|
||||
props.acceptedFiles.map((file, index) => ({
|
||||
file,
|
||||
localID: index,
|
||||
collectionID: collection.id,
|
||||
}));
|
||||
await uploadFiles(filesWithCollectionToUpload);
|
||||
|
@ -196,18 +204,21 @@ export default function Upload(props: Props) {
|
|||
}
|
||||
try {
|
||||
const existingCollection = await syncCollections();
|
||||
let index = 0;
|
||||
for (const [collectionName, files] of collectionWiseFiles) {
|
||||
const collection = await createAlbum(
|
||||
collectionName,
|
||||
existingCollection
|
||||
);
|
||||
collections.push(collection);
|
||||
for (const file of files) {
|
||||
filesWithCollectionToUpload.push({
|
||||
|
||||
filesWithCollectionToUpload.push(
|
||||
...files.map((file) => ({
|
||||
localID: index++,
|
||||
collectionID: collection.id,
|
||||
file,
|
||||
});
|
||||
}
|
||||
}))
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
setProgressView(false);
|
||||
|
@ -336,9 +347,11 @@ export default function Upload(props: Props) {
|
|||
/>
|
||||
<UploadProgress
|
||||
now={percentComplete}
|
||||
filenames={filenames}
|
||||
fileCounter={fileCounter}
|
||||
uploadStage={uploadStage}
|
||||
fileProgress={fileProgress}
|
||||
hasLivePhotos={hasLivePhotos}
|
||||
show={progressView}
|
||||
closeModal={() => setProgressView(false)}
|
||||
retryFailed={retryFailed}
|
||||
|
|
|
@ -17,13 +17,15 @@ interface Props {
|
|||
now;
|
||||
closeModal;
|
||||
retryFailed;
|
||||
fileProgress: Map<string, number>;
|
||||
fileProgress: Map<number, number>;
|
||||
filenames: Map<number, string>;
|
||||
show;
|
||||
fileRejections: FileRejection[];
|
||||
uploadResult: Map<string, number>;
|
||||
uploadResult: Map<number, FileUploadResults>;
|
||||
hasLivePhotos: boolean;
|
||||
}
|
||||
interface FileProgresses {
|
||||
fileName: string;
|
||||
fileID: number;
|
||||
progress: number;
|
||||
}
|
||||
|
||||
|
@ -72,7 +74,8 @@ const NotUploadSectionHeader = styled.div`
|
|||
`;
|
||||
|
||||
interface ResultSectionProps {
|
||||
fileUploadResultMap: Map<FileUploadResults, string[]>;
|
||||
filenames: Map<number, string>;
|
||||
fileUploadResultMap: Map<FileUploadResults, number[]>;
|
||||
fileUploadResult: FileUploadResults;
|
||||
sectionTitle: any;
|
||||
sectionInfo?: any;
|
||||
|
@ -95,8 +98,8 @@ const ResultSection = (props: ResultSectionProps) => {
|
|||
<SectionInfo>{props.sectionInfo}</SectionInfo>
|
||||
)}
|
||||
<FileList>
|
||||
{fileList.map((fileName) => (
|
||||
<li key={fileName}>{fileName}</li>
|
||||
{fileList.map((fileID) => (
|
||||
<li key={fileID}>{props.filenames.get(fileID)}</li>
|
||||
))}
|
||||
</FileList>
|
||||
</Content>
|
||||
|
@ -106,8 +109,10 @@ const ResultSection = (props: ResultSectionProps) => {
|
|||
};
|
||||
|
||||
interface InProgressProps {
|
||||
filenames: Map<number, string>;
|
||||
sectionTitle: string;
|
||||
fileProgressStatuses: FileProgresses[];
|
||||
sectionInfo?: any;
|
||||
}
|
||||
const InProgressSection = (props: InProgressProps) => {
|
||||
const [listView, setListView] = useState(true);
|
||||
|
@ -126,10 +131,15 @@ const InProgressSection = (props: InProgressProps) => {
|
|||
</SectionTitle>
|
||||
<Collapse isOpened={listView}>
|
||||
<Content>
|
||||
{props.sectionInfo && (
|
||||
<SectionInfo>{props.sectionInfo}</SectionInfo>
|
||||
)}
|
||||
<FileList>
|
||||
{fileList.map(({ fileName, progress }) => (
|
||||
<li key={fileName}>
|
||||
{`${fileName} - ${progress}%`}
|
||||
{fileList.map(({ fileID, progress }) => (
|
||||
<li key={fileID}>
|
||||
{`${props.filenames.get(
|
||||
fileID
|
||||
)} - ${progress}%`}
|
||||
</li>
|
||||
))}
|
||||
</FileList>
|
||||
|
@ -141,26 +151,33 @@ const InProgressSection = (props: InProgressProps) => {
|
|||
|
||||
export default function UploadProgress(props: Props) {
|
||||
const fileProgressStatuses = [] as FileProgresses[];
|
||||
const fileUploadResultMap = new Map<FileUploadResults, string[]>();
|
||||
const fileUploadResultMap = new Map<FileUploadResults, number[]>();
|
||||
let filesNotUploaded = false;
|
||||
|
||||
let sectionInfo = null;
|
||||
if (props.fileProgress) {
|
||||
for (const [fileName, progress] of props.fileProgress) {
|
||||
fileProgressStatuses.push({ fileName, progress });
|
||||
for (const [localID, progress] of props.fileProgress) {
|
||||
fileProgressStatuses.push({
|
||||
fileID: localID,
|
||||
progress,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (props.uploadResult) {
|
||||
for (const [fileName, progress] of props.uploadResult) {
|
||||
for (const [localID, progress] of props.uploadResult) {
|
||||
if (!fileUploadResultMap.has(progress)) {
|
||||
fileUploadResultMap.set(progress, []);
|
||||
}
|
||||
if (progress < 0) {
|
||||
if (progress !== FileUploadResults.UPLOADED) {
|
||||
filesNotUploaded = true;
|
||||
}
|
||||
const fileList = fileUploadResultMap.get(progress);
|
||||
fileUploadResultMap.set(progress, [...fileList, fileName]);
|
||||
|
||||
fileUploadResultMap.set(progress, [...fileList, localID]);
|
||||
}
|
||||
}
|
||||
if (props.hasLivePhotos) {
|
||||
sectionInfo = constants.LIVE_PHOTOS_DETECTED();
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
|
@ -200,11 +217,14 @@ export default function UploadProgress(props: Props) {
|
|||
/>
|
||||
)}
|
||||
<InProgressSection
|
||||
filenames={props.filenames}
|
||||
fileProgressStatuses={fileProgressStatuses}
|
||||
sectionTitle={constants.INPROGRESS_UPLOADS}
|
||||
sectionInfo={sectionInfo}
|
||||
/>
|
||||
|
||||
<ResultSection
|
||||
filenames={props.filenames}
|
||||
fileUploadResultMap={fileUploadResultMap}
|
||||
fileUploadResult={FileUploadResults.UPLOADED}
|
||||
sectionTitle={constants.SUCCESSFUL_UPLOADS}
|
||||
|
@ -218,6 +238,7 @@ export default function UploadProgress(props: Props) {
|
|||
)}
|
||||
|
||||
<ResultSection
|
||||
filenames={props.filenames}
|
||||
fileUploadResultMap={fileUploadResultMap}
|
||||
fileUploadResult={FileUploadResults.BLOCKED}
|
||||
sectionTitle={constants.BLOCKED_UPLOADS}
|
||||
|
@ -226,17 +247,20 @@ export default function UploadProgress(props: Props) {
|
|||
)}
|
||||
/>
|
||||
<ResultSection
|
||||
filenames={props.filenames}
|
||||
fileUploadResultMap={fileUploadResultMap}
|
||||
fileUploadResult={FileUploadResults.FAILED}
|
||||
sectionTitle={constants.FAILED_UPLOADS}
|
||||
/>
|
||||
<ResultSection
|
||||
filenames={props.filenames}
|
||||
fileUploadResultMap={fileUploadResultMap}
|
||||
fileUploadResult={FileUploadResults.ALREADY_UPLOADED}
|
||||
sectionTitle={constants.SKIPPED_FILES}
|
||||
sectionInfo={constants.SKIPPED_INFO}
|
||||
/>
|
||||
<ResultSection
|
||||
filenames={props.filenames}
|
||||
fileUploadResultMap={fileUploadResultMap}
|
||||
fileUploadResult={
|
||||
FileUploadResults.LARGER_THAN_AVAILABLE_STORAGE
|
||||
|
@ -247,12 +271,14 @@ export default function UploadProgress(props: Props) {
|
|||
sectionInfo={constants.LARGER_THAN_AVAILABLE_STORAGE_INFO}
|
||||
/>
|
||||
<ResultSection
|
||||
filenames={props.filenames}
|
||||
fileUploadResultMap={fileUploadResultMap}
|
||||
fileUploadResult={FileUploadResults.UNSUPPORTED}
|
||||
sectionTitle={constants.UNSUPPORTED_FILES}
|
||||
sectionInfo={constants.UNSUPPORTED_INFO}
|
||||
/>
|
||||
<ResultSection
|
||||
filenames={props.filenames}
|
||||
fileUploadResultMap={fileUploadResultMap}
|
||||
fileUploadResult={FileUploadResults.TOO_LARGE}
|
||||
sectionTitle={constants.TOO_LARGE_UPLOADS}
|
||||
|
|
|
@ -25,6 +25,7 @@ export const NULL_LOCATION: Location = { latitude: null, longitude: null };
|
|||
export enum UPLOAD_STAGES {
|
||||
START,
|
||||
READING_GOOGLE_METADATA_FILES,
|
||||
EXTRACTING_METADATA,
|
||||
UPLOADING,
|
||||
FINISH,
|
||||
}
|
||||
|
@ -38,3 +39,7 @@ export enum FileUploadResults {
|
|||
LARGER_THAN_AVAILABLE_STORAGE,
|
||||
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: calc(80% - 3.5rem);
|
||||
}
|
||||
.modal .modal-header, .modal .modal-footer {
|
||||
.modal .modal-header, .modal .modal-footer , .toast-header{
|
||||
border-color: #444 !important;
|
||||
}
|
||||
.modal .modal-header .close {
|
||||
.modal .modal-header .close, .toast-header .close {
|
||||
color: #aaa;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
@ -145,7 +145,11 @@ const GlobalStyles = createGlobalStyle`
|
|||
z-index:2000;
|
||||
opacity:0.8 !important;
|
||||
}
|
||||
.modal .card , .table {
|
||||
|
||||
.toast-header{
|
||||
border-radius:0px !important;
|
||||
}
|
||||
.modal .card , .table , .toast {
|
||||
background-color: #202020;
|
||||
border: none;
|
||||
}
|
||||
|
@ -154,7 +158,7 @@ const GlobalStyles = createGlobalStyle`
|
|||
overflow: hidden;
|
||||
margin: 0 0 5px 0;
|
||||
}
|
||||
.modal-content {
|
||||
.modal-content ,.toast-header{
|
||||
border-radius:15px;
|
||||
background-color:#202020 !important;
|
||||
}
|
||||
|
@ -485,6 +489,7 @@ const GlobalStyles = createGlobalStyle`
|
|||
.form-check-input:hover, .form-check-label :hover{
|
||||
cursor:pointer;
|
||||
}
|
||||
|
||||
`;
|
||||
|
||||
export const LogoImage = styled.img`
|
||||
|
|
|
@ -96,9 +96,15 @@ import FixCreationTime, {
|
|||
} from 'components/FixCreationTime';
|
||||
import { Collection, CollectionAndItsLatestFile } from 'types/collection';
|
||||
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 { VISIBILITY_STATE } from 'constants/file';
|
||||
import ToastNotification from 'components/ToastNotification';
|
||||
|
||||
export const DeadCenter = styled.div`
|
||||
flex: 1;
|
||||
|
@ -125,6 +131,7 @@ const defaultGalleryContext: GalleryContextType = {
|
|||
setDialogMessage: () => null,
|
||||
startLoading: () => null,
|
||||
finishLoading: () => null,
|
||||
setNotificationAttributes: () => null,
|
||||
};
|
||||
|
||||
export const GalleryContext = createContext<GalleryContextType>(
|
||||
|
@ -191,9 +198,14 @@ export default function Gallery() {
|
|||
const [fixCreationTimeAttributes, setFixCreationTimeAttributes] =
|
||||
useState<FixCreationTimeAttributes>(null);
|
||||
|
||||
const [notificationAttributes, setNotificationAttributes] =
|
||||
useState<NotificationAttributes>(null);
|
||||
|
||||
const showPlanSelectorModal = () => setPlanModalView(true);
|
||||
const closeMessageDialog = () => setMessageDialogView(false);
|
||||
|
||||
const clearNotificationAttributes = () => setNotificationAttributes(null);
|
||||
|
||||
useEffect(() => {
|
||||
const key = getKey(SESSION_KEYS.ENCRYPTION_KEY);
|
||||
if (!key) {
|
||||
|
@ -555,6 +567,7 @@ export default function Gallery() {
|
|||
setDialogMessage,
|
||||
startLoading,
|
||||
finishLoading,
|
||||
setNotificationAttributes,
|
||||
}}>
|
||||
<FullScreenDropZone
|
||||
getRootProps={getRootProps}
|
||||
|
@ -577,6 +590,10 @@ export default function Gallery() {
|
|||
setLoading={setLoading}
|
||||
/>
|
||||
<AlertBanner bannerMessage={bannerMessage} />
|
||||
<ToastNotification
|
||||
attributes={notificationAttributes}
|
||||
clearAttributes={clearNotificationAttributes}
|
||||
/>
|
||||
<MessageDialog
|
||||
size="lg"
|
||||
show={messageDialogView}
|
||||
|
|
|
@ -79,7 +79,6 @@ export async function replaceThumbnail(
|
|||
);
|
||||
const fileTypeInfo = await getFileType(reader, dummyImageFile);
|
||||
const { thumbnail: newThumbnail } = await generateThumbnail(
|
||||
worker,
|
||||
reader,
|
||||
dummyImageFile,
|
||||
fileTypeInfo
|
||||
|
|
|
@ -32,3 +32,16 @@ export const decodeMotionPhoto = async (
|
|||
}
|
||||
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,
|
||||
} from 'types/upload';
|
||||
import { NULL_LOCATION } from 'constants/upload';
|
||||
import { splitFilenameAndExtension } from 'utils/file';
|
||||
|
||||
interface ParsedMetadataJSONWithTitle {
|
||||
title: string;
|
||||
|
@ -30,7 +31,9 @@ export async function extractMetadata(
|
|||
}
|
||||
|
||||
const extractedMetadata: Metadata = {
|
||||
title: receivedFile.name,
|
||||
title: `${splitFilenameAndExtension(receivedFile.name)[0]}.${
|
||||
fileTypeInfo.exactType
|
||||
}`,
|
||||
creationTime:
|
||||
exifData?.creationTime ?? receivedFile.lastModified * 1000,
|
||||
modificationTime: receivedFile.lastModified * 1000,
|
||||
|
@ -41,8 +44,11 @@ export async function extractMetadata(
|
|||
return extractedMetadata;
|
||||
}
|
||||
|
||||
export const getMetadataMapKey = (collectionID: number, title: string) =>
|
||||
`${collectionID}_${title}`;
|
||||
export const getMetadataJSONMapKey = (
|
||||
collectionID: number,
|
||||
|
||||
title: string
|
||||
) => `${collectionID}-${title}`;
|
||||
|
||||
export async function parseMetadataJSON(
|
||||
reader: FileReader,
|
||||
|
|
|
@ -20,7 +20,7 @@ function calculatePartCount(chunkCount: number) {
|
|||
return partCount;
|
||||
}
|
||||
export async function uploadStreamUsingMultipart(
|
||||
filename: string,
|
||||
fileLocalID: number,
|
||||
dataStream: DataStream
|
||||
) {
|
||||
const uploadPartCount = calculatePartCount(dataStream.chunkCount);
|
||||
|
@ -30,7 +30,7 @@ export async function uploadStreamUsingMultipart(
|
|||
const fileObjectKey = await uploadStreamInParts(
|
||||
multipartUploadURLs,
|
||||
dataStream.stream,
|
||||
filename,
|
||||
fileLocalID,
|
||||
uploadPartCount
|
||||
);
|
||||
return fileObjectKey;
|
||||
|
@ -39,7 +39,7 @@ export async function uploadStreamUsingMultipart(
|
|||
export async function uploadStreamInParts(
|
||||
multipartUploadURLs: MultipartUploadURLs,
|
||||
dataStream: ReadableStream<Uint8Array>,
|
||||
filename: string,
|
||||
fileLocalID: number,
|
||||
uploadPartCount: number
|
||||
) {
|
||||
const streamReader = dataStream.getReader();
|
||||
|
@ -52,7 +52,7 @@ export async function uploadStreamInParts(
|
|||
] of multipartUploadURLs.partURLs.entries()) {
|
||||
const uploadChunk = await combineChunksToFormUploadPart(streamReader);
|
||||
const progressTracker = UIService.trackUploadProgress(
|
||||
filename,
|
||||
fileLocalID,
|
||||
percentPerPart,
|
||||
index
|
||||
);
|
||||
|
|
|
@ -29,12 +29,12 @@ export async function getFileType(
|
|||
): Promise<FileTypeInfo> {
|
||||
try {
|
||||
let fileType: FILE_TYPE;
|
||||
const mimeType = await getMimeType(reader, receivedFile);
|
||||
const typeParts = mimeType?.split('/');
|
||||
if (typeParts?.length !== 2) {
|
||||
const typeResult = await extractFileType(reader, receivedFile);
|
||||
const mimTypeParts = typeResult.mime?.split('/');
|
||||
if (mimTypeParts?.length !== 2) {
|
||||
throw Error(CustomError.TYPE_DETECTION_FAILED);
|
||||
}
|
||||
switch (typeParts[0]) {
|
||||
switch (mimTypeParts[0]) {
|
||||
case TYPE_IMAGE:
|
||||
fileType = FILE_TYPE.IMAGE;
|
||||
break;
|
||||
|
@ -44,7 +44,7 @@ export async function getFileType(
|
|||
default:
|
||||
fileType = FILE_TYPE.OTHERS;
|
||||
}
|
||||
return { fileType, exactType: typeParts[1] };
|
||||
return { fileType, exactType: typeResult.ext };
|
||||
} catch (e) {
|
||||
const fileFormat = getFileExtension(receivedFile.name);
|
||||
const formatMissedByTypeDetection = FORMAT_MISSED_BY_FILE_TYPE_LIB.find(
|
||||
|
@ -85,16 +85,15 @@ export function getFileOriginalName(file: File) {
|
|||
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);
|
||||
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 {
|
||||
const initialFiledata = await getUint8ArrayView(reader, fileBlob);
|
||||
const result = await FileType.fromBuffer(initialFiledata);
|
||||
return result.mime;
|
||||
return await FileType.fromBuffer(initialFiledata);
|
||||
} catch (e) {
|
||||
throw Error(CustomError.TYPE_DETECTION_FAILED);
|
||||
}
|
||||
|
|
|
@ -23,7 +23,6 @@ interface Dimension {
|
|||
}
|
||||
|
||||
export async function generateThumbnail(
|
||||
worker,
|
||||
reader: FileReader,
|
||||
file: File,
|
||||
fileTypeInfo: FileTypeInfo
|
||||
|
@ -35,13 +34,12 @@ export async function generateThumbnail(
|
|||
try {
|
||||
if (fileTypeInfo.fileType === FILE_TYPE.IMAGE) {
|
||||
const isHEIC = isFileHEIC(fileTypeInfo.exactType);
|
||||
canvas = await generateImageThumbnail(worker, file, isHEIC);
|
||||
canvas = await generateImageThumbnail(file, isHEIC);
|
||||
} else {
|
||||
try {
|
||||
const thumb = await FFmpegService.generateThumbnail(file);
|
||||
const dummyImageFile = new File([thumb], file.name);
|
||||
canvas = await generateImageThumbnail(
|
||||
worker,
|
||||
dummyImageFile,
|
||||
false
|
||||
);
|
||||
|
@ -73,11 +71,7 @@ export async function generateThumbnail(
|
|||
}
|
||||
}
|
||||
|
||||
export async function generateImageThumbnail(
|
||||
worker,
|
||||
file: File,
|
||||
isHEIC: boolean
|
||||
) {
|
||||
export async function generateImageThumbnail(file: File, isHEIC: boolean) {
|
||||
const canvas = document.createElement('canvas');
|
||||
const canvasCTX = canvas.getContext('2d');
|
||||
|
||||
|
|
|
@ -9,8 +9,8 @@ class UIService {
|
|||
private perFileProgress: number;
|
||||
private filesUploaded: number;
|
||||
private totalFileCount: number;
|
||||
private fileProgress: Map<string, number>;
|
||||
private uploadResult: Map<string, FileUploadResults>;
|
||||
private fileProgress: Map<number, number>;
|
||||
private uploadResult: Map<number, FileUploadResults>;
|
||||
private progressUpdater: ProgressUpdater;
|
||||
|
||||
init(progressUpdater: ProgressUpdater) {
|
||||
|
@ -20,8 +20,8 @@ class UIService {
|
|||
reset(count: number) {
|
||||
this.setTotalFileCount(count);
|
||||
this.filesUploaded = 0;
|
||||
this.fileProgress = new Map<string, number>();
|
||||
this.uploadResult = new Map<string, number>();
|
||||
this.fileProgress = new Map<number, number>();
|
||||
this.uploadResult = new Map<number, FileUploadResults>();
|
||||
this.updateProgressBarUI();
|
||||
}
|
||||
|
||||
|
@ -30,8 +30,8 @@ class UIService {
|
|||
this.perFileProgress = 100 / this.totalFileCount;
|
||||
}
|
||||
|
||||
setFileProgress(filename: string, progress: number) {
|
||||
this.fileProgress.set(filename, progress);
|
||||
setFileProgress(key: number, progress: number) {
|
||||
this.fileProgress.set(key, progress);
|
||||
this.updateProgressBarUI();
|
||||
}
|
||||
|
||||
|
@ -43,14 +43,22 @@ class UIService {
|
|||
this.progressUpdater.setPercentComplete(percent);
|
||||
}
|
||||
|
||||
setFilenames(filenames: Map<number, string>) {
|
||||
this.progressUpdater.setFilenames(filenames);
|
||||
}
|
||||
|
||||
setHasLivePhoto(hasLivePhoto: boolean) {
|
||||
this.progressUpdater.setHasLivePhotos(hasLivePhoto);
|
||||
}
|
||||
|
||||
increaseFileUploaded() {
|
||||
this.filesUploaded++;
|
||||
this.updateProgressBarUI();
|
||||
}
|
||||
|
||||
moveFileToResultList(filename: string, uploadResult: FileUploadResults) {
|
||||
this.uploadResult.set(filename, uploadResult);
|
||||
this.fileProgress.delete(filename);
|
||||
moveFileToResultList(key: number, uploadResult: FileUploadResults) {
|
||||
this.uploadResult.set(key, uploadResult);
|
||||
this.fileProgress.delete(key);
|
||||
this.updateProgressBarUI();
|
||||
}
|
||||
|
||||
|
@ -82,7 +90,7 @@ class UIService {
|
|||
}
|
||||
|
||||
trackUploadProgress(
|
||||
filename: string,
|
||||
fileLocalID: number,
|
||||
percentPerPart = RANDOM_PERCENTAGE_PROGRESS_FOR_PUT(),
|
||||
index = 0
|
||||
) {
|
||||
|
@ -97,14 +105,12 @@ class UIService {
|
|||
return {
|
||||
cancel,
|
||||
onUploadProgress: (event) => {
|
||||
filename &&
|
||||
this.fileProgress.set(
|
||||
filename,
|
||||
fileLocalID,
|
||||
Math.min(
|
||||
Math.round(
|
||||
percentPerPart * index +
|
||||
(percentPerPart * event.loaded) /
|
||||
event.total
|
||||
(percentPerPart * event.loaded) / event.total
|
||||
),
|
||||
98
|
||||
)
|
||||
|
|
|
@ -8,8 +8,8 @@ import {
|
|||
removeUnnecessaryFileProps,
|
||||
} from 'utils/file';
|
||||
import { logError } from 'utils/sentry';
|
||||
import { getMetadataMapKey, parseMetadataJSON } from './metadataService';
|
||||
import { segregateFiles } from 'utils/upload';
|
||||
import { getMetadataJSONMapKey, parseMetadataJSON } from './metadataService';
|
||||
import { segregateMetadataAndMediaFiles } from 'utils/upload';
|
||||
import uploader from './uploader';
|
||||
import UIService from './uiService';
|
||||
import UploadService from './uploadService';
|
||||
|
@ -18,19 +18,28 @@ import { Collection } from 'types/collection';
|
|||
import { EnteFile } from 'types/file';
|
||||
import {
|
||||
FileWithCollection,
|
||||
MetadataMap,
|
||||
MetadataAndFileTypeInfo,
|
||||
MetadataAndFileTypeInfoMap,
|
||||
ParsedMetadataJSON,
|
||||
ParsedMetadataJSONMap,
|
||||
ProgressUpdater,
|
||||
} 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 { FILE_TYPE } from 'constants/file';
|
||||
import uiService from './uiService';
|
||||
|
||||
const MAX_CONCURRENT_UPLOADS = 4;
|
||||
const FILE_UPLOAD_COMPLETED = 100;
|
||||
|
||||
class UploadManager {
|
||||
private cryptoWorkers = new Array<ComlinkWorker>(MAX_CONCURRENT_UPLOADS);
|
||||
private metadataMap: MetadataMap;
|
||||
private parsedMetadataJSONMap: ParsedMetadataJSONMap;
|
||||
private metadataAndFileTypeInfoMap: MetadataAndFileTypeInfoMap;
|
||||
private filesToBeUploaded: FileWithCollection[];
|
||||
private failedFiles: FileWithCollection[];
|
||||
private existingFilesCollectionWise: Map<number, EnteFile[]>;
|
||||
|
@ -45,7 +54,11 @@ class UploadManager {
|
|||
private async init(newCollections?: Collection[]) {
|
||||
this.filesToBeUploaded = [];
|
||||
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.existingFilesCollectionWise = sortFilesIntoCollections(
|
||||
this.existingFiles
|
||||
|
@ -65,18 +78,38 @@ class UploadManager {
|
|||
) {
|
||||
try {
|
||||
await this.init(newCreatedCollections);
|
||||
const { metadataFiles, mediaFiles } = segregateFiles(
|
||||
fileWithCollectionToBeUploaded
|
||||
);
|
||||
if (metadataFiles.length) {
|
||||
const { metadataJSONFiles, mediaFiles } =
|
||||
segregateMetadataAndMediaFiles(fileWithCollectionToBeUploaded);
|
||||
if (metadataJSONFiles.length) {
|
||||
UIService.setUploadStage(
|
||||
UPLOAD_STAGES.READING_GOOGLE_METADATA_FILES
|
||||
);
|
||||
await this.seedMetadataMap(metadataFiles);
|
||||
await this.parseMetadataJSONFiles(metadataJSONFiles);
|
||||
UploadService.setParsedMetadataJSONMap(
|
||||
this.parsedMetadataJSONMap
|
||||
);
|
||||
}
|
||||
if (mediaFiles.length) {
|
||||
UIService.setUploadStage(UPLOAD_STAGES.EXTRACTING_METADATA);
|
||||
await this.extractMetadataFromFiles(mediaFiles);
|
||||
UploadService.setMetadataAndFileTypeInfoMap(
|
||||
this.metadataAndFileTypeInfoMap
|
||||
);
|
||||
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.setPercentComplete(FILE_UPLOAD_COMPLETED);
|
||||
|
@ -90,27 +123,28 @@ class UploadManager {
|
|||
}
|
||||
}
|
||||
|
||||
private async seedMetadataMap(metadataFiles: FileWithCollection[]) {
|
||||
private async parseMetadataJSONFiles(metadataFiles: FileWithCollection[]) {
|
||||
try {
|
||||
UIService.reset(metadataFiles.length);
|
||||
const reader = new FileReader();
|
||||
for (const fileWithCollection of metadataFiles) {
|
||||
for (const { file, collectionID } of metadataFiles) {
|
||||
try {
|
||||
const parsedMetadataJSONWithTitle = await parseMetadataJSON(
|
||||
reader,
|
||||
fileWithCollection.file
|
||||
file
|
||||
);
|
||||
if (parsedMetadataJSONWithTitle) {
|
||||
const { title, parsedMetadataJSON } =
|
||||
parsedMetadataJSONWithTitle;
|
||||
this.metadataMap.set(
|
||||
getMetadataMapKey(
|
||||
fileWithCollection.collectionID,
|
||||
title
|
||||
),
|
||||
{ ...parsedMetadataJSON }
|
||||
this.parsedMetadataJSONMap.set(
|
||||
getMetadataJSONMapKey(collectionID, title),
|
||||
parsedMetadataJSON && { ...parsedMetadataJSON }
|
||||
);
|
||||
UIService.increaseFileUploaded();
|
||||
}
|
||||
} catch (e) {
|
||||
logError(e, 'parsing failed for a file');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
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[]) {
|
||||
this.filesToBeUploaded.push(...mediaFiles);
|
||||
UIService.reset(mediaFiles.length);
|
||||
|
||||
await UploadService.init(mediaFiles.length, this.metadataMap);
|
||||
await UploadService.setFileCount(mediaFiles.length);
|
||||
|
||||
UIService.setUploadStage(UPLOAD_STAGES.UPLOADING);
|
||||
|
||||
|
@ -150,19 +225,15 @@ class UploadManager {
|
|||
private async uploadNextFileInQueue(worker: any, reader: FileReader) {
|
||||
while (this.filesToBeUploaded.length > 0) {
|
||||
const fileWithCollection = this.filesToBeUploaded.pop();
|
||||
const { collectionID } = fileWithCollection;
|
||||
const existingFilesInCollection =
|
||||
this.existingFilesCollectionWise.get(
|
||||
fileWithCollection.collectionID
|
||||
) ?? [];
|
||||
const collection = this.collections.get(
|
||||
fileWithCollection.collectionID
|
||||
);
|
||||
fileWithCollection.collection = collection;
|
||||
this.existingFilesCollectionWise.get(collectionID) ?? [];
|
||||
const collection = this.collections.get(collectionID);
|
||||
const { fileUploadResult, file } = await uploader(
|
||||
worker,
|
||||
reader,
|
||||
existingFilesInCollection,
|
||||
fileWithCollection
|
||||
{ ...fileWithCollection, collection }
|
||||
);
|
||||
|
||||
if (fileUploadResult === FileUploadResults.UPLOADED) {
|
||||
|
@ -187,7 +258,7 @@ class UploadManager {
|
|||
}
|
||||
|
||||
UIService.moveFileToResultList(
|
||||
fileWithCollection.file.name,
|
||||
fileWithCollection.localID,
|
||||
fileUploadResult
|
||||
);
|
||||
UploadService.reducePendingUploadCount();
|
||||
|
|
|
@ -1,124 +1,131 @@
|
|||
import { Collection } from 'types/collection';
|
||||
import { logError } from 'utils/sentry';
|
||||
import UploadHttpClient from './uploadHttpClient';
|
||||
import { extractMetadata, getMetadataMapKey } from './metadataService';
|
||||
import { generateThumbnail } from './thumbnailService';
|
||||
import { getFileOriginalName, getFileData } from './readFileService';
|
||||
import { encryptFiledata } from './encryptionService';
|
||||
import { uploadStreamUsingMultipart } from './multiPartUploadService';
|
||||
import UIService from './uiService';
|
||||
import { extractFileMetadata, getFilename } from './fileService';
|
||||
import { getFileType } from './readFileService';
|
||||
import { handleUploadError } from 'utils/error';
|
||||
import {
|
||||
B64EncryptionResult,
|
||||
BackupedFile,
|
||||
EncryptedFile,
|
||||
EncryptionResult,
|
||||
FileInMemory,
|
||||
FileTypeInfo,
|
||||
FileWithCollection,
|
||||
FileWithMetadata,
|
||||
isDataStream,
|
||||
MetadataMap,
|
||||
Metadata,
|
||||
MetadataAndFileTypeInfo,
|
||||
MetadataAndFileTypeInfoMap,
|
||||
ParsedMetadataJSON,
|
||||
ParsedMetadataJSONMap,
|
||||
ProcessedFile,
|
||||
UploadAsset,
|
||||
UploadFile,
|
||||
UploadURL,
|
||||
} 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 {
|
||||
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;
|
||||
|
||||
async init(fileCount: number, metadataMap: MetadataMap) {
|
||||
async setFileCount(fileCount: number) {
|
||||
this.pendingUploadCount = fileCount;
|
||||
this.metadataMap = metadataMap;
|
||||
await this.preFetchUploadURLs();
|
||||
}
|
||||
|
||||
setParsedMetadataJSONMap(parsedMetadataJSONMap: ParsedMetadataJSONMap) {
|
||||
this.parsedMetadataJSONMap = parsedMetadataJSONMap;
|
||||
}
|
||||
|
||||
setMetadataAndFileTypeInfoMap(
|
||||
metadataAndFileTypeInfoMap: MetadataAndFileTypeInfoMap
|
||||
) {
|
||||
this.metadataAndFileTypeInfoMap = metadataAndFileTypeInfoMap;
|
||||
}
|
||||
|
||||
reducePendingUploadCount() {
|
||||
this.pendingUploadCount--;
|
||||
}
|
||||
|
||||
async readFile(
|
||||
worker: any,
|
||||
reader: FileReader,
|
||||
rawFile: File,
|
||||
fileTypeInfo: FileTypeInfo
|
||||
): Promise<FileInMemory> {
|
||||
const { thumbnail, hasStaticThumbnail } = await generateThumbnail(
|
||||
worker,
|
||||
reader,
|
||||
rawFile,
|
||||
fileTypeInfo
|
||||
);
|
||||
|
||||
const filedata = await getFileData(reader, rawFile);
|
||||
|
||||
return {
|
||||
filedata,
|
||||
thumbnail,
|
||||
hasStaticThumbnail,
|
||||
};
|
||||
getAssetSize({ isLivePhoto, file, livePhotoAssets }: UploadAsset) {
|
||||
return isLivePhoto
|
||||
? getLivePhotoSize(livePhotoAssets)
|
||||
: getFileSize(file);
|
||||
}
|
||||
|
||||
async getFileMetadata(
|
||||
rawFile: File,
|
||||
collection: Collection,
|
||||
getAssetName({ isLivePhoto, file, livePhotoAssets }: FileWithCollection) {
|
||||
return isLivePhoto
|
||||
? 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
|
||||
): Promise<Metadata> {
|
||||
const originalName = getFileOriginalName(rawFile);
|
||||
const googleMetadata =
|
||||
this.metadataMap.get(
|
||||
getMetadataMapKey(collection.id, originalName)
|
||||
) ?? {};
|
||||
const extractedMetadata: Metadata = await extractMetadata(
|
||||
rawFile,
|
||||
return extractFileMetadata(
|
||||
this.parsedMetadataJSONMap,
|
||||
file,
|
||||
collectionID,
|
||||
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,
|
||||
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,
|
||||
filename: file.metadata.title,
|
||||
},
|
||||
fileKey: encryptedKey,
|
||||
};
|
||||
return result;
|
||||
} catch (e) {
|
||||
logError(e, 'Error encrypting files');
|
||||
throw e;
|
||||
}
|
||||
return encryptFile(worker, file, encryptionKey);
|
||||
}
|
||||
|
||||
async uploadToBucket(file: ProcessedFile): Promise<BackupedFile> {
|
||||
|
@ -126,12 +133,12 @@ class UploadService {
|
|||
let fileObjectKey: string = null;
|
||||
if (isDataStream(file.file.encryptedData)) {
|
||||
fileObjectKey = await uploadStreamUsingMultipart(
|
||||
file.filename,
|
||||
file.localID,
|
||||
file.file.encryptedData
|
||||
);
|
||||
} else {
|
||||
const progressTracker = UIService.trackUploadProgress(
|
||||
file.filename
|
||||
file.localID
|
||||
);
|
||||
const fileUploadURL = await this.getUploadURL();
|
||||
fileObjectKey = await UploadHttpClient.putFile(
|
||||
|
|
|
@ -6,22 +6,10 @@ import { fileAlreadyInCollection } from 'utils/upload';
|
|||
import UploadHttpClient from './uploadHttpClient';
|
||||
import UIService from './uiService';
|
||||
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 { 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 {
|
||||
fileUploadResult: FileUploadResults;
|
||||
file?: EnteFile;
|
||||
|
@ -32,50 +20,44 @@ export default async function uploader(
|
|||
existingFilesInCollection: EnteFile[],
|
||||
fileWithCollection: FileWithCollection
|
||||
): Promise<UploadResponse> {
|
||||
const { file: rawFile, collection } = 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;
|
||||
const { collection, localID, ...uploadAsset } = fileWithCollection;
|
||||
|
||||
UIService.setFileProgress(localID, 0);
|
||||
const { fileTypeInfo, metadata } =
|
||||
UploadService.getFileMetadataAndFileTypeInfo(localID);
|
||||
try {
|
||||
if (rawFile.size >= FIVE_GB_IN_BYTES) {
|
||||
const fileSize = UploadService.getAssetSize(uploadAsset);
|
||||
if (fileSize >= MAX_FILE_SIZE_SUPPORTED) {
|
||||
return { fileUploadResult: FileUploadResults.TOO_LARGE };
|
||||
}
|
||||
fileTypeInfo = await getFileType(reader, rawFile);
|
||||
if (fileTypeInfo.fileType === FILE_TYPE.OTHERS) {
|
||||
throw Error(CustomError.UNSUPPORTED_FILE_FORMAT);
|
||||
}
|
||||
metadata = await uploadService.getFileMetadata(
|
||||
rawFile,
|
||||
collection,
|
||||
fileTypeInfo
|
||||
);
|
||||
if (!metadata) {
|
||||
throw Error(CustomError.NO_METADATA);
|
||||
}
|
||||
|
||||
if (fileAlreadyInCollection(existingFilesInCollection, metadata)) {
|
||||
return { fileUploadResult: FileUploadResults.ALREADY_UPLOADED };
|
||||
}
|
||||
|
||||
file = await UploadService.readFile(
|
||||
worker,
|
||||
const file = await UploadService.readAsset(
|
||||
reader,
|
||||
rawFile,
|
||||
fileTypeInfo
|
||||
fileTypeInfo,
|
||||
uploadAsset
|
||||
);
|
||||
|
||||
if (file.hasStaticThumbnail) {
|
||||
metadata.hasStaticThumbnail = true;
|
||||
}
|
||||
fileWithMetadata = {
|
||||
const fileWithMetadata = {
|
||||
localID,
|
||||
filedata: file.filedata,
|
||||
thumbnail: file.thumbnail,
|
||||
metadata,
|
||||
};
|
||||
|
||||
encryptedFile = await UploadService.encryptFile(
|
||||
const encryptedFile = await UploadService.encryptAsset(
|
||||
worker,
|
||||
fileWithMetadata,
|
||||
collection.key
|
||||
|
@ -117,9 +99,5 @@ export default async function uploader(
|
|||
default:
|
||||
return { fileUploadResult: FileUploadResults.FAILED };
|
||||
}
|
||||
} finally {
|
||||
file = null;
|
||||
fileWithMetadata = null;
|
||||
encryptedFile = null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,4 +33,10 @@ export type GalleryContextType = {
|
|||
setDialogMessage: SetDialogMessage;
|
||||
startLoading: () => void;
|
||||
finishLoading: () => void;
|
||||
setNotificationAttributes: (attributes: NotificationAttributes) => void;
|
||||
};
|
||||
|
||||
export interface NotificationAttributes {
|
||||
message: string;
|
||||
title: string;
|
||||
}
|
||||
|
|
|
@ -48,6 +48,8 @@ export interface MultipartUploadURLs {
|
|||
export interface FileTypeInfo {
|
||||
fileType: FILE_TYPE;
|
||||
exactType: string;
|
||||
imageType?: string;
|
||||
videoType?: string;
|
||||
}
|
||||
|
||||
export interface ProgressUpdater {
|
||||
|
@ -59,17 +61,34 @@ export interface ProgressUpdater {
|
|||
}>
|
||||
>;
|
||||
setUploadStage: React.Dispatch<React.SetStateAction<UPLOAD_STAGES>>;
|
||||
setFileProgress: React.Dispatch<React.SetStateAction<Map<string, number>>>;
|
||||
setUploadResult: React.Dispatch<React.SetStateAction<Map<string, number>>>;
|
||||
setFileProgress: React.Dispatch<React.SetStateAction<Map<number, 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 {
|
||||
file: File;
|
||||
collectionID?: number;
|
||||
export interface UploadAsset {
|
||||
isLivePhoto?: boolean;
|
||||
file?: File;
|
||||
livePhotoAssets?: LivePhotoAssets;
|
||||
}
|
||||
export interface LivePhotoAssets {
|
||||
image: globalThis.File;
|
||||
video: globalThis.File;
|
||||
}
|
||||
|
||||
export interface FileWithCollection extends UploadAsset {
|
||||
localID: number;
|
||||
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 {
|
||||
url: string;
|
||||
|
@ -91,6 +110,7 @@ export interface FileInMemory {
|
|||
export interface FileWithMetadata
|
||||
extends Omit<FileInMemory, 'hasStaticThumbnail'> {
|
||||
metadata: Metadata;
|
||||
localID: number;
|
||||
}
|
||||
|
||||
export interface EncryptedFile {
|
||||
|
@ -101,9 +121,9 @@ export interface ProcessedFile {
|
|||
file: fileAttribute;
|
||||
thumbnail: 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 {
|
||||
collectionID: number;
|
||||
|
|
|
@ -38,6 +38,8 @@ export enum CustomError {
|
|||
BAD_REQUEST = 'bad request',
|
||||
SUBSCRIPTION_NEEDED = 'subscription not present',
|
||||
NOT_FOUND = 'not found ',
|
||||
NO_METADATA = 'no metadata',
|
||||
TOO_LARGE_LIVE_PHOTO_ASSETS = 'too large live photo assets',
|
||||
}
|
||||
|
||||
function parseUploadErrorCodes(error) {
|
||||
|
|
|
@ -7,7 +7,7 @@ import {
|
|||
PublicMagicMetadataProps,
|
||||
} from 'types/file';
|
||||
import { decodeMotionPhoto } from 'services/motionPhotoService';
|
||||
import { getMimeTypeFromBlob } from 'services/upload/readFileService';
|
||||
import { getFileTypeFromBlob } from 'services/upload/readFileService';
|
||||
import DownloadManager from 'services/downloadManager';
|
||||
import { logError } from 'utils/sentry';
|
||||
import { User } from 'types/user';
|
||||
|
@ -48,8 +48,6 @@ export async function downloadFile(
|
|||
) {
|
||||
let fileURL: string;
|
||||
let tempURL: string;
|
||||
const a = document.createElement('a');
|
||||
a.style.display = 'none';
|
||||
if (accessedThroughSharedURL) {
|
||||
fileURL = await PublicCollectionDownloadManager.getCachedOriginalFile(
|
||||
file
|
||||
|
@ -95,19 +93,35 @@ export async function downloadFile(
|
|||
tempEditedFileURL = URL.createObjectURL(fileBlob);
|
||||
fileURL = tempEditedFileURL;
|
||||
}
|
||||
|
||||
a.href = fileURL;
|
||||
let tempImageURL: string;
|
||||
let tempVideoURL: string;
|
||||
|
||||
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 {
|
||||
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);
|
||||
a.click();
|
||||
a.remove();
|
||||
tempURL && URL.revokeObjectURL(tempURL);
|
||||
tempEditedFileURL && URL.revokeObjectURL(tempEditedFileURL);
|
||||
}
|
||||
|
||||
export function isFileHEIC(mimeType: string) {
|
||||
|
@ -326,7 +340,8 @@ export async function convertForPreview(file: EnteFile, fileBlob: Blob) {
|
|||
const reader = new FileReader();
|
||||
|
||||
const mimeType =
|
||||
(await getMimeTypeFromBlob(reader, fileBlob)) ?? typeFromExtension;
|
||||
(await getFileTypeFromBlob(reader, fileBlob))?.mime ??
|
||||
typeFromExtension;
|
||||
if (isFileHEIC(mimeType)) {
|
||||
fileBlob = await HEICConverter.convert(fileBlob);
|
||||
}
|
||||
|
@ -548,3 +563,9 @@ export function needsConversionForPreview(file: EnteFile) {
|
|||
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) {
|
||||
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',
|
||||
AnonymizeUserID = 'anonymizedUserID',
|
||||
THUMBNAIL_FIX_STATE = 'thumbnailFixState',
|
||||
LIVE_PHOTO_INFO_SHOWN_COUNT = 'livePhotoInfoShownCount',
|
||||
}
|
||||
|
||||
export const setData = (key: LS_KEYS, value: object) => {
|
||||
|
|
|
@ -99,13 +99,14 @@ const englishConstants = {
|
|||
ENTER_ALBUM_NAME: 'album name',
|
||||
CLOSE: 'close',
|
||||
NO: 'no',
|
||||
NOTHING_HERE: 'nothing to see here eyes 👀',
|
||||
NOTHING_HERE: 'nothing to see here yet 👀',
|
||||
UPLOAD: {
|
||||
0: 'preparing to upload',
|
||||
1: 'reading google metadata files',
|
||||
2: (fileCounter) =>
|
||||
2: 'reading file metadata to organize file',
|
||||
3: (fileCounter) =>
|
||||
`${fileCounter.finished} / ${fileCounter.total} files backed up`,
|
||||
3: 'backup complete',
|
||||
4: 'backup complete',
|
||||
},
|
||||
UPLOADING_FILES: 'file upload',
|
||||
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',
|
||||
FAILED_UPLOADS: 'failed 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. ',
|
||||
PRESERVED_BY: 'preserved by',
|
||||
ENTE_IO: 'ente.io',
|
||||
PLAYBACK_SUPPORT_COMING: 'playback support coming soon...',
|
||||
LIVE_PHOTO: 'this is a live photo',
|
||||
};
|
||||
|
||||
export default englishConstants;
|
||||
|
|
|
@ -30,10 +30,10 @@ export function areFilesSame(
|
|||
}
|
||||
}
|
||||
|
||||
export function segregateFiles(
|
||||
export function segregateMetadataAndMediaFiles(
|
||||
filesWithCollectionToUpload: FileWithCollection[]
|
||||
) {
|
||||
const metadataFiles: FileWithCollection[] = [];
|
||||
const metadataJSONFiles: FileWithCollection[] = [];
|
||||
const mediaFiles: FileWithCollection[] = [];
|
||||
filesWithCollectionToUpload.forEach((fileWithCollection) => {
|
||||
const file = fileWithCollection.file;
|
||||
|
@ -42,10 +42,10 @@ export function segregateFiles(
|
|||
return;
|
||||
}
|
||||
if (file.name.toLowerCase().endsWith(TYPE_JSON)) {
|
||||
metadataFiles.push(fileWithCollection);
|
||||
metadataJSONFiles.push(fileWithCollection);
|
||||
} else {
|
||||
mediaFiles.push(fileWithCollection);
|
||||
}
|
||||
});
|
||||
return { mediaFiles, metadataFiles };
|
||||
return { mediaFiles, metadataJSONFiles };
|
||||
}
|
||||
|
|
|
@ -3472,9 +3472,9 @@ fn-name@~3.0.0:
|
|||
integrity sha512-eNMNr5exLoavuAMhIUVsOKF79SWd/zG104ef6sxBTSw+cZc6BXdQXDvYcGvp0VbxVVSp1XDUNoz7mg1xMtSznA==
|
||||
|
||||
follow-redirects@^1.0.0, follow-redirects@^1.14.0:
|
||||
version "1.14.7"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.7.tgz#2004c02eb9436eee9a21446a6477debf17e81685"
|
||||
integrity sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ==
|
||||
version "1.14.8"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.8.tgz#016996fb9a11a100566398b1c6839337d7bfa8fc"
|
||||
integrity sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==
|
||||
|
||||
foreach@^2.0.5:
|
||||
version "2.0.5"
|
||||
|
|
Loading…
Reference in a new issue