Export pending list (#1318)

This commit is contained in:
Abhinav Kumar 2023-08-21 22:00:55 +05:30 committed by GitHub
commit 17c2be3902
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 367 additions and 205 deletions

View file

@ -232,14 +232,14 @@
"CAPTION_PLACEHOLDER": "Add a description",
"LOCATION": "Location",
"SHOW_ON_MAP": "View on OpenStreetMap",
"MAP":"Map",
"MAP_SETTINGS":"Map Settings",
"ENABLE_MAPS":"Enable Maps?",
"ENABLE_MAP":"Enable map",
"DISABLE_MAPS":"Disable Maps?",
"ENABLE_MAP_DESCRIPTION":"<p>This will show your photos on a world map.</p> <p>The map is hosted by <a>OpenStreetMap</a>, and the exact locations of your photos are never shared.</p> <p>You can disable this feature anytime from Settings.</p>",
"DISABLE_MAP_DESCRIPTION":"<p>This will disable the display of your photos on a world map.</p> <p>You can enable this feature anytime from Settings.</p>",
"DISABLE_MAP":"Disable map",
"MAP": "Map",
"MAP_SETTINGS": "Map Settings",
"ENABLE_MAPS": "Enable Maps?",
"ENABLE_MAP": "Enable map",
"DISABLE_MAPS": "Disable Maps?",
"ENABLE_MAP_DESCRIPTION": "<p>This will show your photos on a world map.</p> <p>The map is hosted by <a>OpenStreetMap</a>, and the exact locations of your photos are never shared.</p> <p>You can disable this feature anytime from Settings.</p>",
"DISABLE_MAP_DESCRIPTION": "<p>This will disable the display of your photos on a world map.</p> <p>You can enable this feature anytime from Settings.</p>",
"DISABLE_MAP": "Disable map",
"DETAILS": "Details",
"VIEW_EXIF": "View all EXIF data",
"NO_EXIF": "No EXIF data",
@ -357,24 +357,24 @@
"MODIFY_SHARING": "Modify sharing",
"ADD_COLLABORATORS": "Add collaborators",
"ADD_NEW_EMAIL": "Add a new email",
"shared_with_people_zero" :"Share with specific people",
"shared_with_people_zero": "Share with specific people",
"shared_with_people_one": "Shared with 1 person",
"shared_with_people_other": "Shared with {{count, number}} people",
"participants_zero": "No participants",
"participants_one": "1 participant",
"participants_other": "{{count, number}} participants",
"ADD_VIEWERS":"Add viewers",
"ADD_VIEWERS": "Add viewers",
"PARTICIPANTS": "Participants",
"CHANGE_PERMISSIONS_TO_VIEWER": "<p>{{selectedEmail}} will not be able to add more photos to the album</p> <p>They will still be able to remove photos added by them</p>",
"CHANGE_PERMISSIONS_TO_COLLABORATOR": "{{selectedEmail}} will be able to add photos to the album",
"CONVERT_TO_VIEWER": "Yes, convert to viewer",
"CONVERT_TO_COLLABORATOR": "Yes, convert to collaborator",
"CHANGE_PERMISSION":"Change permission?",
"CHANGE_PERMISSION": "Change permission?",
"REMOVE_PARTICIPANT": "Remove?",
"CONFIRM_REMOVE":"Yes, remove",
"MANAGE":"Manage",
"ADDED_AS":"Added as",
"COLLABORATOR_RIGHTS":"Collaborators can add photos and videos to the shared album",
"CONFIRM_REMOVE": "Yes, remove",
"MANAGE": "Manage",
"ADDED_AS": "Added as",
"COLLABORATOR_RIGHTS": "Collaborators can add photos and videos to the shared album",
"REMOVE_PARTICIPANT_HEAD": "Remove participant",
"OWNER": "Owner",
"COLLABORATORS": "Collaborators",
@ -528,6 +528,9 @@
"STOP_EXPORT": "Stop",
"EXPORT_PROGRESS": "<a>{{progress.success}} / {{progress.total}}</a> items synced",
"MIGRATING_EXPORT": "Preparing...",
"RENAMING_COLLECTION_FOLDERS": "Renaming album folders...",
"TRASHING_DELETED_FILES": "Trashing deleted files...",
"TRASHING_DELETED_COLLECTIONS": "Trashing deleted albums...",
"EXPORT_NOTIFICATION": {
"START": "Export started",
"IN_PROGRESS": "Export already in progress",
@ -564,7 +567,7 @@
"NEWEST_FIRST": "Newest first",
"OLDEST_FIRST": "Oldest first",
"CONVERSION_FAILED_NOTIFICATION_MESSAGE": "This file could not be previewed. Click here to download the original.",
"SELECT_COLLECTION":"Select album",
"PIN_ALBUM":"Pin album",
"UNPIN_ALBUM":"Unpin album"
"SELECT_COLLECTION": "Select album",
"PIN_ALBUM": "Pin album",
"UNPIN_ALBUM": "Unpin album"
}

View file

@ -72,8 +72,7 @@ export default function AuthenticateUserModal({
sx={{ position: 'absolute' }}
attributes={{
title: t('PASSWORD'),
}}
PaperProps={{ sx: { padding: '8px 12px', maxWidth: '320px' } }}>
}}>
<VerifyMasterPasswordForm
buttonText={t('AUTHENTICATE')}
callback={useMasterPassword}

View file

@ -44,6 +44,7 @@ export const CollectionTile = styled('div')`
height: 100%;
pointer-events: none;
}
user-select: none;
`;
export const ActiveIndicator = styled('div')`

View file

@ -1,7 +1,6 @@
import React, { useState } from 'react';
import {
Box,
Breakpoint,
Button,
Dialog,
DialogProps,
@ -14,11 +13,9 @@ import { DialogBoxAttributesV2 } from 'types/dialogBox';
import EnteButton from 'components/EnteButton';
type IProps = React.PropsWithChildren<
Omit<DialogProps, 'onClose' | 'maxSize'> & {
Omit<DialogProps, 'onClose'> & {
onClose: () => void;
attributes: DialogBoxAttributesV2;
size?: Breakpoint;
titleCloseButton?: boolean;
}
>;
@ -40,17 +37,21 @@ export default function DialogBoxV2({
onClose: onClose,
});
const { PaperProps, ...rest } = props;
return (
<Dialog
open={open}
onClose={handleClose}
PaperProps={{
...PaperProps,
sx: {
padding: '8px 12px',
maxWidth: '360px',
...PaperProps?.sx,
},
}}
{...props}>
{...rest}>
<Stack spacing={'36px'} p={'16px'}>
<Stack spacing={'19px'}>
{attributes.icon && (

View file

@ -5,20 +5,34 @@ import {
Stack,
Typography,
} from '@mui/material';
import React from 'react';
import { t } from 'i18next';
import { formatDateTime } from 'utils/time/format';
import { SpaceBetweenFlex } from './Container';
import { formatNumber } from 'utils/number/format';
import ExportPendingList from './ExportPendingList';
import { useState } from 'react';
import LinkButton from './pages/gallery/LinkButton';
import { EnteFile } from 'types/file';
interface Props {
pendingFileCount: number;
pendingExports: EnteFile[];
collectionNameMap: Map<number, string>;
onHide: () => void;
lastExportTime: number;
startExport: () => void;
}
export default function ExportFinished(props: Props) {
const [pendingFileListView, setPendingFileListView] =
useState<boolean>(false);
const openPendingFileList = () => {
setPendingFileListView(true);
};
const closePendingFileList = () => {
setPendingFileListView(false);
};
return (
<>
<DialogContent>
@ -27,9 +41,15 @@ export default function ExportFinished(props: Props) {
<Typography color={'text.muted'}>
{t('PENDING_ITEMS')}
</Typography>
<Typography>
{formatNumber(props.pendingFileCount)}
</Typography>
{props.pendingExports.length ? (
<LinkButton onClick={openPendingFileList}>
{formatNumber(props.pendingExports.length)}
</LinkButton>
) : (
<Typography>
{formatNumber(props.pendingExports.length)}
</Typography>
)}
</SpaceBetweenFlex>
<SpaceBetweenFlex minHeight={'48px'}>
<Typography color="text.muted">
@ -54,6 +74,12 @@ export default function ExportFinished(props: Props) {
{t('EXPORT_AGAIN')}
</Button>
</DialogActions>
<ExportPendingList
pendingExports={props.pendingExports}
collectionNameMap={props.collectionNameMap}
isOpen={pendingFileListView}
onClose={closePendingFileList}
/>
</>
);
}

View file

@ -27,16 +27,33 @@ interface Props {
}
export default function ExportInProgress(props: Props) {
const isLoading = props.exportProgress.total === 0;
const showIndeterminateProgress = () => {
return (
props.exportStage === ExportStage.STARTING ||
props.exportStage === ExportStage.MIGRATION ||
props.exportStage === ExportStage.RENAMING_COLLECTION_FOLDERS ||
props.exportStage === ExportStage.TRASHING_DELETED_FILES ||
props.exportStage === ExportStage.TRASHING_DELETED_COLLECTIONS
);
};
return (
<>
<DialogContent>
<VerticallyCentered>
<Box mb={1.5}>
{isLoading ? (
{props.exportStage === ExportStage.STARTING ? (
t('EXPORT_STARTING')
) : props.exportStage === ExportStage.MIGRATION ? (
t('MIGRATING_EXPORT')
) : props.exportStage ===
ExportStage.RENAMING_COLLECTION_FOLDERS ? (
t('RENAMING_COLLECTION_FOLDERS')
) : props.exportStage ===
ExportStage.TRASHING_DELETED_FILES ? (
t('TRASHING_DELETED_FILES')
) : props.exportStage ===
ExportStage.TRASHING_DELETED_COLLECTIONS ? (
t('TRASHING_DELETED_COLLECTIONS')
) : (
<Trans
i18nKey={'EXPORT_PROGRESS'}
@ -53,7 +70,7 @@ export default function ExportInProgress(props: Props) {
<ProgressBar
style={{ width: '100%' }}
now={
isLoading
showIndeterminateProgress()
? 100
: Math.round(
((props.exportProgress.success +

View file

@ -1,7 +1,7 @@
import isElectron from 'is-electron';
import React, { useEffect, useState, useContext } from 'react';
import exportService from 'services/export';
import { ExportProgress, ExportSettings, FileExportStats } from 'types/export';
import { ExportProgress, ExportSettings } from 'types/export';
import {
Box,
Button,
@ -30,6 +30,7 @@ import { t } from 'i18next';
import LinkButton from './pages/gallery/LinkButton';
import { CustomError } from 'utils/error';
import { addLogLine } from 'utils/logging';
import { EnteFile } from 'types/file';
const ExportFolderPathContainer = styled(LinkButton)`
width: 262px;
@ -44,6 +45,7 @@ const ExportFolderPathContainer = styled(LinkButton)`
interface Props {
show: boolean;
onHide: () => void;
collectionNameMap: Map<number, string>;
}
export default function ExportModal(props: Props) {
const appContext = useContext(AppContext);
@ -55,10 +57,7 @@ export default function ExportModal(props: Props) {
failed: 0,
total: 0,
});
const [fileExportStats, setFileExportStats] = useState<FileExportStats>({
totalCount: 0,
pendingCount: 0,
});
const [pendingExports, setPendingExports] = useState<EnteFile[]>([]);
const [lastExportTime, setLastExportTime] = useState(0);
// ====================
@ -72,8 +71,8 @@ export default function ExportModal(props: Props) {
exportService.setUIUpdaters({
setExportStage,
setExportProgress,
setFileExportStats,
setLastExportTime,
setPendingExports,
});
const exportSettings: ExportSettings =
exportService.getExportSettings();
@ -123,20 +122,20 @@ export default function ExportModal(props: Props) {
const syncExportRecord = async (exportFolder: string): Promise<void> => {
try {
if (!exportService.exportFolderExists(exportFolder)) {
const fileExportStats = await exportService.getFileExportStats(
const pendingExports = await exportService.getPendingExports(
null
);
setFileExportStats(fileExportStats);
setPendingExports(pendingExports);
}
const exportRecord = await exportService.getExportRecord(
exportFolder
);
setExportStage(exportRecord.stage);
setLastExportTime(exportRecord.lastAttemptTimestamp);
const fileExportStats = await exportService.getFileExportStats(
const pendingExports = await exportService.getPendingExports(
exportRecord
);
setFileExportStats(fileExportStats);
setPendingExports(pendingExports);
} catch (e) {
if (e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST) {
logError(e, 'syncExportRecord failed');
@ -219,8 +218,9 @@ export default function ExportModal(props: Props) {
stopExport={stopExport}
onHide={props.onHide}
lastExportTime={lastExportTime}
pendingFileCount={fileExportStats.pendingCount}
exportProgress={exportProgress}
pendingExports={pendingExports}
collectionNameMap={props.collectionNameMap}
/>
</Dialog>
);
@ -306,23 +306,29 @@ const ExportDynamicContent = ({
stopExport,
onHide,
lastExportTime,
pendingFileCount,
exportProgress,
pendingExports,
collectionNameMap,
}: {
exportStage: ExportStage;
startExport: () => void;
stopExport: () => void;
onHide: () => void;
lastExportTime: number;
pendingFileCount: number;
exportProgress: ExportProgress;
pendingExports: EnteFile[];
collectionNameMap: Map<number, string>;
}) => {
switch (exportStage) {
case ExportStage.INIT:
return <ExportInit startExport={startExport} />;
case ExportStage.INPROGRESS:
case ExportStage.MIGRATION:
case ExportStage.STARTING:
case ExportStage.EXPORTING_FILES:
case ExportStage.RENAMING_COLLECTION_FOLDERS:
case ExportStage.TRASHING_DELETED_FILES:
case ExportStage.TRASHING_DELETED_COLLECTIONS:
return (
<ExportInProgress
exportStage={exportStage}
@ -336,7 +342,8 @@ const ExportDynamicContent = ({
<ExportFinished
onHide={onHide}
lastExportTime={lastExportTime}
pendingFileCount={pendingFileCount}
pendingExports={pendingExports}
collectionNameMap={collectionNameMap}
startExport={startExport}
/>
);

View file

@ -0,0 +1,84 @@
import { EnteFile } from 'types/file';
import ItemList from 'components/ItemList';
import DialogBoxV2 from './DialogBoxV2';
import { t } from 'i18next';
import { FlexWrapper } from './Container';
import CollectionCard from './Collections/CollectionCard';
import { ResultPreviewTile } from './Collections/styledComponents';
import { Box, styled } from '@mui/material';
interface Iprops {
isOpen: boolean;
onClose: () => void;
collectionNameMap: Map<number, string>;
pendingExports: EnteFile[];
}
export const ItemContainer = styled('div')`
position: relative;
top: 5px;
display: inline-block;
max-width: 394px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
`;
const ExportPendingList = (props: Iprops) => {
const renderListItem = (file: EnteFile) => {
return (
<FlexWrapper>
<Box sx={{ marginRight: '8px' }}>
<CollectionCard
key={file.id}
coverFile={file}
onClick={() => null}
collectionTile={ResultPreviewTile}
/>
</Box>
<ItemContainer>
{`${props.collectionNameMap.get(file.collectionID)} / ${
file.metadata.title
}`}
</ItemContainer>
</FlexWrapper>
);
};
const getItemTitle = (file: EnteFile) => {
return `${props.collectionNameMap.get(file.collectionID)} / ${
file.metadata.title
}`;
};
const generateItemKey = (file: EnteFile) => {
return `${file.collectionID}-${file.id}`;
};
return (
<DialogBoxV2
open={props.isOpen}
onClose={props.onClose}
PaperProps={{
sx: { maxWidth: '444px' },
}}
attributes={{
title: t('PENDING_ITEMS'),
close: {
action: props.onClose,
text: t('CLOSE'),
},
}}>
<ItemList
maxHeight={240}
itemSize={50}
items={props.pendingExports}
renderListItem={renderListItem}
getItemTitle={getItemTitle}
generateItemKey={generateItemKey}
/>
</DialogBoxV2>
);
};
export default ExportPendingList;

View file

@ -1,37 +0,0 @@
import { Box, Tooltip } from '@mui/material';
import React from 'react';
import { FixedSizeList as List } from 'react-window';
interface Iprops {
fileList: any[];
}
export default function FileList(props: Iprops) {
const Row = ({ index, style }) => (
<Tooltip
PopperProps={{
sx: {
'.MuiTooltip-tooltip.MuiTooltip-tooltip.MuiTooltip-tooltip':
{ marginTop: 0 },
},
}}
title={props.fileList[index]}
placement="bottom-start"
enterDelay={300}
enterNextDelay={100}>
<div style={style}>{props.fileList[index]}</div>
</Tooltip>
);
return (
<Box pl={2}>
<List
height={Math.min(35 * props.fileList.length, 160)}
width={'100%'}
itemSize={35}
itemCount={props.fileList.length}>
{Row}
</List>
</Box>
);
}

View file

@ -0,0 +1,83 @@
import { Box, Tooltip } from '@mui/material';
import React from 'react';
import {
FixedSizeList as List,
ListChildComponentProps,
areEqual,
} from 'react-window';
import memoize from 'memoize-one';
interface Iprops {
items: any[];
generateItemKey: (item: any) => string;
getItemTitle: (item: any) => string;
renderListItem: (item: any) => JSX.Element;
maxHeight?: number;
itemSize?: number;
}
interface ItemData {
renderListItem: (item: any) => JSX.Element;
getItemTitle: (item: any) => string;
items: any[];
}
const createItemData = memoize(
(
renderListItem: (item: any) => JSX.Element,
getItemTitle: (item: any) => string,
items: any[]
): ItemData => ({
renderListItem,
getItemTitle,
items,
})
);
const Row = React.memo(
({ index, style, data }: ListChildComponentProps<ItemData>) => {
const { renderListItem, items, getItemTitle } = data;
return (
<Tooltip
PopperProps={{
sx: {
'.MuiTooltip-tooltip.MuiTooltip-tooltip.MuiTooltip-tooltip':
{
marginTop: 0,
},
},
}}
title={getItemTitle(items[index])}
placement="bottom-start"
enterDelay={300}
enterNextDelay={100}>
<div style={style}>{renderListItem(items[index])}</div>
</Tooltip>
);
},
areEqual
);
export default function ItemList(props: Iprops) {
const itemData = createItemData(
props.renderListItem,
props.getItemTitle,
props.items
);
return (
<Box pl={2}>
<List
itemData={itemData}
height={Math.min(
props.itemSize * props.items.length,
props.maxHeight
)}
width={'100%'}
itemSize={props.itemSize}
itemCount={props.items.length}
itemKey={props.generateItemKey}>
{Row}
</List>
</Box>
);
}

View file

@ -1,5 +1,5 @@
import React, { useContext } from 'react';
import FileList from 'components/FileList';
import ItemList from 'components/ItemList';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import { InProgressItemContainer } from './styledComponents';
import {
@ -18,6 +18,29 @@ export const InProgressSection = () => {
useContext(UploadProgressContext);
const fileList = inProgressUploads ?? [];
const renderListItem = ({ localFileID, progress }) => {
return (
<InProgressItemContainer key={localFileID}>
<span>{uploadFileNames.get(localFileID)}</span>
{uploadStage === UPLOAD_STAGES.UPLOADING && (
<>
{' '}
<span className="separator">{`-`}</span>
<span>{`${progress}%`}</span>
</>
)}
</InProgressItemContainer>
);
};
const getItemTitle = ({ localFileID, progress }) => {
return `${uploadFileNames.get(localFileID)} - ${progress}%`;
};
const generateItemKey = ({ localFileID, progress }) => {
return `${localFileID}-${progress}`;
};
return (
<UploadProgressSection>
<UploadProgressSectionTitle expandIcon={<ExpandMoreIcon />}>
@ -29,19 +52,13 @@ export const InProgressSection = () => {
{hasLivePhotos && (
<SectionInfo>{t('LIVE_PHOTOS_DETECTED')}</SectionInfo>
)}
<FileList
fileList={fileList.map(({ localFileID, progress }) => (
<InProgressItemContainer key={localFileID}>
<span>{uploadFileNames.get(localFileID)}</span>
{uploadStage === UPLOAD_STAGES.UPLOADING && (
<>
{' '}
<span className="separator">{`-`}</span>
<span>{`${progress}%`}</span>
</>
)}
</InProgressItemContainer>
))}
<ItemList
items={fileList}
generateItemKey={generateItemKey}
getItemTitle={getItemTitle}
renderListItem={renderListItem}
maxHeight={160}
itemSize={35}
/>
</UploadProgressSectionContent>
</UploadProgressSection>

View file

@ -1,5 +1,5 @@
import React, { useContext } from 'react';
import FileList from 'components/FileList';
import ItemList from 'components/ItemList';
import { Typography } from '@mui/material';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import { ResultItemContainer } from './styledComponents';
@ -26,6 +26,23 @@ export const ResultSection = (props: ResultSectionProps) => {
if (!fileList?.length) {
return <></>;
}
const renderListItem = (fileID) => {
return (
<ResultItemContainer key={fileID}>
{uploadFileNames.get(fileID)}
</ResultItemContainer>
);
};
const getItemTitle = (fileID) => {
return uploadFileNames.get(fileID);
};
const generateItemKey = (fileID) => {
return fileID;
};
return (
<UploadProgressSection>
<UploadProgressSectionTitle expandIcon={<ExpandMoreIcon />}>
@ -35,12 +52,13 @@ export const ResultSection = (props: ResultSectionProps) => {
{props.sectionInfo && (
<SectionInfo>{props.sectionInfo}</SectionInfo>
)}
<FileList
fileList={fileList.map((fileID) => (
<ResultItemContainer key={fileID}>
{uploadFileNames.get(fileID)}
</ResultItemContainer>
))}
<ItemList
items={fileList}
generateItemKey={generateItemKey}
getItemTitle={getItemTitle}
renderListItem={renderListItem}
maxHeight={160}
itemSize={35}
/>
</UploadProgressSectionContent>
</UploadProgressSection>

View file

@ -197,7 +197,7 @@ const Cont = styled('div')<{ disabled: boolean }>`
position: relative;
flex: 1;
cursor: ${(props) => (props.disabled ? 'not-allowed' : 'pointer')};
user-select: none;
& > img {
object-fit: cover;
max-width: 100%;

View file

@ -4,7 +4,11 @@ export const ENTE_TRASH_FOLDER = 'Trash';
export enum ExportStage {
INIT = 0,
INPROGRESS = 1,
MIGRATION = 2,
FINISHED = 3,
MIGRATION = 1,
STARTING = 2,
EXPORTING_FILES = 3,
TRASHING_DELETED_FILES = 4,
RENAMING_COLLECTION_FOLDERS = 5,
TRASHING_DELETED_COLLECTIONS = 6,
FINISHED = 7,
}

View file

@ -75,9 +75,9 @@ import {
getAppNameAndTitle,
} from 'constants/apps';
import exportService from 'services/export';
import { ExportStage } from 'constants/export';
import { REDIRECTS } from 'constants/redirects';
import { getLocalMapEnabled, setLocalMapEnabled } from 'utils/storage';
import { isExportInProgress } from 'utils/export';
const redirectMap = new Map([
[REDIRECTS.ROADMAP, getRoadmapRedirectURL],
@ -275,7 +275,7 @@ export default function App(props) {
if (exportSettings.continuousExport) {
exportService.enableContinuousExport();
}
if (exportRecord.stage === ExportStage.INPROGRESS) {
if (isExportInProgress(exportRecord.stage)) {
addLogLine('export was in progress, resuming');
exportService.scheduleExport();
}

View file

@ -1032,7 +1032,11 @@ export default function Gallery() {
isInSearchMode={isInSearchMode}
/>
)}
<ExportModal show={exportModalView} onHide={closeExportModal} />
<ExportModal
show={exportModalView}
onHide={closeExportModal}
collectionNameMap={collectionNameMap}
/>
<AuthenticateUserModal
open={authenticateUserModalView}
onClose={closeAuthenticateUserModal}

View file

@ -48,14 +48,13 @@ import {
ExportRecord,
ExportSettings,
ExportUIUpdaters,
FileExportStats,
} from 'types/export';
import { User } from 'types/user';
import { FILE_TYPE, TYPE_JPEG, TYPE_JPG } from 'constants/file';
import { ExportStage } from 'constants/export';
import { ElectronAPIs } from 'types/electron';
import { CustomError } from 'utils/error';
import { addLocalLog, addLogLine } from 'utils/logging';
import { addLogLine } from 'utils/logging';
import { eventBus, Events } from '../events';
import {
getCollectionNameMap,
@ -87,7 +86,7 @@ class ExportService {
setExportProgress: () => {},
setExportStage: () => {},
setLastExportTime: () => {},
setFileExportStats: () => {},
setPendingExports: () => {},
};
private currentExportProgress: ExportProgress = {
total: 0,
@ -230,9 +229,9 @@ class ExportService {
}
}
getFileExportStats = async (
getPendingExports = async (
exportRecord: ExportRecord
): Promise<FileExportStats> => {
): Promise<EnteFile[]> => {
try {
const user: User = getData(LS_KEYS.USER);
const files = [
@ -241,44 +240,11 @@ class ExportService {
];
const userPersonalFiles = getPersonalFiles(files, user);
const collections = await getLocalCollections(true);
const userNonEmptyPersonalCollections =
getNonEmptyPersonalCollections(
collections,
userPersonalFiles,
user
);
const unExportedFiles = getUnExportedFiles(
userPersonalFiles,
exportRecord
);
const deletedExportedFiles = getDeletedExportedFiles(
userPersonalFiles,
exportRecord
);
const renamedCollections = getRenamedExportedCollections(
userNonEmptyPersonalCollections,
exportRecord
);
const deletedCollections = getDeletedExportedCollections(
userNonEmptyPersonalCollections,
exportRecord
);
addLocalLog(
() =>
`personal files:${userPersonalFiles.length} unexported files: ${unExportedFiles.length}, deleted exported files: ${deletedExportedFiles.length}, renamed collections: ${renamedCollections.length}, deleted collections: ${deletedCollections.length}`
);
return {
totalCount: userPersonalFiles.length,
pendingCount:
unExportedFiles.length +
deletedExportedFiles.length +
renamedCollections.length +
deletedCollections.length,
};
return unExportedFiles;
} catch (e) {
logError(e, 'getUpdateFileLists failed');
throw e;
@ -289,22 +255,12 @@ class ExportService {
this.verifyExportFolderExists(exportFolder);
const exportRecord = await this.getExportRecord(exportFolder);
await this.updateExportStage(ExportStage.MIGRATION);
this.updateExportProgress({
success: 0,
failed: 0,
total: 0,
});
await this.runMigration(
exportFolder,
exportRecord,
this.updateExportProgress.bind(this)
);
await this.updateExportStage(ExportStage.INPROGRESS);
this.updateExportProgress({
success: 0,
failed: 0,
total: 0,
});
await this.updateExportStage(ExportStage.STARTING);
}
async postExport() {
@ -319,8 +275,8 @@ class ExportService {
const exportRecord = await this.getExportRecord(exportFolder);
const fileExportStats = await this.getFileExportStats(exportRecord);
this.uiUpdater.setFileExportStats(fileExportStats);
const pendingExports = await this.getPendingExports(exportRecord);
this.uiUpdater.setPendingExports(pendingExports);
} catch (e) {
logError(e, 'postExport failed');
}
@ -442,58 +398,45 @@ class ExportService {
this.uiUpdater.setExportProgress({
success: success,
failed: failed,
total:
removedFileUIDs.length +
filesToExport.length +
deletedExportedCollections.length +
renamedCollections.length,
total: filesToExport.length,
});
const incrementSuccess = () => {
this.updateExportProgress({
success: ++success,
failed: failed,
total:
removedFileUIDs.length +
filesToExport.length +
deletedExportedCollections.length +
renamedCollections.length,
total: filesToExport.length,
});
};
const incrementFailed = () => {
this.updateExportProgress({
success: success,
failed: ++failed,
total:
removedFileUIDs.length +
filesToExport.length +
deletedExportedCollections.length +
renamedCollections.length,
total: filesToExport.length,
});
};
if (renamedCollections?.length > 0) {
this.updateExportStage(ExportStage.RENAMING_COLLECTION_FOLDERS);
addLogLine(`renaming ${renamedCollections.length} collections`);
await this.collectionRenamer(
exportFolder,
collectionIDExportNameMap,
renamedCollections,
incrementSuccess,
incrementFailed,
isCanceled
);
}
if (removedFileUIDs?.length > 0) {
this.updateExportStage(ExportStage.TRASHING_DELETED_FILES);
addLogLine(`trashing ${removedFileUIDs.length} files`);
await this.fileTrasher(
exportFolder,
collectionIDExportNameMap,
removedFileUIDs,
incrementSuccess,
incrementFailed,
isCanceled
);
}
if (filesToExport?.length > 0) {
this.updateExportStage(ExportStage.EXPORTING_FILES);
addLogLine(`exporting ${filesToExport.length} files`);
await this.fileExporter(
filesToExport,
@ -506,14 +449,15 @@ class ExportService {
);
}
if (deletedExportedCollections?.length > 0) {
this.updateExportStage(
ExportStage.TRASHING_DELETED_COLLECTIONS
);
addLogLine(
`removing ${deletedExportedCollections.length} collections`
);
await this.collectionRemover(
deletedExportedCollections,
exportFolder,
incrementSuccess,
incrementFailed,
isCanceled
);
}
@ -532,8 +476,6 @@ class ExportService {
exportFolder: string,
collectionIDExportNameMap: Map<number, string>,
renamedCollections: Collection[],
incrementSuccess: () => void,
incrementFailed: () => void,
isCanceled: CancellationStatus
) {
try {
@ -580,9 +522,7 @@ class ExportService {
addLogLine(
`renaming collection with id ${collection.id} from ${oldCollectionExportName} to ${newCollectionExportName} successful`
);
incrementSuccess();
} catch (e) {
incrementFailed();
logError(e, 'collectionRenamer failed a collection');
if (
e.message ===
@ -609,8 +549,6 @@ class ExportService {
async collectionRemover(
deletedExportedCollectionIDs: number[],
exportFolder: string,
incrementSuccess: () => void,
incrementFailed: () => void,
isCanceled: CancellationStatus
) {
try {
@ -657,9 +595,7 @@ class ExportService {
addLogLine(
`removing collection with id ${collectionID} from export folder successful`
);
incrementSuccess();
} catch (e) {
incrementFailed();
logError(e, 'collectionRemover failed a collection');
if (
e.message ===
@ -779,8 +715,6 @@ class ExportService {
exportDir: string,
collectionIDExportNameMap: Map<number, string>,
removedFileUIDs: string[],
incrementSuccess: () => void,
incrementFailed: () => void,
isCanceled: CancellationStatus
): Promise<void> {
try {
@ -878,9 +812,7 @@ class ExportService {
}
await this.removeFileExportedRecord(exportDir, fileUID);
addLogLine(`trashing file with id ${fileUID} successful`);
incrementSuccess();
} catch (e) {
incrementFailed();
logError(e, 'trashing failed for a file');
if (
e.message ===

View file

@ -1,4 +1,5 @@
import { ExportStage } from 'constants/export';
import { EnteFile } from 'types/file';
export interface ExportProgress {
success: number;
@ -17,11 +18,6 @@ export interface FileExportNames {
[ID: string]: string;
}
export interface FileExportStats {
totalCount: number;
pendingCount: number;
}
export interface ExportRecordV0 {
stage: ExportStage;
lastAttemptTimestamp: number;
@ -66,6 +62,6 @@ export interface ExportSettings {
export interface ExportUIUpdaters {
setExportStage: (stage: ExportStage) => void;
setExportProgress: (progress: ExportProgress) => void;
setFileExportStats: (fileExportStats: FileExportStats) => void;
setLastExportTime: (exportTime: number) => void;
setPendingExports: (pendingExports: EnteFile[]) => void;
}

View file

@ -10,7 +10,11 @@ import { EnteFile } from 'types/file';
import { Metadata } from 'types/upload';
import { splitFilenameAndExtension } from 'utils/file';
import { ENTE_METADATA_FOLDER, ENTE_TRASH_FOLDER } from 'constants/export';
import {
ENTE_METADATA_FOLDER,
ENTE_TRASH_FOLDER,
ExportStage,
} from 'constants/export';
import sanitize from 'sanitize-filename';
import { formatDateTimeShort } from 'utils/time/format';
import { HIDDEN_COLLECTION_NAME } from 'services/collectionService';
@ -305,3 +309,6 @@ export const parseLivePhotoExportName = (
const { image, video } = JSON.parse(livePhotoExportName);
return { image, video };
};
export const isExportInProgress = (exportStage: ExportStage) =>
exportStage > ExportStage.INIT && exportStage < ExportStage.FINISHED;