Merge branch 'main' into integrate-mute-update-notification-api

This commit is contained in:
Abhinav 2023-01-21 09:55:29 +05:30
commit f4f1de08eb
146 changed files with 3934 additions and 1817 deletions

View file

@ -53,6 +53,7 @@
"react-hooks/rules-of-hooks": "off", "react-hooks/rules-of-hooks": "off",
"react-hooks/exhaustive-deps": "off", "react-hooks/exhaustive-deps": "off",
"@next/next/no-img-element": "off", "@next/next/no-img-element": "off",
"@typescript-eslint/no-unsafe-argument": "off" "@typescript-eslint/no-unsafe-argument": "off",
"jsx-a11y/alt-text": "off"
} }
} }

View file

@ -1,4 +1,4 @@
#!/bin/sh #!/bin/sh
. "$(dirname "$0")/_/husky.sh" . "$(dirname "$0")/_/husky.sh"
npx lint-staged yarn lint-staged

View file

@ -2,9 +2,16 @@
**ente** is a cloud storage provider that provides end-to-end encryption for your data. **ente** is a cloud storage provider that provides end-to-end encryption for your data.
We have open-source apps across [Android](https://github.com/ente-io/frame), [iOS](https://github.com/ente-io/frame), [web](https://github.com/ente-io/bada-frame) and [desktop](https://github.com/ente-io/bhari-frame) that automatically backup your photos and videos. We have open-source apps across
[Android](https://github.com/ente-io/photos-app),
[iOS](https://github.com/ente-io/photos-app),
[web](https://github.com/ente-io/photos-web) and
[desktop](https://github.com/ente-io/photos-desktop) that automatically backup
your photos and videos.
This repository contains the code for our web app, built with a lot of ❤️, and a
little bit of JavaScript.
This repository contains the code for our web app, built with a lot of ❤️, and a little bit of JavaScript.
<br/><br/><br/> <br/><br/><br/>
![App Screenshots](https://user-images.githubusercontent.com/24503581/189914045-9d4e9c44-37c6-4ac6-9e17-d8c37aee1e08.png) ![App Screenshots](https://user-images.githubusercontent.com/24503581/189914045-9d4e9c44-37c6-4ac6-9e17-d8c37aee1e08.png)
@ -30,7 +37,7 @@ The deployed application is accessible @ [web.ente.io](https://web.ente.io).
## 🧑‍💻 Building from source ## 🧑‍💻 Building from source
1. Clone this repository with `git clone git@github.com:ente-io/bada-frame.git` 1. Clone this repository with `git clone https://github.com/ente-io/photos-web.git`
2. Pull in all submodules with `git submodule update --init --recursive` 2. Pull in all submodules with `git submodule update --init --recursive`
3. Install dependencies with `yarn install` 3. Install dependencies with `yarn install`
4. Finally, run the development server with `yarn dev` 4. Finally, run the development server with `yarn dev`
@ -55,7 +62,8 @@ We maintain a public roadmap, that's driven by our community @ [roadmap.ente.io]
If you like this project, please consider upgrading to a paid subscription. If you like this project, please consider upgrading to a paid subscription.
If you would like to motivate us to keep building, you can do so by [starring](https://github.com/ente-io/bada-frame/stargazers) this project. If you would like to motivate us to keep building, you can do so by
[starring](https://github.com/ente-io/photos-web/stargazers) this project.
<br/> <br/>
@ -69,5 +77,5 @@ An important part of our journey is to build better software by consistently lis
--- ---
Cross-browser testing provided by Cross-browser testing provided by
[<img src="https://d98b8t1nnulk5.cloudfront.net/production/images/layout/logo-header.png?1469004780" width="115" height="25">](https://www.browserstack.com/open-source) [<img src="https://d98b8t1nnulk5.cloudfront.net/production/images/layout/logo-header.png?1469004780" width="115" height="25">](https://www.browserstack.com/open-source)

View file

@ -26,7 +26,7 @@ module.exports = {
'style-src': "'self' 'unsafe-inline'", 'style-src': "'self' 'unsafe-inline'",
'font-src ': "'self'; script-src 'self' 'unsafe-eval' blob:", 'font-src ': "'self'; script-src 'self' 'unsafe-eval' blob:",
'connect-src': 'connect-src':
"'self' https://*.ente.io http://localhost:8080 data: blob: https://ente-prod-eu.s3.eu-central-003.backblazeb2.com ", "'self' https://*.ente.io http://localhost:8080 data: blob: https://ente-prod-eu.s3.eu-central-003.backblazeb2.com https://ente-prod-v3.s3.eu-central-2.wasabisys.com/",
'base-uri ': "'self'", 'base-uri ': "'self'",
// to allow worker // to allow worker
'child-src': "'self' blob:", 'child-src': "'self' blob:",

View file

@ -5,12 +5,6 @@ const withBundleAnalyzer = require('@next/bundle-analyzer')({
const { withSentryConfig } = require('@sentry/nextjs'); const { withSentryConfig } = require('@sentry/nextjs');
const { PHASE_DEVELOPMENT_SERVER } = require('next/constants'); const { PHASE_DEVELOPMENT_SERVER } = require('next/constants');
const withTM = require('next-transpile-modules')([
'@mui/material',
'@mui/system',
'@mui/icons-material',
]);
const { const {
getGitSha, getGitSha,
convertToNextHeaderFormat, convertToNextHeaderFormat,
@ -28,40 +22,44 @@ const IS_SENTRY_ENABLED = getIsSentryEnabled();
module.exports = (phase) => module.exports = (phase) =>
withSentryConfig( withSentryConfig(
withBundleAnalyzer( withBundleAnalyzer({
withTM({ transpilePackages: [
compiler: { '@mui/material',
styledComponents: { '@mui/system',
ssr: true, '@mui/icons-material',
displayName: true, ],
}, compiler: {
}, styledComponents: {
env: { ssr: true,
SENTRY_RELEASE: GIT_SHA, displayName: true,
}, },
},
env: {
SENTRY_RELEASE: GIT_SHA,
NEXT_PUBLIC_IS_TEST_APP: process.env.IS_TEST_RELEASE,
},
headers() { headers() {
return [ return [
{ {
// Apply these headers to all routes in your application.... // Apply these headers to all routes in your application....
source: ALL_ROUTES, source: ALL_ROUTES,
headers: convertToNextHeaderFormat({ headers: convertToNextHeaderFormat({
...COOP_COEP_HEADERS, ...COOP_COEP_HEADERS,
...WEB_SECURITY_HEADERS, ...WEB_SECURITY_HEADERS,
...buildCSPHeader(CSP_DIRECTIVES), ...buildCSPHeader(CSP_DIRECTIVES),
}), }),
}, },
]; ];
}, },
// https://dev.to/marcinwosinek/how-to-add-resolve-fallback-to-webpack-5-in-nextjs-10-i6j // https://dev.to/marcinwosinek/how-to-add-resolve-fallback-to-webpack-5-in-nextjs-10-i6j
webpack: (config, { isServer }) => { webpack: (config, { isServer }) => {
if (!isServer) { if (!isServer) {
config.resolve.fallback.fs = false; config.resolve.fallback.fs = false;
} }
return config; return config;
}, },
}) }),
),
{ {
release: GIT_SHA, release: GIT_SHA,
dryRun: phase === PHASE_DEVELOPMENT_SERVER || !IS_SENTRY_ENABLED, dryRun: phase === PHASE_DEVELOPMENT_SERVER || !IS_SENTRY_ENABLED,

View file

@ -6,7 +6,6 @@
"dev": "next dev", "dev": "next dev",
"albums": "next dev -p 3002", "albums": "next dev -p 3002",
"lint": "next lint", "lint": "next lint",
"prebuild": "yarn lint",
"build": "next build", "build": "next build",
"postbuild": "next export", "postbuild": "next export",
"build-analyze": "ANALYZE=true next build", "build-analyze": "ANALYZE=true next build",
@ -38,13 +37,12 @@
"jszip": "3.7.1", "jszip": "3.7.1",
"libsodium-wrappers": "^0.7.8", "libsodium-wrappers": "^0.7.8",
"localforage": "^1.9.0", "localforage": "^1.9.0",
"next": "^12.3.1", "next": "^13.1.2",
"next-transpile-modules": "^9.0.0",
"photoswipe": "file:./thirdparty/photoswipe", "photoswipe": "file:./thirdparty/photoswipe",
"piexifjs": "^1.0.6", "piexifjs": "^1.0.6",
"react": "^17.0.2", "react": "^18.2.0",
"react-bootstrap": "^1.3.0", "react-bootstrap": "^1.3.0",
"react-dom": "^17.0.2", "react-dom": "^18.2.0",
"react-dropzone": "^11.2.4", "react-dropzone": "^11.2.4",
"react-otp-input": "^2.3.1", "react-otp-input": "^2.3.1",
"react-select": "^4.3.1", "react-select": "^4.3.1",
@ -58,6 +56,7 @@
}, },
"devDependencies": { "devDependencies": {
"@next/bundle-analyzer": "^9.5.3", "@next/bundle-analyzer": "^9.5.3",
"@types/bs58": "^4.0.1",
"@types/debounce-promise": "^3.1.3", "@types/debounce-promise": "^3.1.3",
"@types/libsodium-wrappers": "^0.7.8", "@types/libsodium-wrappers": "^0.7.8",
"@types/node": "^14.6.4", "@types/node": "^14.6.4",
@ -72,7 +71,7 @@
"@types/yup": "^0.29.7", "@types/yup": "^0.29.7",
"@typescript-eslint/eslint-plugin": "^5.43.0", "@typescript-eslint/eslint-plugin": "^5.43.0",
"eslint": "^8.28.0", "eslint": "^8.28.0",
"eslint-config-next": "^13.0.4", "eslint-config-next": "^13.0.6",
"eslint-config-prettier": "^8.5.0", "eslint-config-prettier": "^8.5.0",
"husky": "^7.0.1", "husky": "^7.0.1",
"lint-staged": "^11.1.2", "lint-staged": "^11.1.2",

View file

@ -8,5 +8,5 @@
X-Frame-Options: deny X-Frame-Options: deny
X-XSS-Protection: 1; mode=block X-XSS-Protection: 1; mode=block
Referrer-Policy: same-origin Referrer-Policy: same-origin
Content-Security-Policy-Report-Only: default-src 'self'; img-src 'self' blob: data:; media-src 'self' blob:; style-src 'self' 'unsafe-inline'; font-src 'self'; script-src 'self' 'unsafe-eval' blob:; manifest-src 'self'; child-src 'self' blob:; object-src 'none'; connect-src 'self' https://*.ente.io data: blob: https://ente-prod-eu.s3.eu-central-003.backblazeb2.com ; base-uri 'self'; frame-ancestors 'none'; form-action 'none'; report-uri https://csp-reporter.ente.io; report-to https://csp-reporter.ente.io; Content-Security-Policy-Report-Only: default-src 'self'; img-src 'self' blob: data:; media-src 'self' blob:; style-src 'self' 'unsafe-inline'; font-src 'self'; script-src 'self' 'unsafe-eval' blob:; manifest-src 'self'; child-src 'self' blob:; object-src 'none'; connect-src 'self' https://*.ente.io data: blob: https://ente-prod-eu.s3.eu-central-003.backblazeb2.com https://ente-prod-v3.s3.eu-central-2.wasabisys.com/ ; base-uri 'self'; frame-ancestors 'none'; form-action 'none'; report-uri https://csp-reporter.ente.io; report-to https://csp-reporter.ente.io;

View file

@ -12,9 +12,6 @@ export default function CollectionSortOptions(props: CollectionSortProps) {
<SortByOption sortBy={COLLECTION_SORT_BY.NAME}> <SortByOption sortBy={COLLECTION_SORT_BY.NAME}>
{constants.SORT_BY_NAME} {constants.SORT_BY_NAME}
</SortByOption> </SortByOption>
<SortByOption sortBy={COLLECTION_SORT_BY.CREATION_TIME_DESCENDING}>
{constants.SORT_BY_CREATION_TIME_DESCENDING}
</SortByOption>
<SortByOption sortBy={COLLECTION_SORT_BY.CREATION_TIME_ASCENDING}> <SortByOption sortBy={COLLECTION_SORT_BY.CREATION_TIME_ASCENDING}>
{constants.SORT_BY_CREATION_TIME_ASCENDING} {constants.SORT_BY_CREATION_TIME_ASCENDING}
</SortByOption> </SortByOption>

View file

@ -11,13 +11,25 @@ interface Iprops {
export function CollectionInfo({ name, fileCount, endIcon }: Iprops) { export function CollectionInfo({ name, fileCount, endIcon }: Iprops) {
return ( return (
<div> <div>
<Typography variant="h3">{name}</Typography>
<FlexWrapper> <FlexWrapper>
<Typography variant="subtitle">{name}</Typography> <Typography variant="body2" color="text.secondary">
{endIcon && <Box ml={1.5}>{endIcon}</Box>} {constants.PHOTO_COUNT(fileCount)}
</Typography>
{endIcon && (
<Box
sx={{
svg: {
fontSize: '17px',
color: 'text.secondary',
},
}}
ml={1.5}>
{endIcon}
</Box>
)}
</FlexWrapper> </FlexWrapper>
<Typography variant="body2" color="text.secondary">
{constants.PHOTO_COUNT(fileCount)}
</Typography>
</div> </div>
); );
} }

View file

@ -9,7 +9,9 @@ import { shouldShowOptions } from 'utils/collection';
import { CollectionSummaryType } from 'constants/collection'; import { CollectionSummaryType } from 'constants/collection';
import Favorite from '@mui/icons-material/FavoriteRounded'; import Favorite from '@mui/icons-material/FavoriteRounded';
import Delete from '@mui/icons-material/Delete'; import Delete from '@mui/icons-material/Delete';
import { ArchiveOutlined } from '@mui/icons-material'; import ArchiveOutlined from '@mui/icons-material/ArchiveOutlined';
import PeopleIcon from '@mui/icons-material/People';
import LinkIcon from '@mui/icons-material/Link';
interface Iprops { interface Iprops {
activeCollection: Collection; activeCollection: Collection;
@ -46,6 +48,12 @@ export default function CollectionInfoWithOptions({
return <ArchiveOutlined />; return <ArchiveOutlined />;
case CollectionSummaryType.trash: case CollectionSummaryType.trash:
return <Delete />; return <Delete />;
case CollectionSummaryType.incomingShare:
return <PeopleIcon />;
case CollectionSummaryType.outgoingShare:
return <PeopleIcon />;
case CollectionSummaryType.sharedOnlyViaLink:
return <LinkIcon />;
default: default:
return <></>; return <></>;
} }

View file

@ -11,7 +11,9 @@ import TruncateText from 'components/TruncateText';
import { Box } from '@mui/material'; import { Box } from '@mui/material';
import { CollectionSummaryType } from 'constants/collection'; import { CollectionSummaryType } from 'constants/collection';
import Favorite from '@mui/icons-material/FavoriteRounded'; import Favorite from '@mui/icons-material/FavoriteRounded';
import { ArchiveOutlined } from '@mui/icons-material'; import ArchiveIcon from '@mui/icons-material/Archive';
import PeopleIcon from '@mui/icons-material/People';
import LinkIcon from '@mui/icons-material/Link';
interface Iprops { interface Iprops {
active: boolean; active: boolean;
@ -50,7 +52,20 @@ function CollectionCardIcon({ collectionType }) {
<CollectionBarTileIcon> <CollectionBarTileIcon>
{collectionType === CollectionSummaryType.favorites && <Favorite />} {collectionType === CollectionSummaryType.favorites && <Favorite />}
{collectionType === CollectionSummaryType.archived && ( {collectionType === CollectionSummaryType.archived && (
<ArchiveOutlined /> <ArchiveIcon
sx={(theme) => ({
color: theme.palette.fixed.strokeMutedWhite,
})}
/>
)}
{collectionType === CollectionSummaryType.outgoingShare && (
<PeopleIcon />
)}
{collectionType === CollectionSummaryType.incomingShare && (
<PeopleIcon />
)}
{collectionType === CollectionSummaryType.sharedOnlyViaLink && (
<LinkIcon />
)} )}
</CollectionBarTileIcon> </CollectionBarTileIcon>
); );

View file

@ -2,12 +2,12 @@ import { OverflowMenuOption } from 'components/OverflowMenu/option';
import React from 'react'; import React from 'react';
import EditIcon from '@mui/icons-material/Edit'; import EditIcon from '@mui/icons-material/Edit';
import IosShareIcon from '@mui/icons-material/IosShare'; import PeopleIcon from '@mui/icons-material/People';
import FileDownloadOutlinedIcon from '@mui/icons-material/FileDownloadOutlined';
import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined'; import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
import { CollectionActions } from '.'; import { CollectionActions } from '.';
import { ArchiveOutlined, Unarchive } from '@mui/icons-material'; import Unarchive from '@mui/icons-material/Unarchive';
import ArchiveOutlined from '@mui/icons-material/ArchiveOutlined';
interface Iprops { interface Iprops {
IsArchived: boolean; IsArchived: boolean;
@ -23,29 +23,13 @@ export function AlbumCollectionOption({
}: Iprops) { }: Iprops) {
return ( return (
<> <>
<OverflowMenuOption
onClick={handleCollectionAction(
CollectionActions.SHOW_SHARE_DIALOG,
false
)}
startIcon={<IosShareIcon />}>
{constants.SHARE}
</OverflowMenuOption>
<OverflowMenuOption
onClick={handleCollectionAction(
CollectionActions.CONFIRM_DOWNLOAD,
false
)}
startIcon={<FileDownloadOutlinedIcon />}>
{constants.DOWNLOAD}
</OverflowMenuOption>
<OverflowMenuOption <OverflowMenuOption
onClick={handleCollectionAction( onClick={handleCollectionAction(
CollectionActions.SHOW_RENAME_DIALOG, CollectionActions.SHOW_RENAME_DIALOG,
false false
)} )}
startIcon={<EditIcon />}> startIcon={<EditIcon />}>
{constants.RENAME} {constants.RENAME_COLLECTION}
</OverflowMenuOption> </OverflowMenuOption>
{IsArchived ? ( {IsArchived ? (
<OverflowMenuOption <OverflowMenuOption
@ -53,13 +37,13 @@ export function AlbumCollectionOption({
CollectionActions.UNARCHIVE CollectionActions.UNARCHIVE
)} )}
startIcon={<Unarchive />}> startIcon={<Unarchive />}>
{constants.UNARCHIVE} {constants.UNARCHIVE_COLLECTION}
</OverflowMenuOption> </OverflowMenuOption>
) : ( ) : (
<OverflowMenuOption <OverflowMenuOption
onClick={handleCollectionAction(CollectionActions.ARCHIVE)} onClick={handleCollectionAction(CollectionActions.ARCHIVE)}
startIcon={<ArchiveOutlined />}> startIcon={<ArchiveOutlined />}>
{constants.ARCHIVE} {constants.ARCHIVE_COLLECTION}
</OverflowMenuOption> </OverflowMenuOption>
)} )}
<OverflowMenuOption <OverflowMenuOption
@ -68,7 +52,15 @@ export function AlbumCollectionOption({
CollectionActions.CONFIRM_DELETE, CollectionActions.CONFIRM_DELETE,
false false
)}> )}>
{constants.DELETE} {constants.DELETE_COLLECTION}
</OverflowMenuOption>
<OverflowMenuOption
onClick={handleCollectionAction(
CollectionActions.SHOW_SHARE_DIALOG,
false
)}
startIcon={<PeopleIcon />}>
{constants.SHARE_COLLECTION}
</OverflowMenuOption> </OverflowMenuOption>
</> </>
); );

View file

@ -0,0 +1,81 @@
import { CollectionActions } from '.';
import React from 'react';
import { CollectionSummaryType } from 'constants/collection';
import PeopleIcon from '@mui/icons-material/People';
import FileDownloadOutlinedIcon from '@mui/icons-material/FileDownloadOutlined';
import { FlexWrapper } from 'components/Container';
import { IconButton, Tooltip } from '@mui/material';
import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined';
import constants from 'utils/strings/constants';
interface Iprops {
handleCollectionAction: (
action: CollectionActions,
loader?: boolean
) => (...args: any[]) => Promise<void>;
collectionSummaryType: CollectionSummaryType;
}
export function QuickOptions({
handleCollectionAction,
collectionSummaryType,
}: Iprops) {
return (
<FlexWrapper sx={{ gap: '16px' }}>
{!(
collectionSummaryType === CollectionSummaryType.trash ||
collectionSummaryType === CollectionSummaryType.favorites
) && (
<Tooltip
title={
collectionSummaryType ===
CollectionSummaryType.outgoingShare
? constants.MODIFY_SHARING
: collectionSummaryType ===
CollectionSummaryType.incomingShare
? constants.SHARING_DETAILS
: constants.SHARE_COLLECTION
}>
<IconButton>
<PeopleIcon
onClick={handleCollectionAction(
CollectionActions.SHOW_SHARE_DIALOG,
false
)}
/>
</IconButton>
</Tooltip>
)}
{!(collectionSummaryType === CollectionSummaryType.trash) && (
<Tooltip
title={
collectionSummaryType ===
CollectionSummaryType.favorites
? constants.DOWNLOAD_FAVOURITES
: constants.DOWNLOAD_COLLECTION
}>
<IconButton>
<FileDownloadOutlinedIcon
onClick={handleCollectionAction(
CollectionActions.CONFIRM_DOWNLOAD,
false
)}
/>
</IconButton>
</Tooltip>
)}
{collectionSummaryType === CollectionSummaryType.trash && (
<Tooltip title={constants.EMPTY_TRASH}>
<IconButton>
<DeleteOutlinedIcon
onClick={handleCollectionAction(
CollectionActions.CONFIRM_DELETE,
false
)}
/>
</IconButton>
</Tooltip>
)}
</FlexWrapper>
);
}

View file

@ -0,0 +1,25 @@
import { OverflowMenuOption } from 'components/OverflowMenu/option';
import React from 'react';
import LogoutIcon from '@mui/icons-material/Logout';
import constants from 'utils/strings/constants';
import { CollectionActions } from '.';
interface Iprops {
handleCollectionAction: (
action: CollectionActions,
loader?: boolean
) => (...args: any[]) => Promise<void>;
}
export function SharedCollectionOption({ handleCollectionAction }: Iprops) {
return (
<OverflowMenuOption
startIcon={<LogoutIcon />}
onClick={handleCollectionAction(
CollectionActions.CONFIRM_LEAVE_SHARED_ALBUM,
false
)}>
{constants.LEAVE_ALBUM}
</OverflowMenuOption>
);
}

View file

@ -17,7 +17,10 @@ import { AppContext } from 'pages/_app';
import OverflowMenu from 'components/OverflowMenu/menu'; import OverflowMenu from 'components/OverflowMenu/menu';
import { CollectionSummaryType } from 'constants/collection'; import { CollectionSummaryType } from 'constants/collection';
import { TrashCollectionOption } from './TrashCollectionOption'; import { TrashCollectionOption } from './TrashCollectionOption';
import { SharedCollectionOption } from './SharedCollectionOption';
import { QuickOptions } from './QuickOptions';
import MoreHoriz from '@mui/icons-material/MoreHoriz'; import MoreHoriz from '@mui/icons-material/MoreHoriz';
import { HorizontalFlex } from 'components/Container';
interface CollectionOptionsProps { interface CollectionOptionsProps {
setCollectionNamerAttributes: SetCollectionNamerAttributes; setCollectionNamerAttributes: SetCollectionNamerAttributes;
@ -39,6 +42,8 @@ export enum CollectionActions {
SHOW_SHARE_DIALOG, SHOW_SHARE_DIALOG,
CONFIRM_EMPTY_TRASH, CONFIRM_EMPTY_TRASH,
EMPTY_TRASH, EMPTY_TRASH,
CONFIRM_LEAVE_SHARED_ALBUM,
LEAVE_SHARED_ALBUM,
} }
const CollectionOptions = (props: CollectionOptionsProps) => { const CollectionOptions = (props: CollectionOptionsProps) => {
@ -93,6 +98,12 @@ const CollectionOptions = (props: CollectionOptionsProps) => {
case CollectionActions.EMPTY_TRASH: case CollectionActions.EMPTY_TRASH:
callback = emptyTrash; callback = emptyTrash;
break; break;
case CollectionActions.CONFIRM_LEAVE_SHARED_ALBUM:
callback = confirmLeaveSharedAlbum;
break;
case CollectionActions.LEAVE_SHARED_ALBUM:
callback = leaveSharedAlbum;
break;
default: default:
logError( logError(
Error('invalid collection action '), Error('invalid collection action '),
@ -130,6 +141,11 @@ const CollectionOptions = (props: CollectionOptionsProps) => {
redirectToAll(); redirectToAll();
}; };
const leaveSharedAlbum = async () => {
await CollectionAPI.leaveSharedAlbum(activeCollection.id);
redirectToAll();
};
const archiveCollection = () => { const archiveCollection = () => {
changeCollectionVisibility(activeCollection, VISIBILITY_STATE.ARCHIVED); changeCollectionVisibility(activeCollection, VISIBILITY_STATE.ARCHIVED);
}; };
@ -174,7 +190,7 @@ const CollectionOptions = (props: CollectionOptionsProps) => {
const confirmDownloadCollection = () => { const confirmDownloadCollection = () => {
setDialogMessage({ setDialogMessage({
title: constants.CONFIRM_DOWNLOAD_COLLECTION, title: constants.DOWNLOAD_COLLECTION,
content: constants.DOWNLOAD_COLLECTION_MESSAGE(), content: constants.DOWNLOAD_COLLECTION_MESSAGE(),
proceed: { proceed: {
text: constants.DOWNLOAD, text: constants.DOWNLOAD,
@ -200,26 +216,56 @@ const CollectionOptions = (props: CollectionOptionsProps) => {
close: { text: constants.CANCEL }, close: { text: constants.CANCEL },
}); });
const confirmLeaveSharedAlbum = () => {
setDialogMessage({
title: constants.LEAVE_SHARED_ALBUM_TITLE,
content: constants.LEAVE_SHARED_ALBUM_MESSAGE,
proceed: {
text: constants.LEAVE_SHARED_ALBUM,
action: handleCollectionAction(
CollectionActions.LEAVE_SHARED_ALBUM
),
variant: 'danger',
},
close: {
text: constants.CANCEL,
},
});
};
return ( return (
<OverflowMenu <HorizontalFlex sx={{ display: 'inline-flex', gap: '16px' }}>
ariaControls={'collection-options'} <QuickOptions
triggerButtonIcon={<MoreHoriz />} handleCollectionAction={handleCollectionAction}
triggerButtonProps={{ collectionSummaryType={collectionSummaryType}
sx: { />
background: (theme) => theme.palette.fill.dark, {!(collectionSummaryType === CollectionSummaryType.favorites) && (
}, <OverflowMenu
}}> ariaControls={'collection-options'}
{collectionSummaryType === CollectionSummaryType.trash ? ( triggerButtonIcon={<MoreHoriz />}
<TrashCollectionOption triggerButtonProps={{
handleCollectionAction={handleCollectionAction} sx: {
/> background: (theme) => theme.palette.fill.dark,
) : ( },
<AlbumCollectionOption }}>
IsArchived={IsArchived(activeCollection)} {collectionSummaryType === CollectionSummaryType.trash ? (
handleCollectionAction={handleCollectionAction} <TrashCollectionOption
/> handleCollectionAction={handleCollectionAction}
/>
) : collectionSummaryType ===
CollectionSummaryType.incomingShare ? (
<SharedCollectionOption
handleCollectionAction={handleCollectionAction}
/>
) : (
<AlbumCollectionOption
IsArchived={IsArchived(activeCollection)}
handleCollectionAction={handleCollectionAction}
/>
)}
</OverflowMenu>
)} )}
</OverflowMenu> </HorizontalFlex>
); );
}; };

View file

@ -14,11 +14,12 @@ export interface CollectionSelectorAttributes {
showNextModal: () => void; showNextModal: () => void;
title: string; title: string;
fromCollection?: number; fromCollection?: number;
onCancel?: () => void;
} }
interface Props { interface Props {
open: boolean; open: boolean;
onClose: (closeBtnClick?: boolean) => void; onClose: () => void;
attributes: CollectionSelectorAttributes; attributes: CollectionSelectorAttributes;
collections: Collection[]; collections: Collection[];
collectionSummaries: CollectionSummaries; collectionSummaries: CollectionSummaries;
@ -61,15 +62,18 @@ function CollectionSelector({
props.onClose(); props.onClose();
}; };
const onCloseButtonClick = () => props.onClose(true); const onUserTriggeredClose = () => {
attributes.onCancel?.();
props.onClose();
};
return ( return (
<AllCollectionDialog <AllCollectionDialog
onClose={props.onClose} onClose={onUserTriggeredClose}
open={props.open} open={props.open}
position="center" position="center"
fullScreen={appContext.isMobile}> fullScreen={appContext.isMobile}>
<DialogTitleWithCloseButton onClose={onCloseButtonClick}> <DialogTitleWithCloseButton onClose={onUserTriggeredClose}>
{attributes.title} {attributes.title}
</DialogTitleWithCloseButton> </DialogTitleWithCloseButton>
<DialogContent> <DialogContent>

View file

@ -2,15 +2,22 @@ import { Box, Typography } from '@mui/material';
import React from 'react'; import React from 'react';
import Select from 'react-select'; import Select from 'react-select';
import { DropdownStyle } from 'styles/dropdown'; import { DropdownStyle } from 'styles/dropdown';
import { Collection, PublicURL, UpdatePublicURL } from 'types/collection';
import { getDeviceLimitOptions } from 'utils/collection'; import { getDeviceLimitOptions } from 'utils/collection';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
import { OptionWithDivider } from './selectComponents/OptionWithDivider'; import { OptionWithDivider } from './selectComponents/OptionWithDivider';
interface Iprops {
publicShareProp: PublicURL;
collection: Collection;
updatePublicShareURLHelper: (req: UpdatePublicURL) => Promise<void>;
}
export function ManageDeviceLimit({ export function ManageDeviceLimit({
publicShareProp, publicShareProp,
collection, collection,
updatePublicShareURLHelper, updatePublicShareURLHelper,
}) { }: Iprops) {
const updateDeviceLimit = async (newLimit: number) => { const updateDeviceLimit = async (newLimit: number) => {
return updatePublicShareURLHelper({ return updatePublicShareURLHelper({
collectionID: collection.id, collectionID: collection.id,

View file

@ -1,13 +1,21 @@
import { Box, Typography } from '@mui/material'; import { Box, Typography } from '@mui/material';
import { AppContext } from 'pages/_app'; import { AppContext } from 'pages/_app';
import React, { useContext } from 'react'; import React, { useContext } from 'react';
import { PublicURL, Collection, UpdatePublicURL } from 'types/collection';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
import PublicShareSwitch from '../switch'; import PublicShareSwitch from '../switch';
interface Iprops {
publicShareProp: PublicURL;
collection: Collection;
updatePublicShareURLHelper: (req: UpdatePublicURL) => Promise<void>;
}
export function ManageDownloadAccess({ export function ManageDownloadAccess({
publicShareProp, publicShareProp,
updatePublicShareURLHelper, updatePublicShareURLHelper,
collection, collection,
}) { }: Iprops) {
const appContext = useContext(AppContext); const appContext = useContext(AppContext);
const handleFileDownloadSetting = () => { const handleFileDownloadSetting = () => {
@ -41,7 +49,7 @@ export function ManageDownloadAccess({
<Box> <Box>
<Typography mb={0.5}>{constants.FILE_DOWNLOAD}</Typography> <Typography mb={0.5}>{constants.FILE_DOWNLOAD}</Typography>
<PublicShareSwitch <PublicShareSwitch
checked={publicShareProp?.enableDownload ?? false} checked={publicShareProp?.enableDownload ?? true}
onChange={handleFileDownloadSetting} onChange={handleFileDownloadSetting}
/> />
</Box> </Box>

View file

@ -1,12 +1,11 @@
import { ManageLinkPassword } from './linkPassword'; import { ManageLinkPassword } from './linkPassword';
import { ManageDeviceLimit } from './deviceLimit'; import { ManageDeviceLimit } from './deviceLimit';
import { ManageLinkExpiry } from './linkExpiry'; import { ManageLinkExpiry } from './linkExpiry';
import { PublicLinkSetPassword } from '../setPassword';
import { Stack, Typography } from '@mui/material'; import { Stack, Typography } from '@mui/material';
import { GalleryContext } from 'pages/gallery'; import { GalleryContext } from 'pages/gallery';
import React, { useContext, useState } from 'react'; import React, { useContext, useState } from 'react';
import { updateShareableURL } from 'services/collectionService'; import { updateShareableURL } from 'services/collectionService';
import { UpdatePublicURL } from 'types/collection'; import { Collection, PublicURL, UpdatePublicURL } from 'types/collection';
import { sleep } from 'utils/common'; import { sleep } from 'utils/common';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
import { import {
@ -15,19 +14,24 @@ import {
} from '../../styledComponents'; } from '../../styledComponents';
import { ManageDownloadAccess } from './downloadAccess'; import { ManageDownloadAccess } from './downloadAccess';
import { handleSharingErrors } from 'utils/error/ui'; import { handleSharingErrors } from 'utils/error/ui';
import { SetPublicShareProp } from 'types/publicCollection';
import { ManagePublicCollect } from './publicCollect';
interface Iprops {
publicShareProp: PublicURL;
collection: Collection;
setPublicShareProp: SetPublicShareProp;
}
export default function PublicShareManage({ export default function PublicShareManage({
publicShareProp, publicShareProp,
collection, collection,
setPublicShareProp, setPublicShareProp,
}) { }: Iprops) {
const galleryContext = useContext(GalleryContext); const galleryContext = useContext(GalleryContext);
const [changePasswordView, setChangePasswordView] = useState(false);
const [sharableLinkError, setSharableLinkError] = useState(null); const [sharableLinkError, setSharableLinkError] = useState(null);
const closeConfigurePassword = () => setChangePasswordView(false);
const updatePublicShareURLHelper = async (req: UpdatePublicURL) => { const updatePublicShareURLHelper = async (req: UpdatePublicURL) => {
try { try {
galleryContext.setBlockingLoad(true); galleryContext.setBlockingLoad(true);
@ -73,6 +77,13 @@ export default function PublicShareManage({
updatePublicShareURLHelper updatePublicShareURLHelper
} }
/> />
<ManagePublicCollect
collection={collection}
publicShareProp={publicShareProp}
updatePublicShareURLHelper={
updatePublicShareURLHelper
}
/>
<ManageDownloadAccess <ManageDownloadAccess
collection={collection} collection={collection}
publicShareProp={publicShareProp} publicShareProp={publicShareProp}
@ -81,7 +92,6 @@ export default function PublicShareManage({
} }
/> />
<ManageLinkPassword <ManageLinkPassword
setChangePasswordView={setChangePasswordView}
collection={collection} collection={collection}
publicShareProp={publicShareProp} publicShareProp={publicShareProp}
updatePublicShareURLHelper={ updatePublicShareURLHelper={
@ -102,14 +112,6 @@ export default function PublicShareManage({
)} )}
</ManageSectionOptions> </ManageSectionOptions>
</details> </details>
<PublicLinkSetPassword
open={changePasswordView}
onClose={closeConfigurePassword}
collection={collection}
publicShareProp={publicShareProp}
updatePublicShareURLHelper={updatePublicShareURLHelper}
setChangePasswordView={setChangePasswordView}
/>
</> </>
); );
} }

View file

@ -2,16 +2,23 @@ import { Box, Typography } from '@mui/material';
import React from 'react'; import React from 'react';
import Select from 'react-select'; import Select from 'react-select';
import { linkExpiryStyle } from 'styles/linkExpiry'; import { linkExpiryStyle } from 'styles/linkExpiry';
import { PublicURL, Collection, UpdatePublicURL } from 'types/collection';
import { shareExpiryOptions } from 'utils/collection'; import { shareExpiryOptions } from 'utils/collection';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
import { formatDateTime } from 'utils/time/format'; import { formatDateTime } from 'utils/time/format';
import { OptionWithDivider } from './selectComponents/OptionWithDivider'; import { OptionWithDivider } from './selectComponents/OptionWithDivider';
interface Iprops {
publicShareProp: PublicURL;
collection: Collection;
updatePublicShareURLHelper: (req: UpdatePublicURL) => Promise<void>;
}
export function ManageLinkExpiry({ export function ManageLinkExpiry({
publicShareProp, publicShareProp,
collection, collection,
updatePublicShareURLHelper, updatePublicShareURLHelper,
}) { }: Iprops) {
const updateDeviceExpiry = async (optionFn) => { const updateDeviceExpiry = async (optionFn) => {
return updatePublicShareURLHelper({ return updatePublicShareURLHelper({
collectionID: collection.id, collectionID: collection.id,

View file

@ -1,48 +0,0 @@
import { Box, Typography } from '@mui/material';
import { AppContext } from 'pages/_app';
import React, { useContext } from 'react';
import constants from 'utils/strings/constants';
import PublicShareSwitch from '../switch';
export function ManageLinkPassword({
collection,
publicShareProp,
updatePublicShareURLHelper,
setChangePasswordView,
}) {
const appContext = useContext(AppContext);
const handlePasswordChangeSetting = async () => {
if (publicShareProp.passwordEnabled) {
await confirmDisablePublicUrlPassword();
} else {
setChangePasswordView(true);
}
};
const confirmDisablePublicUrlPassword = async () => {
appContext.setDialogMessage({
title: constants.DISABLE_PASSWORD,
content: constants.DISABLE_PASSWORD_MESSAGE,
close: { text: constants.CANCEL },
proceed: {
text: constants.DISABLE,
action: () =>
updatePublicShareURLHelper({
collectionID: collection.id,
disablePassword: true,
}),
variant: 'danger',
},
});
};
return (
<Box>
<Typography mb={0.5}> {constants.LINK_PASSWORD_LOCK}</Typography>
<PublicShareSwitch
checked={!!publicShareProp?.passwordEnabled}
onChange={handlePasswordChangeSetting}
/>
</Box>
);
}

View file

@ -0,0 +1,72 @@
import { Box, Typography } from '@mui/material';
import { AppContext } from 'pages/_app';
import React, { useContext, useState } from 'react';
import { PublicURL, Collection, UpdatePublicURL } from 'types/collection';
import constants from 'utils/strings/constants';
import { PublicLinkSetPassword } from './setPassword';
import PublicShareSwitch from '../../switch';
interface Iprops {
publicShareProp: PublicURL;
collection: Collection;
updatePublicShareURLHelper: (req: UpdatePublicURL) => Promise<void>;
}
export function ManageLinkPassword({
collection,
publicShareProp,
updatePublicShareURLHelper,
}: Iprops) {
const appContext = useContext(AppContext);
const [changePasswordView, setChangePasswordView] = useState(false);
const closeConfigurePassword = () => setChangePasswordView(false);
const handlePasswordChangeSetting = async () => {
if (publicShareProp.passwordEnabled) {
await confirmDisablePublicUrlPassword();
} else {
setChangePasswordView(true);
}
};
const confirmDisablePublicUrlPassword = async () => {
appContext.setDialogMessage({
title: constants.DISABLE_PASSWORD,
content: constants.DISABLE_PASSWORD_MESSAGE,
close: { text: constants.CANCEL },
proceed: {
text: constants.DISABLE,
action: () =>
updatePublicShareURLHelper({
collectionID: collection.id,
disablePassword: true,
}),
variant: 'danger',
},
});
};
return (
<>
<Box>
<Typography mb={0.5}>
{' '}
{constants.LINK_PASSWORD_LOCK}
</Typography>
<PublicShareSwitch
checked={!!publicShareProp?.passwordEnabled}
onChange={handlePasswordChangeSetting}
/>
</Box>
<PublicLinkSetPassword
open={changePasswordView}
onClose={closeConfigurePassword}
collection={collection}
publicShareProp={publicShareProp}
updatePublicShareURLHelper={updatePublicShareURLHelper}
setChangePasswordView={setChangePasswordView}
/>
</>
);
}

View file

@ -3,7 +3,7 @@ import SingleInputForm, {
SingleInputFormProps, SingleInputFormProps,
} from 'components/SingleInputForm'; } from 'components/SingleInputForm';
import React from 'react'; import React from 'react';
import CryptoWorker from 'utils/crypto'; import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
export function PublicLinkSetPassword({ export function PublicLinkSetPassword({
@ -28,8 +28,8 @@ export function PublicLinkSetPassword({
}; };
const enablePublicUrlPassword = async (password: string) => { const enablePublicUrlPassword = async (password: string) => {
const cryptoWorker = await new CryptoWorker(); const cryptoWorker = await ComlinkCryptoWorker.getInstance();
const kekSalt: string = await cryptoWorker.generateSaltToDeriveKey(); const kekSalt = await cryptoWorker.generateSaltToDeriveKey();
const kek = await cryptoWorker.deriveInteractiveKey(password, kekSalt); const kek = await cryptoWorker.deriveInteractiveKey(password, kekSalt);
return updatePublicShareURLHelper({ return updatePublicShareURLHelper({

View file

@ -0,0 +1,34 @@
import { Box, Typography } from '@mui/material';
import React from 'react';
import { PublicURL, Collection, UpdatePublicURL } from 'types/collection';
import constants from 'utils/strings/constants';
import PublicShareSwitch from '../switch';
interface Iprops {
publicShareProp: PublicURL;
collection: Collection;
updatePublicShareURLHelper: (req: UpdatePublicURL) => Promise<void>;
}
export function ManagePublicCollect({
publicShareProp,
updatePublicShareURLHelper,
collection,
}: Iprops) {
const handleFileDownloadSetting = () => {
updatePublicShareURLHelper({
collectionID: collection.id,
enableCollect: !publicShareProp.enableCollect,
});
};
return (
<Box>
<Typography mb={0.5}>{constants.PUBLIC_COLLECT}</Typography>
<PublicShareSwitch
checked={publicShareProp?.enableCollect}
onChange={handleFileDownloadSetting}
/>
</Box>
);
}

View file

@ -93,10 +93,10 @@ export default function Collections(props: Iprops) {
} }
/> />
), ),
itemType: ITEM_TYPE.OTHER, itemType: ITEM_TYPE.HEADER,
height: 68, height: 68,
}); });
}, [collectionSummaries, activeCollectionID]); }, [collectionSummaries, activeCollectionID, isInSearchMode]);
if (shouldBeHidden) { if (shouldBeHidden) {
return <></>; return <></>;

View file

@ -78,7 +78,7 @@ export const CollectionBarTileText = styled(Overlay)`
export const CollectionBarTileIcon = styled(Overlay)` export const CollectionBarTileIcon = styled(Overlay)`
padding: 4px; padding: 4px;
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-start;
align-items: flex-end; align-items: flex-end;
& > .MuiSvgIcon-root { & > .MuiSvgIcon-root {
font-size: 20px; font-size: 20px;

View file

@ -106,7 +106,6 @@ export default function FixCreationTime(props: Props) {
<div <div
style={{ style={{
marginBottom: '10px', marginBottom: '10px',
padding: '0 5%',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
...(fixState === FIX_STATE.RUNNING ...(fixState === FIX_STATE.RUNNING

View file

@ -23,7 +23,6 @@ const Option = ({
color: value !== Number(selected) ? '#aaa' : '#fff', color: value !== Number(selected) ? '#aaa' : '#fff',
}}> }}>
<Form.Check.Input <Form.Check.Input
style={{ marginTop: '6px' }}
id={value.toString()} id={value.toString()}
type="radio" type="radio"
value={value} value={value}

View file

@ -0,0 +1,22 @@
import { Box } from '@mui/material';
import Ente from 'components/icons/ente';
import Link from 'next/link';
import { ENTE_WEBSITE_LINK } from 'constants/urls';
export function EnteLinkLogo() {
return (
<Link href={ENTE_WEBSITE_LINK}>
<Box
sx={(theme) => ({
':hover': {
cursor: 'pointer',
svg: {
fill: theme.palette.text.secondary,
},
},
})}>
<Ente />
</Box>
</Link>
);
}

View file

@ -1,6 +1,6 @@
import { GalleryContext } from 'pages/gallery'; import { GalleryContext } from 'pages/gallery';
import PreviewCard from './pages/gallery/PreviewCard'; import PreviewCard from './pages/gallery/PreviewCard';
import React, { useContext, useEffect, useState } from 'react'; import React, { useContext, useEffect, useRef, useState } from 'react';
import { EnteFile } from 'types/file'; import { EnteFile } from 'types/file';
import { styled } from '@mui/material'; import { styled } from '@mui/material';
import DownloadManager from 'services/downloadManager'; import DownloadManager from 'services/downloadManager';
@ -32,6 +32,7 @@ import { User } from 'types/user';
import { getData, LS_KEYS } from 'utils/storage/localStorage'; import { getData, LS_KEYS } from 'utils/storage/localStorage';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { Collection } from 'types/collection'; import { Collection } from 'types/collection';
import { addLogLine } from 'utils/logging';
const Container = styled('div')` const Container = styled('div')`
display: block; display: block;
@ -113,81 +114,112 @@ const PhotoFrame = ({
const router = useRouter(); const router = useRouter();
const [isSourceLoaded, setIsSourceLoaded] = useState(false); const [isSourceLoaded, setIsSourceLoaded] = useState(false);
const filteredData = useMemo(() => { const updateInProgress = useRef(false);
const idSet = new Set(); const updateRequired = useRef(false);
const user: User = getData(LS_KEYS.USER);
return files const [filteredData, setFilteredData] = useState<EnteFile[]>([]);
.map((item, index) => ({
...item,
dataIndex: index,
w: window.innerWidth,
h: window.innerHeight,
title: item.pubMagicMetadata?.data.caption,
}))
.filter((item) => {
if (
deletedFileIds?.has(item.id) &&
activeCollection !== TRASH_SECTION
) {
return false;
}
if (
search?.date &&
!isSameDayAnyYear(search.date)(
new Date(item.metadata.creationTime / 1000)
)
) {
return false;
}
if (
search?.location &&
!isInsideBox(
{
latitude: item.metadata.latitude,
longitude: item.metadata.longitude,
},
search.location
)
) {
return false;
}
if (
!isDeduplicating &&
activeCollection === ALL_SECTION &&
(IsArchived(item) ||
archivedCollections?.has(item.collectionID))
) {
return false;
}
if (activeCollection === ARCHIVE_SECTION && !IsArchived(item)) {
return false;
}
if (isSharedFile(user, item) && !isSharedCollection) { useEffect(() => {
return false; const main = () => {
} if (updateInProgress.current) {
if (activeCollection === TRASH_SECTION && !item.isTrashed) { updateRequired.current = true;
return false; return;
} }
if (activeCollection !== TRASH_SECTION && item.isTrashed) { updateInProgress.current = true;
return false; const idSet = new Set();
} const user: User = getData(LS_KEYS.USER);
if (!idSet.has(item.id)) {
const filteredData = files
.map((item, index) => ({
...item,
dataIndex: index,
w: window.innerWidth,
h: window.innerHeight,
title: item.pubMagicMetadata?.data.caption,
}))
.filter((item) => {
if ( if (
activeCollection === ALL_SECTION || deletedFileIds?.has(item.id) &&
activeCollection === ARCHIVE_SECTION || activeCollection !== TRASH_SECTION
activeCollection === TRASH_SECTION ||
activeCollection === item.collectionID
) { ) {
idSet.add(item.id); return false;
return true; }
if (
search?.date &&
!isSameDayAnyYear(search.date)(
new Date(item.metadata.creationTime / 1000)
)
) {
return false;
}
if (
search?.location &&
!isInsideBox(
{
latitude: item.metadata.latitude,
longitude: item.metadata.longitude,
},
search.location
)
) {
return false;
}
if (
!isDeduplicating &&
activeCollection === ALL_SECTION &&
(IsArchived(item) ||
archivedCollections?.has(item.collectionID))
) {
return false;
}
if (
activeCollection === ARCHIVE_SECTION &&
!IsArchived(item)
) {
return false;
}
if (isSharedFile(user, item) && !isSharedCollection) {
return false;
}
if (activeCollection === TRASH_SECTION && !item.isTrashed) {
return false;
}
if (activeCollection !== TRASH_SECTION && item.isTrashed) {
return false;
}
if (!idSet.has(item.id)) {
if (
activeCollection === ALL_SECTION ||
activeCollection === ARCHIVE_SECTION ||
activeCollection === TRASH_SECTION ||
activeCollection === item.collectionID ||
isInSearchMode
) {
idSet.add(item.id);
return true;
}
return false;
} }
return false; return false;
} });
return false; setFilteredData(filteredData);
}); updateInProgress.current = false;
}, [files, deletedFileIds, search, activeCollection]); if (updateRequired.current) {
updateRequired.current = false;
setTimeout(() => {
main();
}, 0);
}
};
main();
}, [
files,
deletedFileIds,
search?.date,
search?.location,
activeCollection,
]);
const fileToCollectionsMap = useMemo(() => { const fileToCollectionsMap = useMemo(() => {
const fileToCollectionsMap = new Map<number, number[]>(); const fileToCollectionsMap = new Map<number, number[]>();
@ -493,12 +525,26 @@ const PhotoFrame = ({
index: number, index: number,
item: EnteFile item: EnteFile
) => { ) => {
addLogLine(
`[${
item.id
}] getSlideData called for thumbnail:${!!item.msrc} original:${
!!item.msrc && item.src !== item.msrc
} inProgress:${fetching[item.id]}`
);
if (!item.msrc) { if (!item.msrc) {
addLogLine(`[${item.id}] doesn't have thumbnail`);
try { try {
let url: string; let url: string;
if (galleryContext.thumbs.has(item.id)) { if (galleryContext.thumbs.has(item.id)) {
addLogLine(
`[${item.id}] gallery context cache hit, using cached thumb`
);
url = galleryContext.thumbs.get(item.id); url = galleryContext.thumbs.get(item.id);
} else { } else {
addLogLine(
`[${item.id}] gallery context cache miss, calling downloadManager to get thumb`
);
if ( if (
publicCollectionGalleryContext.accessedThroughSharedURL publicCollectionGalleryContext.accessedThroughSharedURL
) { ) {
@ -523,6 +569,9 @@ const PhotoFrame = ({
item.w = newFile.w; item.w = newFile.w;
item.h = newFile.h; item.h = newFile.h;
addLogLine(
`[${item.id}] calling invalidateCurrItems for thumbnail`
);
try { try {
instance.invalidateCurrItems(); instance.invalidateCurrItems();
if (instance.isOpen()) { if (instance.isOpen()) {
@ -540,16 +589,23 @@ const PhotoFrame = ({
} }
} }
if (!fetching[item.id]) { if (!fetching[item.id]) {
addLogLine(`[${item.id}] new file download fetch original request`);
try { try {
fetching[item.id] = true; fetching[item.id] = true;
let urls: { original: string[]; converted: string[] }; let urls: { original: string[]; converted: string[] };
if (galleryContext.files.has(item.id)) { if (galleryContext.files.has(item.id)) {
addLogLine(
`[${item.id}] gallery context cache hit, using cached file`
);
const mergedURL = galleryContext.files.get(item.id); const mergedURL = galleryContext.files.get(item.id);
urls = { urls = {
original: mergedURL.original.split(','), original: mergedURL.original.split(','),
converted: mergedURL.converted.split(','), converted: mergedURL.converted.split(','),
}; };
} else { } else {
addLogLine(
`[${item.id}] gallery context cache miss, calling downloadManager to get file`
);
appContext.startLoading(); appContext.startLoading();
if ( if (
publicCollectionGalleryContext.accessedThroughSharedURL publicCollectionGalleryContext.accessedThroughSharedURL
@ -576,7 +632,8 @@ const PhotoFrame = ({
let convertedVideoURL; let convertedVideoURL;
if (item.metadata.fileType === FILE_TYPE.LIVE_PHOTO) { if (item.metadata.fileType === FILE_TYPE.LIVE_PHOTO) {
[originalImageURL, originalVideoURL] = urls.converted; [originalImageURL, originalVideoURL] = urls.original;
[convertedImageURL, convertedVideoURL] = urls.converted;
} else if (item.metadata.fileType === FILE_TYPE.VIDEO) { } else if (item.metadata.fileType === FILE_TYPE.VIDEO) {
[originalVideoURL] = urls.original; [originalVideoURL] = urls.original;
[convertedVideoURL] = urls.converted; [convertedVideoURL] = urls.converted;
@ -600,6 +657,9 @@ const PhotoFrame = ({
item.w = newFile.w; item.w = newFile.w;
item.h = newFile.h; item.h = newFile.h;
try { try {
addLogLine(
`[${item.id}] calling invalidateCurrItems for src`
);
instance.invalidateCurrItems(); instance.invalidateCurrItems();
if (instance.isOpen()) { if (instance.isOpen()) {
instance.updateSize(true); instance.updateSize(true);
@ -609,13 +669,12 @@ const PhotoFrame = ({
e, e,
'updating photoswipe after src url update failed' 'updating photoswipe after src url update failed'
); );
// ignore throw e;
} }
} catch (e) { } catch (e) {
logError(e, 'getSlideData failed get src url failed'); logError(e, 'getSlideData failed get src url failed');
// no-op
} finally {
fetching[item.id] = false; fetching[item.id] = false;
// no-op
} }
} }
}; };

View file

@ -1,4 +1,4 @@
import React, { useRef, useEffect, useContext } from 'react'; import React, { useRef, useEffect, useContext, useState } from 'react';
import { VariableSizeList as List } from 'react-window'; import { VariableSizeList as List } from 'react-window';
import { Box, Link, styled } from '@mui/material'; import { Box, Link, styled } from '@mui/material';
import { EnteFile } from 'types/file'; import { EnteFile } from 'types/file';
@ -25,11 +25,15 @@ import { formatDate } from 'utils/time/format';
const A_DAY = 24 * 60 * 60 * 1000; const A_DAY = 24 * 60 * 60 * 1000;
const FOOTER_HEIGHT = 90; const FOOTER_HEIGHT = 90;
const ALBUM_FOOTER_HEIGHT = 75;
export enum ITEM_TYPE { export enum ITEM_TYPE {
TIME = 'TIME', TIME = 'TIME',
FILE = 'FILE', FILE = 'FILE',
SIZE_AND_COUNT = 'SIZE_AND_COUNT', SIZE_AND_COUNT = 'SIZE_AND_COUNT',
HEADER = 'HEADER',
FOOTER = 'FOOTER',
MARKETING_FOOTER = 'MARKETING_FOOTER',
OTHER = 'OTHER', OTHER = 'OTHER',
} }
@ -128,7 +132,6 @@ const SizeAndCountContainer = styled(DateContainer)`
`; `;
const FooterContainer = styled(ListItemContainer)` const FooterContainer = styled(ListItemContainer)`
font-size: 14px;
margin-bottom: 0.75rem; margin-bottom: 0.75rem;
@media (max-width: 540px) { @media (max-width: 540px) {
font-size: 12px; font-size: 12px;
@ -141,6 +144,13 @@ const FooterContainer = styled(ListItemContainer)`
margin-top: calc(2rem + 20px); margin-top: calc(2rem + 20px);
`; `;
const AlbumFooterContainer = styled(ListItemContainer)`
margin-top: 48px;
margin-bottom: 10px;
text-align: center;
justify-content: center;
`;
const NothingContainer = styled(ListItemContainer)` const NothingContainer = styled(ListItemContainer)`
color: #979797; color: #979797;
text-align: center; text-align: center;
@ -171,17 +181,16 @@ export function PhotoList({
resetFetching, resetFetching,
}: Props) { }: Props) {
const galleryContext = useContext(GalleryContext); const galleryContext = useContext(GalleryContext);
const timeStampListRef = useRef([]);
const timeStampList = timeStampListRef?.current ?? [];
const filteredDataCopyRef = useRef([]);
const filteredDataCopy = filteredDataCopyRef.current ?? [];
const listRef = useRef(null);
const publicCollectionGalleryContext = useContext( const publicCollectionGalleryContext = useContext(
PublicCollectionGalleryContext PublicCollectionGalleryContext
); );
const deduplicateContext = useContext(DeduplicateContext); const deduplicateContext = useContext(DeduplicateContext);
const [timeStampList, setTimeStampList] = useState<TimeStampListItem[]>([]);
const refreshInProgress = useRef(false);
const shouldRefresh = useRef(false);
const listRef = useRef(null);
const fittableColumns = getFractionFittableColumns(width); const fittableColumns = getFractionFittableColumns(width);
let columns = Math.ceil(fittableColumns); let columns = Math.ceil(fittableColumns);
@ -200,59 +209,135 @@ export function PhotoList({
}; };
useEffect(() => { useEffect(() => {
let timeStampList: TimeStampListItem[] = []; const main = () => {
if (refreshInProgress.current) {
shouldRefresh.current = true;
return;
}
refreshInProgress.current = true;
let timeStampList: TimeStampListItem[] = [];
if (galleryContext.photoListHeader) { if (galleryContext.photoListHeader) {
timeStampList.push( timeStampList.push(
getPhotoListHeader(galleryContext.photoListHeader) getPhotoListHeader(galleryContext.photoListHeader)
); );
} else if (publicCollectionGalleryContext.photoListHeader) { } else if (publicCollectionGalleryContext.photoListHeader) {
timeStampList.push( timeStampList.push(
getPhotoListHeader( getPhotoListHeader(
publicCollectionGalleryContext.photoListHeader publicCollectionGalleryContext.photoListHeader
) )
); );
} }
if (deduplicateContext.isOnDeduplicatePage) { if (deduplicateContext.isOnDeduplicatePage) {
skipMerge = true; skipMerge = true;
groupByFileSize(timeStampList); groupByFileSize(timeStampList);
} else { } else {
groupByTime(timeStampList); groupByTime(timeStampList);
} }
if (!skipMerge) { if (!skipMerge) {
timeStampList = mergeTimeStampList(timeStampList, columns); timeStampList = mergeTimeStampList(timeStampList, columns);
} }
if (timeStampList.length === 1) { if (timeStampList.length === 1) {
timeStampList.push(getEmptyListItem()); timeStampList.push(getEmptyListItem());
} }
if (
showAppDownloadBanner ||
publicCollectionGalleryContext.accessedThroughSharedURL
) {
timeStampList.push(getVacuumItem(timeStampList)); timeStampList.push(getVacuumItem(timeStampList));
if (publicCollectionGalleryContext.accessedThroughSharedURL) { if (publicCollectionGalleryContext.accessedThroughSharedURL) {
if (publicCollectionGalleryContext.photoListFooter) {
timeStampList.push(
getPhotoListFooter(
publicCollectionGalleryContext.photoListFooter
)
);
}
timeStampList.push(getAlbumsFooter()); timeStampList.push(getAlbumsFooter());
} else { } else if (showAppDownloadBanner) {
timeStampList.push(getAppDownloadFooter()); timeStampList.push(getAppDownloadFooter());
} }
}
timeStampListRef.current = timeStampList; setTimeStampList(timeStampList);
filteredDataCopyRef.current = filteredData; refreshInProgress.current = false;
refreshList(); if (shouldRefresh.current) {
shouldRefresh.current = false;
setTimeout(main, 0);
}
};
main();
}, [ }, [
width, width,
height, height,
filteredData, filteredData,
showAppDownloadBanner,
publicCollectionGalleryContext.accessedThroughSharedURL,
galleryContext.photoListHeader,
publicCollectionGalleryContext.photoListHeader,
deduplicateContext.isOnDeduplicatePage, deduplicateContext.isOnDeduplicatePage,
deduplicateContext.fileSizeMap, deduplicateContext.fileSizeMap,
]); ]);
useEffect(() => {
setTimeStampList((timeStampList) => {
timeStampList = timeStampList ?? [];
const hasHeader =
timeStampList.length > 0 &&
timeStampList[0].itemType === ITEM_TYPE.HEADER;
if (hasHeader) {
return timeStampList;
}
if (galleryContext.photoListHeader) {
return [
getPhotoListHeader(galleryContext.photoListHeader),
...timeStampList,
];
} else if (publicCollectionGalleryContext.photoListHeader) {
return [
getPhotoListHeader(
publicCollectionGalleryContext.photoListHeader
),
...timeStampList,
];
} else {
return timeStampList;
}
});
}, [
galleryContext.photoListHeader,
publicCollectionGalleryContext.photoListHeader,
]);
useEffect(() => {
setTimeStampList((timeStampList) => {
timeStampList = timeStampList ?? [];
const hasFooter =
timeStampList.length > 0 &&
timeStampList[timeStampList.length - 1].itemType ===
ITEM_TYPE.MARKETING_FOOTER;
if (hasFooter) {
return timeStampList;
}
if (publicCollectionGalleryContext.accessedThroughSharedURL) {
if (publicCollectionGalleryContext.photoListFooter) {
return [
...timeStampList,
getPhotoListFooter(
publicCollectionGalleryContext.photoListFooter
),
getAlbumsFooter(),
];
}
} else if (showAppDownloadBanner) {
return [...timeStampList, getAppDownloadFooter()];
} else {
return timeStampList;
}
});
}, [
publicCollectionGalleryContext.accessedThroughSharedURL,
showAppDownloadBanner,
publicCollectionGalleryContext.photoListFooter,
]);
useEffect(() => {
refreshList();
}, [timeStampList]);
const groupByFileSize = (timeStampList: TimeStampListItem[]) => { const groupByFileSize = (timeStampList: TimeStampListItem[]) => {
let index = 0; let index = 0;
while (index < filteredData.length) { while (index < filteredData.length) {
@ -295,10 +380,10 @@ export function PhotoList({
const groupByTime = (timeStampList: TimeStampListItem[]) => { const groupByTime = (timeStampList: TimeStampListItem[]) => {
let listItemIndex = 0; let listItemIndex = 0;
let currentDate = -1; let currentDate;
filteredData.forEach((item, index) => { filteredData.forEach((item, index) => {
if ( if (
!currentDate ||
!isSameDay( !isSameDay(
new Date(item.metadata.creationTime / 1000), new Date(item.metadata.creationTime / 1000),
new Date(currentDate) new Date(currentDate)
@ -338,10 +423,13 @@ export function PhotoList({
}); });
}; };
const isSameDay = (first, second) => const isSameDay = (first, second) => {
first.getFullYear() === second.getFullYear() && return (
first.getMonth() === second.getMonth() && first.getFullYear() === second.getFullYear() &&
first.getDate() === second.getDate(); first.getMonth() === second.getMonth() &&
first.getDate() === second.getDate()
);
};
const getPhotoListHeader = (photoListHeader) => { const getPhotoListHeader = (photoListHeader) => {
return { return {
@ -354,6 +442,17 @@ export function PhotoList({
}; };
}; };
const getPhotoListFooter = (photoListFooter) => {
return {
...photoListFooter,
item: (
<ListItemContainer span={columns}>
{photoListFooter.item}
</ListItemContainer>
),
};
};
const getEmptyListItem = () => { const getEmptyListItem = () => {
return { return {
itemType: ITEM_TYPE.OTHER, itemType: ITEM_TYPE.OTHER,
@ -367,12 +466,17 @@ export function PhotoList({
}; };
}; };
const getVacuumItem = (timeStampList) => { const getVacuumItem = (timeStampList) => {
const footerHeight =
publicCollectionGalleryContext.accessedThroughSharedURL
? ALBUM_FOOTER_HEIGHT +
(publicCollectionGalleryContext.photoListFooter?.height ?? 0)
: FOOTER_HEIGHT;
const photoFrameHeight = (() => { const photoFrameHeight = (() => {
let sum = 0; let sum = 0;
const getCurrentItemSize = getItemSize(timeStampList); const getCurrentItemSize = getItemSize(timeStampList);
for (let i = 0; i < timeStampList.length; i++) { for (let i = 0; i < timeStampList.length; i++) {
sum += getCurrentItemSize(i); sum += getCurrentItemSize(i);
if (height - sum <= FOOTER_HEIGHT) { if (height - sum <= footerHeight) {
break; break;
} }
} }
@ -381,17 +485,19 @@ export function PhotoList({
return { return {
itemType: ITEM_TYPE.OTHER, itemType: ITEM_TYPE.OTHER,
item: <></>, item: <></>,
height: Math.max(height - photoFrameHeight - FOOTER_HEIGHT, 0), height: Math.max(height - photoFrameHeight - footerHeight, 0),
}; };
}; };
const getAppDownloadFooter = () => { const getAppDownloadFooter = () => {
return { return {
itemType: ITEM_TYPE.OTHER, itemType: ITEM_TYPE.MARKETING_FOOTER,
height: FOOTER_HEIGHT, height: FOOTER_HEIGHT,
item: ( item: (
<FooterContainer span={columns}> <FooterContainer span={columns}>
<Typography>{constants.INSTALL_MOBILE_APP()}</Typography> <Typography variant="body2">
{constants.INSTALL_MOBILE_APP()}
</Typography>
</FooterContainer> </FooterContainer>
), ),
}; };
@ -399,17 +505,17 @@ export function PhotoList({
const getAlbumsFooter = () => { const getAlbumsFooter = () => {
return { return {
itemType: ITEM_TYPE.OTHER, itemType: ITEM_TYPE.MARKETING_FOOTER,
height: FOOTER_HEIGHT, height: ALBUM_FOOTER_HEIGHT,
item: ( item: (
<FooterContainer span={columns}> <AlbumFooterContainer span={columns}>
<p> <Typography variant="body2">
{constants.PRESERVED_BY}{' '} {constants.SHARED_USING}{' '}
<Link target="_blank" href={ENTE_WEBSITE_LINK}> <Link target="_blank" href={ENTE_WEBSITE_LINK}>
{constants.ENTE_IO} {constants.ENTE_IO}
</Link> </Link>
</p> </Typography>
</FooterContainer> </AlbumFooterContainer>
), ),
}; };
}; };
@ -527,14 +633,14 @@ export function PhotoList({
switch (listItem.itemType) { switch (listItem.itemType) {
case ITEM_TYPE.TIME: case ITEM_TYPE.TIME:
return listItem.dates ? ( return listItem.dates ? (
listItem.dates.map((item) => ( listItem.dates
<> .map((item) => [
<DateContainer key={item.date} span={item.span}> <DateContainer key={item.date} span={item.span}>
{item.date} {item.date}
</DateContainer> </DateContainer>,
<div /> <div key={`${item.date}-gap`} />,
</> ])
)) .flat()
) : ( ) : (
<DateContainer span={columns}> <DateContainer span={columns}>
{listItem.date} {listItem.date}
@ -551,7 +657,7 @@ export function PhotoList({
case ITEM_TYPE.FILE: { case ITEM_TYPE.FILE: {
const ret = listItem.items.map((item, idx) => const ret = listItem.items.map((item, idx) =>
getThumbnail( getThumbnail(
filteredDataCopy, filteredData,
listItem.itemStartIndex + idx, listItem.itemStartIndex + idx,
isScrolling isScrolling
) )

View file

@ -6,10 +6,6 @@ import SingleInputForm, {
SingleInputFormProps, SingleInputFormProps,
} from 'components/SingleInputForm'; } from 'components/SingleInputForm';
export interface formValues {
filename: string;
}
export const FileNameEditDialog = ({ export const FileNameEditDialog = ({
isInEditMode, isInEditMode,
closeEditMode, closeEditMode,

View file

@ -11,9 +11,9 @@ import { SmallLoadingSpinner } from '../styledComponents/SmallLoadingSpinner';
import * as Yup from 'yup'; import * as Yup from 'yup';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
import Close from '@mui/icons-material/Close'; import Close from '@mui/icons-material/Close';
import { Done } from '@mui/icons-material'; import Done from '@mui/icons-material/Done';
export interface formValues { interface formValues {
caption: string; caption: string;
} }

View file

@ -9,11 +9,12 @@ import {
import { FlexWrapper } from 'components/Container'; import { FlexWrapper } from 'components/Container';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
import { FILE_TYPE } from 'constants/file'; import { FILE_TYPE } from 'constants/file';
import { PhotoOutlined, VideoFileOutlined } from '@mui/icons-material';
import InfoItem from './InfoItem'; import InfoItem from './InfoItem';
import { makeHumanReadableStorage } from 'utils/billing'; import { makeHumanReadableStorage } from 'utils/billing';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import { FileNameEditDialog } from './FileNameEditDialog'; import { FileNameEditDialog } from './FileNameEditDialog';
import VideocamOutlined from '@mui/icons-material/VideocamOutlined';
import PhotoOutlined from '@mui/icons-material/PhotoOutlined';
const getFileTitle = (filename, extension) => { const getFileTitle = (filename, extension) => {
if (extension) { if (extension) {
@ -98,10 +99,10 @@ export function RenderFileName({
<> <>
<InfoItem <InfoItem
icon={ icon={
file.metadata.fileType === FILE_TYPE.IMAGE ? ( file.metadata.fileType === FILE_TYPE.VIDEO ? (
<PhotoOutlined /> <VideocamOutlined />
) : ( ) : (
<VideoFileOutlined /> <PhotoOutlined />
) )
} }
title={getFileTitle(filename, extension)} title={getFileTitle(filename, extension)}

View file

@ -6,13 +6,7 @@ import { Box, DialogProps, Link, Stack, styled } from '@mui/material';
import { Location } from 'types/upload'; import { Location } from 'types/upload';
import { getEXIFLocation } from 'services/upload/exifService'; import { getEXIFLocation } from 'services/upload/exifService';
import { RenderCaption } from './RenderCaption'; import { RenderCaption } from './RenderCaption';
import {
BackupOutlined,
CameraOutlined,
FolderOutlined,
LocationOnOutlined,
TextSnippetOutlined,
} from '@mui/icons-material';
import CopyButton from 'components/CodeBlock/CopyButton'; import CopyButton from 'components/CodeBlock/CopyButton';
import { formatDate, formatTime } from 'utils/time/format'; import { formatDate, formatTime } from 'utils/time/format';
import Titlebar from 'components/Titlebar'; import Titlebar from 'components/Titlebar';
@ -24,6 +18,11 @@ import { Chip } from 'components/Chip';
import LinkButton from 'components/pages/gallery/LinkButton'; import LinkButton from 'components/pages/gallery/LinkButton';
import { ExifData } from './ExifData'; import { ExifData } from './ExifData';
import { EnteDrawer } from 'components/EnteDrawer'; import { EnteDrawer } from 'components/EnteDrawer';
import CameraOutlined from '@mui/icons-material/CameraOutlined';
import LocationOnOutlined from '@mui/icons-material/LocationOnOutlined';
import TextSnippetOutlined from '@mui/icons-material/TextSnippetOutlined';
import FolderOutlined from '@mui/icons-material/FolderOutlined';
import BackupOutlined from '@mui/icons-material/BackupOutlined';
export const FileInfoSidebar = styled((props: DialogProps) => ( export const FileInfoSidebar = styled((props: DialogProps) => (
<EnteDrawer {...props} anchor="right" /> <EnteDrawer {...props} anchor="right" />

View file

@ -9,7 +9,11 @@ import {
import { EnteFile } from 'types/file'; import { EnteFile } from 'types/file';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
import exifr from 'exifr'; import exifr from 'exifr';
import { downloadFile, copyFileToClipboard } from 'utils/file'; import {
downloadFile,
copyFileToClipboard,
getFileExtension,
} from 'utils/file';
import { livePhotoBtnHTML } from 'components/LivePhotoBtn'; import { livePhotoBtnHTML } from 'components/LivePhotoBtn';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
@ -32,9 +36,10 @@ import ChevronRight from '@mui/icons-material/ChevronRight';
import DeleteIcon from '@mui/icons-material/Delete'; import DeleteIcon from '@mui/icons-material/Delete';
import { trashFiles } from 'services/fileService'; import { trashFiles } from 'services/fileService';
import { getTrashFileMessage } from 'utils/ui'; import { getTrashFileMessage } from 'utils/ui';
import { ChevronLeft, ContentCopy } from '@mui/icons-material';
import { styled } from '@mui/material'; import { styled } from '@mui/material';
import { addLocalLog } from 'utils/logging'; import { addLocalLog } from 'utils/logging';
import ContentCopy from '@mui/icons-material/ContentCopy';
import ChevronLeft from '@mui/icons-material/ChevronLeft';
interface PhotoswipeFullscreenAPI { interface PhotoswipeFullscreenAPI {
enter: () => void; enter: () => void;
@ -114,21 +119,17 @@ function PhotoViewer(props: Iprops) {
} }
function handleKeyUp(event: KeyboardEvent) { function handleKeyUp(event: KeyboardEvent) {
if (!isOpen) { if (!isOpen || showInfo) {
return; return;
} }
addLocalLog(() => 'Event: ' + event.key); addLocalLog(() => 'Event: ' + event.key);
if (event.key === 'i' || event.key === 'I') {
if (!showInfo) {
setShowInfo(true);
} else {
setShowInfo(false);
}
}
if (showInfo) {
return;
}
switch (event.key) { switch (event.key) {
case 'i':
case 'I':
setShowInfo(true);
break;
case 'Backspace': case 'Backspace':
case 'Delete': case 'Delete':
confirmTrashFile(photoSwipe?.currItem as EnteFile); confirmTrashFile(photoSwipe?.currItem as EnteFile);
@ -240,7 +241,6 @@ function PhotoViewer(props: Iprops) {
useEffect(() => { useEffect(() => {
exifCopy.current = exif; exifCopy.current = exif;
console.log(exif);
}, [exif]); }, [exif]);
function updateFavButton(file: EnteFile) { function updateFavButton(file: EnteFile) {
@ -294,7 +294,7 @@ function PhotoViewer(props: Iprops) {
if (callback || event === 'destroy') { if (callback || event === 'destroy') {
photoSwipe.listen(event, function (...args) { photoSwipe.listen(event, function (...args) {
if (callback) { if (callback) {
args.unshift(this); args.unshift(photoSwipe);
callback(...args); callback(...args);
} }
if (event === 'destroy') { if (event === 'destroy') {
@ -308,6 +308,11 @@ function PhotoViewer(props: Iprops) {
}); });
photoSwipe.listen('beforeChange', () => { photoSwipe.listen('beforeChange', () => {
const currItem = photoSwipe?.currItem as EnteFile; const currItem = photoSwipe?.currItem as EnteFile;
updateFavButton(currItem);
if (currItem.metadata.fileType !== FILE_TYPE.IMAGE) {
setExif({ key: currItem.src, value: null });
return;
}
if ( if (
!currItem || !currItem ||
!exifCopy?.current?.value === null || !exifCopy?.current?.value === null ||
@ -317,10 +322,13 @@ function PhotoViewer(props: Iprops) {
} }
setExif({ key: currItem.src, value: undefined }); setExif({ key: currItem.src, value: undefined });
checkExifAvailable(currItem); checkExifAvailable(currItem);
updateFavButton(currItem);
}); });
photoSwipe.listen('resize', () => { photoSwipe.listen('resize', () => {
const currItem = photoSwipe?.currItem as EnteFile; const currItem = photoSwipe?.currItem as EnteFile;
if (currItem.metadata.fileType !== FILE_TYPE.IMAGE) {
setExif({ key: currItem.src, value: null });
return;
}
if ( if (
!currItem || !currItem ||
!exifCopy?.current?.value === null || !exifCopy?.current?.value === null ||
@ -416,16 +424,12 @@ function PhotoViewer(props: Iprops) {
const checkExifAvailable = async (file: EnteFile) => { const checkExifAvailable = async (file: EnteFile) => {
try { try {
console.log('checkExifAvailable', file.src);
if (exifExtractionInProgress.current === file.src) { if (exifExtractionInProgress.current === file.src) {
console.log('already in process');
return; return;
} }
try { try {
if (file.isSourceLoaded) { if (file.isSourceLoaded) {
console.log('starting processing');
exifExtractionInProgress.current = file.src; exifExtractionInProgress.current = file.src;
console.log(file.originalImageURL);
const imageBlob = await ( const imageBlob = await (
await fetch(file.originalImageURL) await fetch(file.originalImageURL)
).blob(); ).blob();
@ -433,24 +437,21 @@ function PhotoViewer(props: Iprops) {
string, string,
any any
>; >;
console.log({ exifData });
console.log(exifExtractionInProgress, file.src);
if (exifExtractionInProgress.current === file.src) { if (exifExtractionInProgress.current === file.src) {
if (exifData) { if (exifData) {
console.log('set extracted metadata');
setExif({ key: file.src, value: exifData }); setExif({ key: file.src, value: exifData });
} else { } else {
console.log("doesn't have metadata");
setExif({ key: file.src, value: null }); setExif({ key: file.src, value: null });
} }
} }
} }
} finally { } finally {
console.log('cleared exifExtractionInProgress');
exifExtractionInProgress.current = null; exifExtractionInProgress.current = null;
} }
} catch (e) { } catch (e) {
logError(e, 'exifr parsing failed'); setExif({ key: file.src, value: null });
const fileExtension = getFileExtension(file.metadata.title);
logError(e, 'exifr parsing failed', { extension: fileExtension });
} }
}; };
@ -529,7 +530,7 @@ function PhotoViewer(props: Iprops) {
<button <button
className="pswp__button pswp__button--close" className="pswp__button pswp__button--close"
title={constants.CLOSE} title={constants.CLOSE_OPTION}
/> />
{props.enableDownload && ( {props.enableDownload && (

View file

@ -6,9 +6,13 @@ import { addLogLine, getDebugLogs } from 'utils/logging';
import SidebarButton from './Button'; import SidebarButton from './Button';
import isElectron from 'is-electron'; import isElectron from 'is-electron';
import ElectronService from 'services/electron/common'; import ElectronService from 'services/electron/common';
import { testUpload } from 'tests/upload.test';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import { isInternalUser } from 'utils/user'; import { isInternalUser } from 'utils/user';
import { testUpload } from '../../../tests/upload.test';
import {
testZipFileReading,
testZipWithRootFileReadingTest,
} from '../../../tests/zip-file-reading.test';
export default function DebugSection() { export default function DebugSection() {
const appContext = useContext(AppContext); const appContext = useContext(AppContext);
@ -63,9 +67,17 @@ export default function DebugSection() {
</Typography> </Typography>
)} )}
{isInternalUser() && ( {isInternalUser() && (
<SidebarButton onClick={testUpload}> <>
{constants.RUN_TESTS} <SidebarButton onClick={testUpload}>
</SidebarButton> Test Upload
</SidebarButton>
<SidebarButton onClick={testZipFileReading}>
Test Zip file reading
</SidebarButton>
<SidebarButton onClick={testZipWithRootFileReadingTest}>
Zip with Root file Test
</SidebarButton>
</>
)} )}
</> </>
); );

View file

@ -4,7 +4,8 @@ import { GalleryContext } from 'pages/gallery';
import { ARCHIVE_SECTION, TRASH_SECTION } from 'constants/collection'; import { ARCHIVE_SECTION, TRASH_SECTION } from 'constants/collection';
import { CollectionSummaries } from 'types/collection'; import { CollectionSummaries } from 'types/collection';
import ShortcutButton from './ShortcutButton'; import ShortcutButton from './ShortcutButton';
import { ArchiveOutlined, DeleteOutline } from '@mui/icons-material'; import DeleteOutline from '@mui/icons-material/DeleteOutline';
import ArchiveOutlined from '@mui/icons-material/ArchiveOutlined';
interface Iprops { interface Iprops {
closeSidebar: () => void; closeSidebar: () => void;
collectionSummaries: CollectionSummaries; collectionSummaries: CollectionSummaries;

View file

@ -16,7 +16,7 @@ export function FamilyUsageProgressBar({
<Box position={'relative'} width="100%"> <Box position={'relative'} width="100%">
<Progressbar <Progressbar
sx={{ backgroundColor: 'transparent' }} sx={{ backgroundColor: 'transparent' }}
value={(userUsage * 100) / totalStorage} value={Math.min((userUsage * 100) / totalStorage, 100)}
/> />
<Progressbar <Progressbar
sx={{ sx={{
@ -28,7 +28,7 @@ export function FamilyUsageProgressBar({
}, },
width: '100%', width: '100%',
}} }}
value={(totalUsage * 100) / totalStorage} value={Math.min((totalUsage * 100) / totalStorage, 100)}
/> />
</Box> </Box>
); );

View file

@ -13,7 +13,7 @@ interface Iprops {
export function IndividualUsageSection({ usage, storage, fileCount }: Iprops) { export function IndividualUsageSection({ usage, storage, fileCount }: Iprops) {
return ( return (
<Box width="100%"> <Box width="100%">
<Progressbar value={(usage * 100) / storage} /> <Progressbar value={Math.min((usage * 100) / storage, 100)} />
<SpaceBetweenFlex <SpaceBetweenFlex
sx={{ sx={{
marginTop: 1.5, marginTop: 1.5,

View file

@ -26,21 +26,24 @@ export default function StorageSection({ usage, storage }: Iprops) {
<Typography variant="body2" color={'text.secondary'}> <Typography variant="body2" color={'text.secondary'}>
{constants.STORAGE} {constants.STORAGE}
</Typography> </Typography>
<DefaultBox>
<Typography <Typography
fontWeight={'bold'} fontWeight={'bold'}
sx={{ fontSize: '24px', lineHeight: '30px' }}> sx={{ fontSize: '24px', lineHeight: '30px' }}>
<DefaultBox>
{`${makeHumanReadableStorage(usage, 'round-up')} ${ {`${makeHumanReadableStorage(usage, 'round-up')} ${
constants.OF constants.OF
} ${makeHumanReadableStorage(storage)} ${constants.USED}`} } ${makeHumanReadableStorage(storage)} ${constants.USED}`}
</DefaultBox> </Typography>
<MobileSmallBox> </DefaultBox>
<MobileSmallBox>
<Typography
fontWeight={'bold'}
sx={{ fontSize: '24px', lineHeight: '30px' }}>
{`${convertBytesToGBs(usage)} / ${convertBytesToGBs( {`${convertBytesToGBs(usage)} / ${convertBytesToGBs(
storage storage
)} ${constants.GB} ${constants.USED}`} )} ${constants.GB} ${constants.USED}`}
</MobileSmallBox> </Typography>
</Typography> </MobileSmallBox>
</Box> </Box>
); );
} }

View file

@ -2,13 +2,13 @@ import { ToggleButton, ToggleButtonGroup } from '@mui/material';
import React from 'react'; import React from 'react';
import DarkModeIcon from '@mui/icons-material/DarkMode'; import DarkModeIcon from '@mui/icons-material/DarkMode';
import LightModeIcon from '@mui/icons-material/LightMode'; import LightModeIcon from '@mui/icons-material/LightMode';
import { THEMES } from 'types/theme'; import { THEME_COLOR } from 'constants/theme';
interface Iprops { interface Iprops {
theme: THEMES; theme: THEME_COLOR;
setTheme: (theme: THEMES) => void; setTheme: (theme: THEME_COLOR) => void;
} }
export default function ThemeSwitcher({ theme, setTheme }: Iprops) { export default function ThemeSwitcher({ theme, setTheme }: Iprops) {
const handleChange = (event, theme: THEMES) => { const handleChange = (event, theme: THEME_COLOR) => {
if (theme !== null) { if (theme !== null) {
setTheme(theme); setTheme(theme);
} }
@ -20,10 +20,10 @@ export default function ThemeSwitcher({ theme, setTheme }: Iprops) {
value={theme} value={theme}
exclusive exclusive
onChange={handleChange}> onChange={handleChange}>
<ToggleButton value={THEMES.LIGHT}> <ToggleButton value={THEME_COLOR.LIGHT}>
<LightModeIcon /> <LightModeIcon />
</ToggleButton> </ToggleButton>
<ToggleButton value={THEMES.DARK}> <ToggleButton value={THEME_COLOR.DARK}>
<DarkModeIcon /> <DarkModeIcon />
</ToggleButton> </ToggleButton>
</ToggleButtonGroup> </ToggleButtonGroup>

View file

@ -11,6 +11,10 @@ import isElectron from 'is-electron';
import WatchFolder from 'components/WatchFolder'; import WatchFolder from 'components/WatchFolder';
import { getDownloadAppMessage } from 'utils/ui'; import { getDownloadAppMessage } from 'utils/ui';
import ThemeSwitcher from './ThemeSwitcher';
import { SpaceBetweenFlex } from 'components/Container';
import { isInternalUser } from 'utils/user';
export default function UtilitySection({ closeSidebar }) { export default function UtilitySection({ closeSidebar }) {
const router = useRouter(); const router = useRouter();
const { const {
@ -18,6 +22,8 @@ export default function UtilitySection({ closeSidebar }) {
startLoading, startLoading,
watchFolderView, watchFolderView,
setWatchFolderView, setWatchFolderView,
theme,
setTheme,
} = useContext(AppContext); } = useContext(AppContext);
const [recoverModalView, setRecoveryModalView] = useState(false); const [recoverModalView, setRecoveryModalView] = useState(false);
@ -70,6 +76,12 @@ export default function UtilitySection({ closeSidebar }) {
<SidebarButton onClick={openRecoveryKeyModal}> <SidebarButton onClick={openRecoveryKeyModal}>
{constants.RECOVERY_KEY} {constants.RECOVERY_KEY}
</SidebarButton> </SidebarButton>
{isInternalUser() && (
<SpaceBetweenFlex sx={{ px: 1.5 }}>
{constants.CHOSE_THEME}
<ThemeSwitcher theme={theme} setTheme={setTheme} />
</SpaceBetweenFlex>
)}
<SidebarButton onClick={openTwoFactorModal}> <SidebarButton onClick={openTwoFactorModal}>
{constants.TWO_FACTOR} {constants.TWO_FACTOR}
</SidebarButton> </SidebarButton>
@ -85,7 +97,6 @@ export default function UtilitySection({ closeSidebar }) {
{/* <SidebarButton onClick={openThumbnailCompressModal}> {/* <SidebarButton onClick={openThumbnailCompressModal}>
{constants.COMPRESS_THUMBNAILS} {constants.COMPRESS_THUMBNAILS}
</SidebarButton> */} </SidebarButton> */}
<RecoveryKey <RecoveryKey
show={recoverModalView} show={recoverModalView}
onHide={closeRecoveryKeyModal} onHide={closeRecoveryKeyModal}
@ -98,7 +109,6 @@ export default function UtilitySection({ closeSidebar }) {
setLoading={startLoading} setLoading={startLoading}
/> />
<WatchFolder open={watchFolderView} onClose={closeWatchFolder} /> <WatchFolder open={watchFolderView} onClose={closeWatchFolder} />
{/* <FixLargeThumbnails {/* <FixLargeThumbnails
isOpen={fixLargeThumbsView} isOpen={fixLargeThumbsView}
hide={() => setFixLargeThumbsView(false)} hide={() => setFixLargeThumbsView(false)}

View file

@ -47,8 +47,12 @@ export default function SignUp(props: SignUpProps) {
{ email, passphrase, confirm }: FormValues, { email, passphrase, confirm }: FormValues,
{ setFieldError }: FormikHelpers<FormValues> { setFieldError }: FormikHelpers<FormValues>
) => { ) => {
setLoading(true);
try { try {
if (passphrase !== confirm) {
setFieldError('confirm', constants.PASSPHRASE_MATCH_ERROR);
return;
}
setLoading(true);
try { try {
setData(LS_KEYS.USER, { email }); setData(LS_KEYS.USER, { email });
await sendOtt(email); await sendOtt(email);
@ -60,30 +64,23 @@ export default function SignUp(props: SignUpProps) {
throw e; throw e;
} }
try { try {
if (passphrase === confirm) { const { keyAttributes, masterKey } =
const { keyAttributes, masterKey } = await generateKeyAttributes(passphrase);
await generateKeyAttributes(passphrase); setData(LS_KEYS.ORIGINAL_KEY_ATTRIBUTES, keyAttributes);
setData(LS_KEYS.ORIGINAL_KEY_ATTRIBUTES, keyAttributes); await generateAndSaveIntermediateKeyAttributes(
await generateAndSaveIntermediateKeyAttributes( passphrase,
passphrase, keyAttributes,
keyAttributes, masterKey
masterKey
);
await saveKeyInSessionStore(
SESSION_KEYS.ENCRYPTION_KEY,
masterKey
);
setJustSignedUp(true);
router.push(PAGES.VERIFY);
} else {
setFieldError('confirm', constants.PASSPHRASE_MATCH_ERROR);
}
} catch (e) {
setFieldError(
'passphrase',
constants.PASSWORD_GENERATION_FAILED
); );
await saveKeyInSessionStore(
SESSION_KEYS.ENCRYPTION_KEY,
masterKey
);
setJustSignedUp(true);
router.push(PAGES.VERIFY);
} catch (e) {
setFieldError('confirm', constants.PASSWORD_GENERATION_FAILED);
throw e; throw e;
} }
} catch (err) { } catch (err) {

View file

@ -27,6 +27,8 @@ export interface SingleInputFormProps {
caption?: any; caption?: any;
hiddenPostInput?: any; hiddenPostInput?: any;
autoComplete?: string; autoComplete?: string;
blockButton?: boolean;
hiddenLabel?: boolean;
} }
export default function SingleInputForm(props: SingleInputFormProps) { export default function SingleInputForm(props: SingleInputFormProps) {
@ -87,12 +89,15 @@ export default function SingleInputForm(props: SingleInputFormProps) {
<form noValidate onSubmit={handleSubmit}> <form noValidate onSubmit={handleSubmit}>
{props.hiddenPreInput} {props.hiddenPreInput}
<TextField <TextField
hiddenLabel={props.hiddenLabel}
variant="filled" variant="filled"
fullWidth fullWidth
type={showPassword ? 'text' : props.fieldType} type={showPassword ? 'text' : props.fieldType}
id={props.fieldType} id={props.fieldType}
name={props.fieldType} name={props.fieldType}
label={props.placeholder} {...(props.hiddenLabel
? { placeholder: props.placeholder }
: { label: props.placeholder })}
value={values.inputValue} value={values.inputValue}
onChange={handleChange('inputValue')} onChange={handleChange('inputValue')}
error={Boolean(errors.inputValue)} error={Boolean(errors.inputValue)}
@ -124,21 +129,35 @@ export default function SingleInputForm(props: SingleInputFormProps) {
{props.caption} {props.caption}
</FormHelperText> </FormHelperText>
{props.hiddenPostInput} {props.hiddenPostInput}
<FlexWrapper justifyContent={'flex-end'}> <FlexWrapper
justifyContent={'flex-end'}
flexWrap={
props.blockButton ? 'wrap-reverse' : 'nowrap'
}>
{props.secondaryButtonAction && ( {props.secondaryButtonAction && (
<Button <Button
onClick={props.secondaryButtonAction} onClick={props.secondaryButtonAction}
size="large" size="large"
color="secondary" color="secondary"
sx={{ sx={{
'&&&': { mt: 2, mb: 4, mr: 1, ...buttonSx }, '&&&': {
mt: !props.blockButton ? 2 : 0.5,
mb: !props.blockButton ? 4 : 0,
mr: !props.blockButton ? 1 : 0,
...buttonSx,
},
}} }}
{...restSubmitButtonProps}> {...restSubmitButtonProps}>
{constants.CANCEL} {constants.CANCEL}
</Button> </Button>
)} )}
<SubmitButton <SubmitButton
sx={{ '&&&': { mt: 2, ...buttonSx } }} sx={{
'&&&': {
mt: 2,
...buttonSx,
},
}}
buttonText={props.buttonText} buttonText={props.buttonText}
loading={loading} loading={loading}
{...restSubmitButtonProps} {...restSubmitButtonProps}

View file

@ -26,10 +26,15 @@ const SubmitButton: FC<ButtonProps<'button', SubmitButtonProps>> = ({
disabled={disabled || loading || success} disabled={disabled || loading || success}
sx={{ sx={{
my: 4, my: 4,
'&.Mui-disabled': { ...(loading
backgroundColor: (theme) => theme.palette.accent.main, ? {
color: (theme) => theme.palette.text.primary, '&.Mui-disabled': {
}, backgroundColor: (theme) =>
theme.palette.accent.main,
color: (theme) => theme.palette.text.primary,
},
}
: {}),
...sx, ...sx,
}} }}
{...props}> {...props}>

View file

@ -1,4 +1,5 @@
import { ArrowBack, Close } from '@mui/icons-material'; import Close from '@mui/icons-material/Close';
import ArrowBack from '@mui/icons-material/ArrowBack';
import { Box, IconButton, Typography } from '@mui/material'; import { Box, IconButton, Typography } from '@mui/material';
import React from 'react'; import React from 'react';
import { FlexWrapper } from './Container'; import { FlexWrapper } from './Container';

View file

@ -1,11 +1,11 @@
import React from 'react'; import React from 'react';
import { IconButton, styled } from '@mui/material'; import { ButtonProps, IconButton, styled } from '@mui/material';
import FileUploadOutlinedIcon from '@mui/icons-material/FileUploadOutlined'; import FileUploadOutlinedIcon from '@mui/icons-material/FileUploadOutlined';
import { Button } from '@mui/material'; import { Button } from '@mui/material';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
import uploadManager from 'services/upload/uploadManager'; import uploadManager from 'services/upload/uploadManager';
const Wrapper = styled('div')` const Wrapper = styled('div')<{ $disableShrink: boolean }>`
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@ -14,22 +14,35 @@ const Wrapper = styled('div')`
& .mobile-button { & .mobile-button {
display: none; display: none;
} }
@media (max-width: 624px) { ${({ $disableShrink }) =>
!$disableShrink &&
`@media (max-width: 624px) {
& .mobile-button { & .mobile-button {
display: block; display: block;
} }
& .desktop-button { & .desktop-button {
display: none; display: none;
} }
} }`}
`; `;
interface Iprops { interface Iprops {
openUploader: () => void; openUploader: () => void;
text?: string;
color?: ButtonProps['color'];
disableShrink?: boolean;
icon?: JSX.Element;
} }
function UploadButton({ openUploader }: Iprops) { function UploadButton({
openUploader,
text,
color,
disableShrink,
icon,
}: Iprops) {
return ( return (
<Wrapper <Wrapper
$disableShrink={disableShrink}
style={{ style={{
cursor: !uploadManager.shouldAllowNewUpload() && 'not-allowed', cursor: !uploadManager.shouldAllowNewUpload() && 'not-allowed',
}}> }}>
@ -37,9 +50,9 @@ function UploadButton({ openUploader }: Iprops) {
onClick={openUploader} onClick={openUploader}
disabled={!uploadManager.shouldAllowNewUpload()} disabled={!uploadManager.shouldAllowNewUpload()}
className="desktop-button" className="desktop-button"
color="secondary" color={color ?? 'secondary'}
startIcon={<FileUploadOutlinedIcon />}> startIcon={icon ?? <FileUploadOutlinedIcon />}>
{constants.UPLOAD} {text ?? constants.UPLOAD}
</Button> </Button>
<IconButton <IconButton

View file

@ -10,6 +10,7 @@ import { NotUploadSectionHeader } from './styledComponents';
import UploadProgressContext from 'contexts/uploadProgress'; import UploadProgressContext from 'contexts/uploadProgress';
import { dialogCloseHandler } from 'components/DialogBox/TitleWithCloseButton'; import { dialogCloseHandler } from 'components/DialogBox/TitleWithCloseButton';
import { APP_DOWNLOAD_URL } from 'utils/common'; import { APP_DOWNLOAD_URL } from 'utils/common';
import { ENTE_WEBSITE_LINK } from 'constants/urls';
export function UploadProgressDialog() { export function UploadProgressDialog() {
const { open, onClose, uploadStage, finishedUploads } = useContext( const { open, onClose, uploadStage, finishedUploads } = useContext(
@ -26,7 +27,8 @@ export function UploadProgressDialog() {
finishedUploads.get(UPLOAD_RESULT.LARGER_THAN_AVAILABLE_STORAGE) finishedUploads.get(UPLOAD_RESULT.LARGER_THAN_AVAILABLE_STORAGE)
?.length > 0 || ?.length > 0 ||
finishedUploads.get(UPLOAD_RESULT.TOO_LARGE)?.length > 0 || finishedUploads.get(UPLOAD_RESULT.TOO_LARGE)?.length > 0 ||
finishedUploads.get(UPLOAD_RESULT.UNSUPPORTED)?.length > 0 finishedUploads.get(UPLOAD_RESULT.UNSUPPORTED)?.length > 0 ||
finishedUploads.get(UPLOAD_RESULT.SKIPPED_VIDEOS)?.length > 0
) { ) {
setHasUnUploadedFiles(true); setHasUnUploadedFiles(true);
} else { } else {
@ -84,6 +86,13 @@ export function UploadProgressDialog() {
uploadResult={UPLOAD_RESULT.FAILED} uploadResult={UPLOAD_RESULT.FAILED}
sectionTitle={constants.FAILED_UPLOADS} sectionTitle={constants.FAILED_UPLOADS}
/> />
<ResultSection
uploadResult={UPLOAD_RESULT.SKIPPED_VIDEOS}
sectionTitle={constants.SKIPPED_VIDEOS}
sectionInfo={constants.SKIPPED_VIDEOS_INFO(
ENTE_WEBSITE_LINK
)}
/>
<ResultSection <ResultSection
uploadResult={UPLOAD_RESULT.ALREADY_UPLOADED} uploadResult={UPLOAD_RESULT.ALREADY_UPLOADED}
sectionTitle={constants.SKIPPED_FILES} sectionTitle={constants.SKIPPED_FILES}

View file

@ -1,19 +1,48 @@
import React from 'react'; import React, { useContext, useEffect, useRef } from 'react';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
import { default as FileUploadIcon } from '@mui/icons-material/ImageOutlined'; import { default as FileUploadIcon } from '@mui/icons-material/ImageOutlined';
import { default as FolderUploadIcon } from '@mui/icons-material/PermMediaOutlined'; import { default as FolderUploadIcon } from '@mui/icons-material/PermMediaOutlined';
import GoogleIcon from '@mui/icons-material/Google'; import GoogleIcon from '@mui/icons-material/Google';
import { UploadTypeOption } from './option'; import { UploadTypeOption } from './option';
import DialogTitleWithCloseButton from 'components/DialogBox/TitleWithCloseButton'; import DialogTitleWithCloseButton, {
dialogCloseHandler,
} from 'components/DialogBox/TitleWithCloseButton';
import { Box, Dialog, Stack, Typography } from '@mui/material'; import { Box, Dialog, Stack, Typography } from '@mui/material';
import { PublicCollectionGalleryContext } from 'utils/publicCollectionGallery';
import { isMobileOrTable } from 'utils/common/deviceDetection';
interface Iprops {
onClose: () => void;
show: boolean;
uploadFiles: () => void;
uploadFolders: () => void;
uploadGoogleTakeoutZips: () => void;
hideZipUploadOption?: boolean;
}
export default function UploadTypeSelector({ export default function UploadTypeSelector({
onHide, onClose,
show, show,
uploadFiles, uploadFiles,
uploadFolders, uploadFolders,
uploadGoogleTakeoutZips, uploadGoogleTakeoutZips,
}) { hideZipUploadOption,
}: Iprops) {
const publicCollectionGalleryContext = useContext(
PublicCollectionGalleryContext
);
const directlyShowUploadFiles = useRef(isMobileOrTable());
useEffect(() => {
if (
show &&
directlyShowUploadFiles.current &&
publicCollectionGalleryContext.accessedThroughSharedURL
) {
uploadFiles();
onClose();
}
}, [show]);
return ( return (
<Dialog <Dialog
open={show} open={show}
@ -24,9 +53,11 @@ export default function UploadTypeSelector({
[theme.breakpoints.down(360)]: { p: 0 }, [theme.breakpoints.down(360)]: { p: 0 },
}), }),
}} }}
onClose={onHide}> onClose={dialogCloseHandler({ onClose })}>
<DialogTitleWithCloseButton onClose={onHide}> <DialogTitleWithCloseButton onClose={onClose}>
{constants.UPLOAD} {publicCollectionGalleryContext.accessedThroughSharedURL
? constants.SELECT_PHOTOS
: constants.UPLOAD}
</DialogTitleWithCloseButton> </DialogTitleWithCloseButton>
<Box p={1.5} pt={0.5}> <Box p={1.5} pt={0.5}>
<Stack spacing={0.5}> <Stack spacing={0.5}>
@ -40,11 +71,13 @@ export default function UploadTypeSelector({
startIcon={<FolderUploadIcon />}> startIcon={<FolderUploadIcon />}>
{constants.UPLOAD_DIRS} {constants.UPLOAD_DIRS}
</UploadTypeOption> </UploadTypeOption>
<UploadTypeOption {!hideZipUploadOption && (
onClick={uploadGoogleTakeoutZips} <UploadTypeOption
startIcon={<GoogleIcon />}> onClick={uploadGoogleTakeoutZips}
{constants.UPLOAD_GOOGLE_TAKEOUT} startIcon={<GoogleIcon />}>
</UploadTypeOption> {constants.UPLOAD_GOOGLE_TAKEOUT}
</UploadTypeOption>
)}
</Stack> </Stack>
<Typography p={1.5} pt={4} color="text.secondary"> <Typography p={1.5} pt={4} color="text.secondary">
{constants.DRAG_AND_DROP_HINT} {constants.DRAG_AND_DROP_HINT}

View file

@ -10,7 +10,6 @@ import { SetCollections, SetCollectionSelectorAttributes } from 'types/gallery';
import { GalleryContext } from 'pages/gallery'; import { GalleryContext } from 'pages/gallery';
import { AppContext } from 'pages/_app'; import { AppContext } from 'pages/_app';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
import UploadManager from 'services/upload/uploadManager';
import uploadManager from 'services/upload/uploadManager'; import uploadManager from 'services/upload/uploadManager';
import ImportService from 'services/importService'; import ImportService from 'services/importService';
import isElectron from 'is-electron'; import isElectron from 'is-electron';
@ -40,7 +39,10 @@ import {
PICKED_UPLOAD_TYPE, PICKED_UPLOAD_TYPE,
} from 'constants/upload'; } from 'constants/upload';
import importService from 'services/importService'; import importService from 'services/importService';
import { getDownloadAppMessage } from 'utils/ui'; import {
getDownloadAppMessage,
getRootLevelFileWithFolderNotAllowMessage,
} from 'utils/ui';
import UploadTypeSelector from './UploadTypeSelector'; import UploadTypeSelector from './UploadTypeSelector';
import { import {
filterOutSystemFiles, filterOutSystemFiles,
@ -50,21 +52,28 @@ import {
import { getUserOwnedCollections } from 'utils/collection'; import { getUserOwnedCollections } from 'utils/collection';
import billingService from 'services/billingService'; import billingService from 'services/billingService';
import { addLogLine } from 'utils/logging'; import { addLogLine } from 'utils/logging';
import { PublicCollectionGalleryContext } from 'utils/publicCollectionGallery';
import UserNameInputDialog from 'components/UserNameInputDialog';
import {
getPublicCollectionUID,
getPublicCollectionUploaderName,
savePublicCollectionUploaderName,
} from 'services/publicCollectionService';
const FIRST_ALBUM_NAME = 'My First Album'; const FIRST_ALBUM_NAME = 'My First Album';
interface Props { interface Props {
syncWithRemote: (force?: boolean, silent?: boolean) => Promise<void>; syncWithRemote: (force?: boolean, silent?: boolean) => Promise<void>;
closeCollectionSelector: () => void; closeCollectionSelector?: () => void;
closeUploadTypeSelector: () => void; closeUploadTypeSelector: () => void;
setCollectionSelectorAttributes: SetCollectionSelectorAttributes; setCollectionSelectorAttributes?: SetCollectionSelectorAttributes;
setCollectionNamerAttributes: SetCollectionNamerAttributes; setCollectionNamerAttributes?: SetCollectionNamerAttributes;
setLoading: SetLoading; setLoading: SetLoading;
setShouldDisableDropzone: (value: boolean) => void; setShouldDisableDropzone: (value: boolean) => void;
showCollectionSelector: () => void; showCollectionSelector?: () => void;
setFiles: SetFiles; setFiles: SetFiles;
setCollections: SetCollections; setCollections?: SetCollections;
isFirstUpload: boolean; isFirstUpload?: boolean;
uploadTypeSelectorView: boolean; uploadTypeSelectorView: boolean;
showSessionExpiredMessage: () => void; showSessionExpiredMessage: () => void;
showUploadFilesDialog: () => void; showUploadFilesDialog: () => void;
@ -72,9 +81,17 @@ interface Props {
webFolderSelectorFiles: File[]; webFolderSelectorFiles: File[];
webFileSelectorFiles: File[]; webFileSelectorFiles: File[];
dragAndDropFiles: File[]; dragAndDropFiles: File[];
zipUploadDisabled?: boolean;
uploadCollection?: Collection;
} }
export default function Uploader(props: Props) { export default function Uploader(props: Props) {
const appContext = useContext(AppContext);
const galleryContext = useContext(GalleryContext);
const publicCollectionGalleryContext = useContext(
PublicCollectionGalleryContext
);
const [uploadProgressView, setUploadProgressView] = useState(false); const [uploadProgressView, setUploadProgressView] = useState(false);
const [uploadStage, setUploadStage] = useState<UPLOAD_STAGES>( const [uploadStage, setUploadStage] = useState<UPLOAD_STAGES>(
UPLOAD_STAGES.START UPLOAD_STAGES.START
@ -93,11 +110,13 @@ export default function Uploader(props: Props) {
const [hasLivePhotos, setHasLivePhotos] = useState(false); const [hasLivePhotos, setHasLivePhotos] = useState(false);
const [choiceModalView, setChoiceModalView] = useState(false); const [choiceModalView, setChoiceModalView] = useState(false);
const [userNameInputDialogView, setUserNameInputDialogView] =
useState(false);
const [importSuggestion, setImportSuggestion] = useState<ImportSuggestion>( const [importSuggestion, setImportSuggestion] = useState<ImportSuggestion>(
DEFAULT_IMPORT_SUGGESTION DEFAULT_IMPORT_SUGGESTION
); );
const appContext = useContext(AppContext); const [electronFiles, setElectronFiles] = useState<ElectronFile[]>(null);
const galleryContext = useContext(GalleryContext); const [webFiles, setWebFiles] = useState([]);
const toUploadFiles = useRef<File[] | ElectronFile[]>(null); const toUploadFiles = useRef<File[] | ElectronFile[]>(null);
const isPendingDesktopUpload = useRef(false); const isPendingDesktopUpload = useRef(false);
@ -106,20 +125,33 @@ export default function Uploader(props: Props) {
const pickedUploadType = useRef<PICKED_UPLOAD_TYPE>(null); const pickedUploadType = useRef<PICKED_UPLOAD_TYPE>(null);
const zipPaths = useRef<string[]>(null); const zipPaths = useRef<string[]>(null);
const currentUploadPromise = useRef<Promise<void>>(null); const currentUploadPromise = useRef<Promise<void>>(null);
const [electronFiles, setElectronFiles] = useState<ElectronFile[]>(null); const uploadRunning = useRef(false);
const [webFiles, setWebFiles] = useState([]); const uploaderNameRef = useRef<string>(null);
const closeUploadProgress = () => setUploadProgressView(false); const closeUploadProgress = () => setUploadProgressView(false);
const showUserNameInputDialog = () => setUserNameInputDialogView(true);
const setCollectionName = (collectionName: string) => { const setCollectionName = (collectionName: string) => {
isPendingDesktopUpload.current = true; isPendingDesktopUpload.current = true;
pendingDesktopUploadCollectionName.current = collectionName; pendingDesktopUploadCollectionName.current = collectionName;
}; };
const uploadRunning = useRef(false); const handleChoiceModalClose = () => {
setChoiceModalView(false);
uploadRunning.current = false;
};
const handleCollectionSelectorCancel = () => {
uploadRunning.current = false;
appContext.resetSharedFiles();
};
const handleUserNameInputDialogClose = () => {
setUserNameInputDialogView(false);
uploadRunning.current = false;
};
useEffect(() => { useEffect(() => {
UploadManager.init( uploadManager.init(
{ {
setPercentComplete, setPercentComplete,
setUploadCounter, setUploadCounter,
@ -129,12 +161,16 @@ export default function Uploader(props: Props) {
setUploadFilenames: setUploadFileNames, setUploadFilenames: setUploadFileNames,
setHasLivePhotos, setHasLivePhotos,
}, },
props.setFiles props.setFiles,
publicCollectionGalleryContext
); );
if (isElectron() && ImportService.checkAllElectronAPIsExists()) { if (isElectron() && ImportService.checkAllElectronAPIsExists()) {
ImportService.getPendingUploads().then( ImportService.getPendingUploads().then(
({ files: electronFiles, collectionName, type }) => { ({ files: electronFiles, collectionName, type }) => {
addLogLine(
`found pending desktop upload, resuming uploads`
);
resumeDesktopUpload(type, electronFiles, collectionName); resumeDesktopUpload(type, electronFiles, collectionName);
} }
); );
@ -145,7 +181,11 @@ export default function Uploader(props: Props) {
appContext.setIsFolderSyncRunning appContext.setIsFolderSyncRunning
); );
} }
}, []); }, [
publicCollectionGalleryContext.accessedThroughSharedURL,
publicCollectionGalleryContext.token,
publicCollectionGalleryContext.passwordToken,
]);
// this handles the change of selectorFiles changes on web when user selects // this handles the change of selectorFiles changes on web when user selects
// files for upload through the opened file/folder selector or dragAndDrop them // files for upload through the opened file/folder selector or dragAndDrop them
@ -160,13 +200,16 @@ export default function Uploader(props: Props) {
pickedUploadType.current === PICKED_UPLOAD_TYPE.FOLDERS && pickedUploadType.current === PICKED_UPLOAD_TYPE.FOLDERS &&
props.webFolderSelectorFiles?.length > 0 props.webFolderSelectorFiles?.length > 0
) { ) {
addLogLine(`received folder upload request`);
setWebFiles(props.webFolderSelectorFiles); setWebFiles(props.webFolderSelectorFiles);
} else if ( } else if (
pickedUploadType.current === PICKED_UPLOAD_TYPE.FILES && pickedUploadType.current === PICKED_UPLOAD_TYPE.FILES &&
props.webFileSelectorFiles?.length > 0 props.webFileSelectorFiles?.length > 0
) { ) {
addLogLine(`received file upload request`);
setWebFiles(props.webFileSelectorFiles); setWebFiles(props.webFileSelectorFiles);
} else if (props.dragAndDropFiles?.length > 0) { } else if (props.dragAndDropFiles?.length > 0) {
addLogLine(`received drag and drop upload request`);
setWebFiles(props.dragAndDropFiles); setWebFiles(props.dragAndDropFiles);
} }
}, [ }, [
@ -181,17 +224,37 @@ export default function Uploader(props: Props) {
webFiles?.length > 0 || webFiles?.length > 0 ||
appContext.sharedFiles?.length > 0 appContext.sharedFiles?.length > 0
) { ) {
addLogLine(
`upload request type:${
electronFiles?.length > 0
? 'electronFiles'
: webFiles?.length > 0
? 'webFiles'
: 'sharedFiles'
} count ${
electronFiles?.length ??
webFiles?.length ??
appContext?.sharedFiles.length
}`
);
if (uploadRunning.current) { if (uploadRunning.current) {
if (watchFolderService.isUploadRunning()) { if (watchFolderService.isUploadRunning()) {
addLogLine(
'watchFolder upload was running, pausing it to run user upload'
);
// pause watch folder service on user upload // pause watch folder service on user upload
watchFolderService.pauseRunningSync(); watchFolderService.pauseRunningSync();
} else { } else {
addLogLine(
'an upload is already running, rejecting new upload request'
);
// no-op // no-op
// a user upload is already in progress // a user upload is already in progress
return; return;
} }
} }
if (isCanvasBlocked()) { if (isCanvasBlocked()) {
addLogLine('canvas blocked, blocking upload');
appContext.setDialogMessage({ appContext.setDialogMessage({
title: constants.CANVAS_BLOCKED_TITLE, title: constants.CANVAS_BLOCKED_TITLE,
@ -236,7 +299,8 @@ export default function Uploader(props: Props) {
handleCollectionCreationAndUpload( handleCollectionCreationAndUpload(
importSuggestion, importSuggestion,
props.isFirstUpload, props.isFirstUpload,
pickedUploadType.current pickedUploadType.current,
publicCollectionGalleryContext.accessedThroughSharedURL
); );
pickedUploadType.current = null; pickedUploadType.current = null;
props.setLoading(false); props.setLoading(false);
@ -257,14 +321,20 @@ export default function Uploader(props: Props) {
}; };
const preCollectionCreationAction = async () => { const preCollectionCreationAction = async () => {
props.closeCollectionSelector(); props.closeCollectionSelector?.();
props.setShouldDisableDropzone(!uploadManager.shouldAllowNewUpload()); props.setShouldDisableDropzone(!uploadManager.shouldAllowNewUpload());
setUploadStage(UPLOAD_STAGES.START); setUploadStage(UPLOAD_STAGES.START);
setUploadProgressView(true); setUploadProgressView(true);
}; };
const uploadFilesToExistingCollection = async (collection: Collection) => { const uploadFilesToExistingCollection = async (
collection: Collection,
uploaderName?: string
) => {
try { try {
addLogLine(
`upload file to an existing collection - "${collection.name}"`
);
await preCollectionCreationAction(); await preCollectionCreationAction();
const filesWithCollectionToUpload: FileWithCollection[] = const filesWithCollectionToUpload: FileWithCollection[] =
toUploadFiles.current.map((file, index) => ({ toUploadFiles.current.map((file, index) => ({
@ -272,10 +342,11 @@ export default function Uploader(props: Props) {
localID: index, localID: index,
collectionID: collection.id, collectionID: collection.id,
})); }));
waitInQueueAndUploadFiles(filesWithCollectionToUpload, [ waitInQueueAndUploadFiles(
collection, filesWithCollectionToUpload,
]); [collection],
toUploadFiles.current = null; uploaderName
);
} catch (e) { } catch (e) {
logError(e, 'Failed to upload files to existing collections'); logError(e, 'Failed to upload files to existing collections');
} }
@ -286,6 +357,9 @@ export default function Uploader(props: Props) {
collectionName?: string collectionName?: string
) => { ) => {
try { try {
addLogLine(
`upload file to an new collections strategy:${strategy} ,collectionName:${collectionName}`
);
await preCollectionCreationAction(); await preCollectionCreationAction();
let filesWithCollectionToUpload: FileWithCollection[] = []; let filesWithCollectionToUpload: FileWithCollection[] = [];
const collections: Collection[] = []; const collections: Collection[] = [];
@ -303,6 +377,9 @@ export default function Uploader(props: Props) {
toUploadFiles.current toUploadFiles.current
); );
} }
addLogLine(
`upload collections - [${[...collectionNameToFilesMap.keys()]}]`
);
try { try {
const existingCollection = getUserOwnedCollections( const existingCollection = getUserOwnedCollections(
await syncCollections() await syncCollections()
@ -350,13 +427,18 @@ export default function Uploader(props: Props) {
const waitInQueueAndUploadFiles = ( const waitInQueueAndUploadFiles = (
filesWithCollectionToUploadIn: FileWithCollection[], filesWithCollectionToUploadIn: FileWithCollection[],
collections: Collection[] collections: Collection[],
uploaderName?: string
) => { ) => {
const currentPromise = currentUploadPromise.current; const currentPromise = currentUploadPromise.current;
currentUploadPromise.current = waitAndRun( currentUploadPromise.current = waitAndRun(
currentPromise, currentPromise,
async () => async () =>
await uploadFiles(filesWithCollectionToUploadIn, collections) await uploadFiles(
filesWithCollectionToUploadIn,
collections,
uploaderName
)
); );
}; };
@ -374,9 +456,11 @@ export default function Uploader(props: Props) {
const uploadFiles = async ( const uploadFiles = async (
filesWithCollectionToUploadIn: FileWithCollection[], filesWithCollectionToUploadIn: FileWithCollection[],
collections: Collection[] collections: Collection[],
uploaderName?: string
) => { ) => {
try { try {
addLogLine('uploadFiles called');
preUploadAction(); preUploadAction();
if ( if (
isElectron() && isElectron() &&
@ -401,7 +485,8 @@ export default function Uploader(props: Props) {
const shouldCloseUploadProgress = const shouldCloseUploadProgress =
await uploadManager.queueFilesForUpload( await uploadManager.queueFilesForUpload(
filesWithCollectionToUploadIn, filesWithCollectionToUploadIn,
collections collections,
uploaderName
); );
if (shouldCloseUploadProgress) { if (shouldCloseUploadProgress) {
closeUploadProgress(); closeUploadProgress();
@ -418,6 +503,7 @@ export default function Uploader(props: Props) {
} }
} }
} catch (err) { } catch (err) {
logError(err, 'failed to upload files');
showUserFacingError(err.message); showUserFacingError(err.message);
closeUploadProgress(); closeUploadProgress();
throw err; throw err;
@ -430,13 +516,16 @@ export default function Uploader(props: Props) {
try { try {
addLogLine('user retrying failed upload'); addLogLine('user retrying failed upload');
const filesWithCollections = const filesWithCollections =
await uploadManager.getFailedFilesWithCollections(); uploadManager.getFailedFilesWithCollections();
const uploaderName = uploadManager.getUploaderName();
await preUploadAction(); await preUploadAction();
await uploadManager.queueFilesForUpload( await uploadManager.queueFilesForUpload(
filesWithCollections.files, filesWithCollections.files,
filesWithCollections.collections filesWithCollections.collections,
uploaderName
); );
} catch (err) { } catch (err) {
logError(err, 'retry failed files failed');
showUserFacingError(err.message); showUserFacingError(err.message);
closeUploadProgress(); closeUploadProgress();
} finally { } finally {
@ -478,6 +567,7 @@ export default function Uploader(props: Props) {
const uploadToSingleNewCollection = (collectionName: string) => { const uploadToSingleNewCollection = (collectionName: string) => {
if (collectionName) { if (collectionName) {
addLogLine(`upload to single collection - "${collectionName}"`);
uploadFilesToNewCollections( uploadFilesToNewCollections(
UPLOAD_STRATEGY.SINGLE_COLLECTION, UPLOAD_STRATEGY.SINGLE_COLLECTION,
collectionName collectionName
@ -495,45 +585,75 @@ export default function Uploader(props: Props) {
}); });
}; };
const handleCollectionCreationAndUpload = ( const handleCollectionCreationAndUpload = async (
importSuggestion: ImportSuggestion, importSuggestion: ImportSuggestion,
isFirstUpload: boolean, isFirstUpload: boolean,
pickedUploadType: PICKED_UPLOAD_TYPE pickedUploadType: PICKED_UPLOAD_TYPE,
accessedThroughSharedURL?: boolean
) => { ) => {
if (isPendingDesktopUpload.current) { try {
isPendingDesktopUpload.current = false; if (accessedThroughSharedURL) {
if (pendingDesktopUploadCollectionName.current) { addLogLine(
uploadToSingleNewCollection( `uploading files to pulbic collection - ${props.uploadCollection.name} - ${props.uploadCollection.id}`
pendingDesktopUploadCollectionName.current
); );
pendingDesktopUploadCollectionName.current = null; const uploaderName = await getPublicCollectionUploaderName(
} else { getPublicCollectionUID(publicCollectionGalleryContext.token)
);
uploaderNameRef.current = uploaderName;
showUserNameInputDialog();
return;
}
if (isPendingDesktopUpload.current) {
isPendingDesktopUpload.current = false;
if (pendingDesktopUploadCollectionName.current) {
addLogLine(
`upload pending files to collection - ${pendingDesktopUploadCollectionName.current}`
);
uploadToSingleNewCollection(
pendingDesktopUploadCollectionName.current
);
pendingDesktopUploadCollectionName.current = null;
} else {
addLogLine(
`pending upload - strategy - "multiple collections" `
);
uploadFilesToNewCollections(
UPLOAD_STRATEGY.COLLECTION_PER_FOLDER
);
}
return;
}
if (isElectron() && pickedUploadType === PICKED_UPLOAD_TYPE.ZIPS) {
addLogLine('uploading zip files');
uploadFilesToNewCollections( uploadFilesToNewCollections(
UPLOAD_STRATEGY.COLLECTION_PER_FOLDER UPLOAD_STRATEGY.COLLECTION_PER_FOLDER
); );
return;
} }
return; if (isFirstUpload && !importSuggestion.rootFolderName) {
importSuggestion.rootFolderName = FIRST_ALBUM_NAME;
}
let showNextModal = () => {};
if (importSuggestion.hasNestedFolders) {
addLogLine(`nested folders detected`);
showNextModal = () => setChoiceModalView(true);
} else {
showNextModal = () =>
uploadToSingleNewCollection(
importSuggestion.rootFolderName
);
}
props.setCollectionSelectorAttributes({
callback: uploadFilesToExistingCollection,
onCancel: handleCollectionSelectorCancel,
showNextModal,
title: constants.UPLOAD_TO_COLLECTION,
});
} catch (e) {
logError(e, 'handleCollectionCreationAndUpload failed');
} }
if (isElectron() && pickedUploadType === PICKED_UPLOAD_TYPE.ZIPS) {
uploadFilesToNewCollections(UPLOAD_STRATEGY.COLLECTION_PER_FOLDER);
return;
}
if (isFirstUpload && !importSuggestion.rootFolderName) {
importSuggestion.rootFolderName = FIRST_ALBUM_NAME;
}
let showNextModal = () => {};
if (importSuggestion.hasNestedFolders) {
showNextModal = () => setChoiceModalView(true);
} else {
showNextModal = () =>
uploadToSingleNewCollection(importSuggestion.rootFolderName);
}
props.setCollectionSelectorAttributes({
callback: uploadFilesToExistingCollection,
showNextModal,
title: constants.UPLOAD_TO_COLLECTION,
});
}; };
const handleDesktopUpload = async (type: PICKED_UPLOAD_TYPE) => { const handleDesktopUpload = async (type: PICKED_UPLOAD_TYPE) => {
let files: ElectronFile[]; let files: ElectronFile[];
pickedUploadType.current = type; pickedUploadType.current = type;
@ -547,6 +667,9 @@ export default function Uploader(props: Props) {
zipPaths.current = response.zipPaths; zipPaths.current = response.zipPaths;
} }
if (files?.length > 0) { if (files?.length > 0) {
addLogLine(
` desktop upload for type:${type} and fileCount: ${files?.length} requested`
);
setElectronFiles(files); setElectronFiles(files);
props.closeUploadTypeSelector(); props.closeUploadTypeSelector();
} }
@ -579,26 +702,57 @@ export default function Uploader(props: Props) {
const handleFolderUpload = handleUpload(PICKED_UPLOAD_TYPE.FOLDERS); const handleFolderUpload = handleUpload(PICKED_UPLOAD_TYPE.FOLDERS);
const handleZipUpload = handleUpload(PICKED_UPLOAD_TYPE.ZIPS); const handleZipUpload = handleUpload(PICKED_UPLOAD_TYPE.ZIPS);
const handlePublicUpload = async (
uploaderName: string,
skipSave?: boolean
) => {
try {
if (!skipSave) {
savePublicCollectionUploaderName(
getPublicCollectionUID(
publicCollectionGalleryContext.token
),
uploaderName
);
}
await uploadFilesToExistingCollection(
props.uploadCollection,
uploaderName
);
} catch (e) {
logError(e, 'public upload failed ');
}
};
const handleUploadToSingleCollection = () => {
uploadToSingleNewCollection(importSuggestion.rootFolderName);
};
const handleUploadToMultipleCollections = () => {
if (importSuggestion.hasRootLevelFileWithFolder) {
appContext.setDialogMessage(
getRootLevelFileWithFolderNotAllowMessage()
);
return;
}
uploadFilesToNewCollections(UPLOAD_STRATEGY.COLLECTION_PER_FOLDER);
};
return ( return (
<> <>
<UploadStrategyChoiceModal <UploadStrategyChoiceModal
open={choiceModalView} open={choiceModalView}
onClose={() => setChoiceModalView(false)} onClose={handleChoiceModalClose}
uploadToSingleCollection={() => uploadToSingleCollection={handleUploadToSingleCollection}
uploadToSingleNewCollection(importSuggestion.rootFolderName) uploadToMultipleCollection={handleUploadToMultipleCollections}
}
uploadToMultipleCollection={() =>
uploadFilesToNewCollections(
UPLOAD_STRATEGY.COLLECTION_PER_FOLDER
)
}
/> />
<UploadTypeSelector <UploadTypeSelector
show={props.uploadTypeSelectorView} show={props.uploadTypeSelectorView}
onHide={props.closeUploadTypeSelector} onClose={props.closeUploadTypeSelector}
uploadFiles={handleFileUpload} uploadFiles={handleFileUpload}
uploadFolders={handleFolderUpload} uploadFolders={handleFolderUpload}
uploadGoogleTakeoutZips={handleZipUpload} uploadGoogleTakeoutZips={handleZipUpload}
hideZipUploadOption={props.zipUploadDisabled}
/> />
<UploadProgress <UploadProgress
open={uploadProgressView} open={uploadProgressView}
@ -613,6 +767,13 @@ export default function Uploader(props: Props) {
finishedUploads={finishedUploads} finishedUploads={finishedUploads}
cancelUploads={cancelUploads} cancelUploads={cancelUploads}
/> />
<UserNameInputDialog
open={userNameInputDialogView}
onClose={handleUserNameInputDialogClose}
onNameSubmit={handlePublicUpload}
toUploadFilesCount={toUploadFiles.current?.length}
uploaderName={uploaderNameRef.current}
/>
</> </>
); );
} }

View file

@ -0,0 +1,43 @@
import React from 'react';
import constants from 'utils/strings/constants';
import DialogBox from './DialogBox';
import AutoAwesomeOutlinedIcon from '@mui/icons-material/AutoAwesomeOutlined';
import { Typography } from '@mui/material';
import SingleInputForm from './SingleInputForm';
export default function UserNameInputDialog({
open,
onClose,
onNameSubmit,
toUploadFilesCount,
uploaderName,
}) {
const handleSubmit = async (inputValue: string) => {
onClose();
await onNameSubmit(inputValue);
};
return (
<DialogBox
size="xs"
open={open}
onClose={onClose}
attributes={{
title: constants.ENTER_NAME,
icon: <AutoAwesomeOutlinedIcon />,
}}>
<Typography color={'text.secondary'} pb={1}>
{constants.PUBLIC_UPLOADER_NAME_MESSAGE}
</Typography>
<SingleInputForm
hiddenLabel
initialValue={uploaderName}
callback={handleSubmit}
placeholder={constants.NAME_PLACEHOLDER}
buttonText={constants.ADD_X_PHOTOS(toUploadFilesCount)}
fieldType="text"
blockButton
secondaryButtonAction={onClose}
/>
</DialogBox>
);
}

View file

@ -1,7 +1,6 @@
import React from 'react'; import React from 'react';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
import CryptoWorker from 'utils/crypto';
import SingleInputForm, { import SingleInputForm, {
SingleInputFormProps, SingleInputFormProps,
} from 'components/SingleInputForm'; } from 'components/SingleInputForm';
@ -10,6 +9,7 @@ import { CustomError } from 'utils/error';
import { Input } from '@mui/material'; import { Input } from '@mui/material';
import { KeyAttributes, User } from 'types/user'; import { KeyAttributes, User } from 'types/user';
import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker';
export interface VerifyMasterPasswordFormProps { export interface VerifyMasterPasswordFormProps {
user: User; user: User;
@ -29,7 +29,7 @@ export default function VerifyMasterPasswordForm({
setFieldError setFieldError
) => { ) => {
try { try {
const cryptoWorker = await new CryptoWorker(); const cryptoWorker = await ComlinkCryptoWorker.getInstance();
let kek: string = null; let kek: string = null;
try { try {
kek = await cryptoWorker.deriveKey( kek = await cryptoWorker.deriveKey(
@ -43,7 +43,7 @@ export default function VerifyMasterPasswordForm({
throw Error(CustomError.WEAK_DEVICE); throw Error(CustomError.WEAK_DEVICE);
} }
try { try {
const key: string = await cryptoWorker.decryptB64( const key = await cryptoWorker.decryptB64(
keyAttributes.encryptedKey, keyAttributes.encryptedKey,
keyAttributes.keyDecryptionNonce, keyAttributes.keyDecryptionNonce,
kek kek

View file

@ -0,0 +1,14 @@
import React from 'react';
export default function Ente() {
return (
<svg
width="50"
height="26"
viewBox="0 0 43 13"
fill="#fff"
xmlns="http://www.w3.org/2000/svg">
<path d="M6.102 12.144C4.998 12.144 4.026 11.928 3.186 11.496C2.358 11.064 1.716 10.476 1.26 9.732C0.804 8.976 0.576 8.118 0.576 7.158C0.576 6.186 0.798 5.328 1.242 4.584C1.698 3.828 2.316 3.24 3.096 2.82C3.876 2.388 4.758 2.172 5.742 2.172C6.69 2.172 7.542 2.376 8.298 2.784C9.066 3.18 9.672 3.756 10.116 4.512C10.56 5.256 10.782 6.15 10.782 7.194C10.782 7.302 10.776 7.428 10.764 7.572C10.752 7.704 10.74 7.83 10.728 7.95H2.862V6.312H9.252L8.172 6.798C8.172 6.294 8.07 5.856 7.866 5.484C7.662 5.112 7.38 4.824 7.02 4.62C6.66 4.404 6.24 4.296 5.76 4.296C5.28 4.296 4.854 4.404 4.482 4.62C4.122 4.824 3.84 5.118 3.636 5.502C3.432 5.874 3.33 6.318 3.33 6.834V7.266C3.33 7.794 3.444 8.262 3.672 8.67C3.912 9.066 4.242 9.372 4.662 9.588C5.094 9.792 5.598 9.894 6.174 9.894C6.69 9.894 7.14 9.816 7.524 9.66C7.92 9.504 8.28 9.27 8.604 8.958L10.098 10.578C9.654 11.082 9.096 11.472 8.424 11.748C7.752 12.012 6.978 12.144 6.102 12.144ZM18.5375 2.172C19.3055 2.172 19.9895 2.328 20.5895 2.64C21.2015 2.94 21.6815 3.408 22.0295 4.044C22.3775 4.668 22.5515 5.472 22.5515 6.456V12H19.7435V6.888C19.7435 6.108 19.5695 5.532 19.2215 5.16C18.8855 4.788 18.4055 4.602 17.7815 4.602C17.3375 4.602 16.9355 4.698 16.5755 4.89C16.2275 5.07 15.9515 5.352 15.7475 5.736C15.5555 6.12 15.4595 6.612 15.4595 7.212V12H12.6515V2.316H15.3335V4.998L14.8295 4.188C15.1775 3.54 15.6755 3.042 16.3235 2.694C16.9715 2.346 17.7095 2.172 18.5375 2.172ZM29.0568 12.144C27.9168 12.144 27.0288 11.856 26.3928 11.28C25.7568 10.692 25.4388 9.822 25.4388 8.67V0.174H28.2468V8.634C28.2468 9.042 28.3548 9.36 28.5708 9.588C28.7868 9.804 29.0808 9.912 29.4528 9.912C29.8968 9.912 30.2748 9.792 30.5868 9.552L31.3428 11.532C31.0548 11.736 30.7068 11.892 30.2988 12C29.9028 12.096 29.4888 12.144 29.0568 12.144ZM23.9448 4.692V2.532H30.6588V4.692H23.9448ZM37.4262 12.144C36.3222 12.144 35.3502 11.928 34.5102 11.496C33.6822 11.064 33.0402 10.476 32.5842 9.732C32.1282 8.976 31.9002 8.118 31.9002 7.158C31.9002 6.186 32.1222 5.328 32.5662 4.584C33.0222 3.828 33.6402 3.24 34.4202 2.82C35.2002 2.388 36.0822 2.172 37.0662 2.172C38.0142 2.172 38.8662 2.376 39.6222 2.784C40.3902 3.18 40.9962 3.756 41.4402 4.512C41.8842 5.256 42.1062 6.15 42.1062 7.194C42.1062 7.302 42.1002 7.428 42.0882 7.572C42.0762 7.704 42.0642 7.83 42.0522 7.95H34.1862V6.312H40.5762L39.4962 6.798C39.4962 6.294 39.3942 5.856 39.1902 5.484C38.9862 5.112 38.7042 4.824 38.3442 4.62C37.9842 4.404 37.5642 4.296 37.0842 4.296C36.6042 4.296 36.1782 4.404 35.8062 4.62C35.4462 4.824 35.1642 5.118 34.9602 5.502C34.7562 5.874 34.6542 6.318 34.6542 6.834V7.266C34.6542 7.794 34.7682 8.262 34.9962 8.67C35.2362 9.066 35.5662 9.372 35.9862 9.588C36.4182 9.792 36.9222 9.894 37.4982 9.894C38.0142 9.894 38.4642 9.816 38.8482 9.66C39.2442 9.504 39.6042 9.27 39.9282 8.958L41.4222 10.578C40.9782 11.082 40.4202 11.472 39.7482 11.748C39.0762 12.012 38.3022 12.144 37.4262 12.144Z" />
</svg>
);
}

View file

@ -1,7 +1,6 @@
import React from 'react'; import React from 'react';
import NavbarBase from 'components/Navbar/base'; import NavbarBase from 'components/Navbar/base';
import SidebarToggler from 'components/Navbar/SidebarToggler'; import SidebarToggler from 'components/Navbar/SidebarToggler';
import { getNonTrashedUniqueUserFiles } from 'utils/file';
import SearchBar from 'components/Search/SearchBar'; import SearchBar from 'components/Search/SearchBar';
import { Collection } from 'types/collection'; import { Collection } from 'types/collection';
import { EnteFile } from 'types/file'; import { EnteFile } from 'types/file';
@ -36,7 +35,7 @@ export function GalleryNavbar({
isInSearchMode={isInSearchMode} isInSearchMode={isInSearchMode}
setIsInSearchMode={setIsInSearchMode} setIsInSearchMode={setIsInSearchMode}
collections={collections} collections={collections}
files={getNonTrashedUniqueUserFiles(files)} files={files}
setActiveCollection={setActiveCollection} setActiveCollection={setActiveCollection}
updateSearch={updateSearch} updateSearch={updateSearch}
/> />

View file

@ -2,6 +2,7 @@ import { Stack } from '@mui/material';
import { AppContext } from 'pages/_app'; import { AppContext } from 'pages/_app';
import React, { useContext } from 'react'; import React, { useContext } from 'react';
import { Subscription } from 'types/billing'; import { Subscription } from 'types/billing';
import { SetLoading } from 'types/gallery';
import { import {
activateSubscription, activateSubscription,
cancelSubscription, cancelSubscription,
@ -15,7 +16,7 @@ import ManageSubscriptionButton from './button';
interface Iprops { interface Iprops {
subscription: Subscription; subscription: Subscription;
closeModal: () => void; closeModal: () => void;
setLoading: (value: boolean) => void; setLoading: SetLoading;
} }
export function ManageSubscription({ export function ManageSubscription({

View file

@ -32,7 +32,7 @@ interface IProps {
showPlaceholder: boolean; showPlaceholder: boolean;
} }
const Check = styled('input')<{ active: boolean }>` const Check = styled('input')<{ $active: boolean }>`
appearance: none; appearance: none;
position: absolute; position: absolute;
z-index: 10; z-index: 10;
@ -85,7 +85,7 @@ const Check = styled('input')<{ active: boolean }>`
border-right: 2px solid #ddd; border-right: 2px solid #ddd;
border-bottom: 2px solid #ddd; border-bottom: 2px solid #ddd;
} }
${(props) => props.active && 'opacity: 0.5 '}; ${(props) => props.$active && 'opacity: 0.5 '};
&:checked { &:checked {
opacity: 1 !important; opacity: 1 !important;
} }
@ -104,15 +104,15 @@ export const HoverOverlay = styled('div')<{ checked: boolean }>`
'background:linear-gradient(rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0))'}; 'background:linear-gradient(rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0))'};
`; `;
export const InSelectRangeOverLay = styled('div')<{ active: boolean }>` export const InSelectRangeOverLay = styled('div')<{ $active: boolean }>`
opacity: ${(props) => (!props.active ? 0 : 1)}; opacity: ${(props) => (!props.$active ? 0 : 1)};
left: 0; left: 0;
top: 0; top: 0;
outline: none; outline: none;
height: 100%; height: 100%;
width: 100%; width: 100%;
position: absolute; position: absolute;
${(props) => props.active && 'background:rgba(81, 205, 124, 0.25)'}; ${(props) => props.$active && 'background:rgba(81, 205, 124, 0.25)'};
`; `;
export const FileAndCollectionNameOverlay = styled('div')` export const FileAndCollectionNameOverlay = styled('div')`
@ -304,7 +304,7 @@ export default function PreviewCard(props: IProps) {
type="checkbox" type="checkbox"
checked={selected} checked={selected}
onChange={handleSelect} onChange={handleSelect}
active={isRangeSelectActive && isInsSelectRange} $active={isRangeSelectActive && isInsSelectRange}
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
/> />
)} )}
@ -313,7 +313,7 @@ export default function PreviewCard(props: IProps) {
<SelectedOverlay selected={selected} /> <SelectedOverlay selected={selected} />
<HoverOverlay checked={selected} /> <HoverOverlay checked={selected} />
<InSelectRangeOverLay <InSelectRangeOverLay
active={isRangeSelectActive && isInsSelectRange} $active={isRangeSelectActive && isInsSelectRange}
/> />
{isLivePhoto(file) && <LivePhotoIndicator />} {isLivePhoto(file) && <LivePhotoIndicator />}
{deduplicateContext.isOnDeduplicatePage && ( {deduplicateContext.isOnDeduplicatePage && (

View file

@ -1,7 +1,7 @@
import { ENTE_WEBSITE_LINK } from 'constants/urls'; import { ENTE_WEBSITE_LINK } from 'constants/urls';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Button, styled } from '@mui/material'; import { Button, styled } from '@mui/material';
import GetDeviceOS, { OS } from 'utils/common/deviceDetection'; import { getDeviceOS, OS } from 'utils/common/deviceDetection';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
export const NoStyleAnchor = styled('a')` export const NoStyleAnchor = styled('a')`
@ -16,7 +16,7 @@ function GoToEnte() {
const [os, setOS] = useState<OS>(OS.UNKNOWN); const [os, setOS] = useState<OS>(OS.UNKNOWN);
useEffect(() => { useEffect(() => {
const os = GetDeviceOS(); const os = getDeviceOS();
setOS(os); setOS(os);
}, []); }, []);

View file

@ -1,18 +1,25 @@
import { CenteredFlex, FluidContainer } from 'components/Container'; import { EnteLinkLogo } from 'components/Navbar/EnteLinkLogo';
import { EnteLogo } from 'components/EnteLogo'; import { FluidContainer } from 'components/Container';
import NavbarBase from 'components/Navbar/base'; import NavbarBase from 'components/Navbar/base';
import UploadButton from 'components/Upload/UploadButton';
import React from 'react'; import React from 'react';
import constants from 'utils/strings/constants';
import GoToEnte from './GoToEnte'; import GoToEnte from './GoToEnte';
export default function SharedAlbumNavbar() { export default function SharedAlbumNavbar({ showUploadButton, openUploader }) {
return ( return (
<NavbarBase> <NavbarBase>
<FluidContainer> <FluidContainer>
<CenteredFlex> <EnteLinkLogo />
<EnteLogo />
</CenteredFlex>
</FluidContainer> </FluidContainer>
<GoToEnte /> {showUploadButton ? (
<UploadButton
openUploader={openUploader}
text={constants.ADD_PHOTOS}
/>
) : (
<GoToEnte />
)}
</NavbarBase> </NavbarBase>
); );
} }

View file

@ -15,13 +15,14 @@ export enum CollectionSummaryType {
archive = 'archive', archive = 'archive',
trash = 'trash', trash = 'trash',
all = 'all', all = 'all',
shared = 'shared', outgoingShare = 'outgoingShare',
incomingShare = 'incomingShare',
sharedOnlyViaLink = 'sharedOnlyViaLink',
archived = 'archived', archived = 'archived',
} }
export enum COLLECTION_SORT_BY { export enum COLLECTION_SORT_BY {
NAME, NAME,
CREATION_TIME_ASCENDING, CREATION_TIME_ASCENDING,
CREATION_TIME_DESCENDING,
UPDATION_TIME_DESCENDING, UPDATION_TIME_DESCENDING,
} }
@ -34,7 +35,9 @@ export const COLLECTION_SORT_ORDER = new Map([
[CollectionSummaryType.favorites, 1], [CollectionSummaryType.favorites, 1],
[CollectionSummaryType.album, 2], [CollectionSummaryType.album, 2],
[CollectionSummaryType.folder, 2], [CollectionSummaryType.folder, 2],
[CollectionSummaryType.shared, 2], [CollectionSummaryType.incomingShare, 2],
[CollectionSummaryType.outgoingShare, 2],
[CollectionSummaryType.sharedOnlyViaLink, 2],
[CollectionSummaryType.archived, 2], [CollectionSummaryType.archived, 2],
[CollectionSummaryType.archive, 3], [CollectionSummaryType.archive, 3],
[CollectionSummaryType.trash, 4], [CollectionSummaryType.trash, 4],
@ -49,15 +52,15 @@ export const SYSTEM_COLLECTION_TYPES = new Set([
export const UPLOAD_NOT_ALLOWED_COLLECTION_TYPES = new Set([ export const UPLOAD_NOT_ALLOWED_COLLECTION_TYPES = new Set([
CollectionSummaryType.all, CollectionSummaryType.all,
CollectionSummaryType.archive, CollectionSummaryType.archive,
CollectionSummaryType.shared, CollectionSummaryType.incomingShare,
CollectionSummaryType.outgoingShare,
CollectionSummaryType.sharedOnlyViaLink,
CollectionSummaryType.trash, CollectionSummaryType.trash,
]); ]);
export const OPTIONS_NOT_HAVING_COLLECTION_TYPES = new Set([ export const OPTIONS_NOT_HAVING_COLLECTION_TYPES = new Set([
CollectionSummaryType.all, CollectionSummaryType.all,
CollectionSummaryType.archive, CollectionSummaryType.archive,
CollectionSummaryType.shared,
CollectionSummaryType.favorites,
]); ]);
export const HIDE_FROM_COLLECTION_BAR_TYPES = new Set([ export const HIDE_FROM_COLLECTION_BAR_TYPES = new Set([

View file

@ -2,7 +2,7 @@ export const MIN_EDITED_CREATION_TIME = new Date(1800, 0, 1);
export const MAX_EDITED_CREATION_TIME = new Date(); export const MAX_EDITED_CREATION_TIME = new Date();
export const MAX_EDITED_FILE_NAME_LENGTH = 100; export const MAX_EDITED_FILE_NAME_LENGTH = 100;
export const MAX_CAPTION_SIZE = 280; export const MAX_CAPTION_SIZE = 5000;
export const MAX_TRASH_BATCH_SIZE = 1000; export const MAX_TRASH_BATCH_SIZE = 1000;
export const TYPE_HEIC = 'heic'; export const TYPE_HEIC = 'heic';

View file

@ -15,7 +15,3 @@ export enum PAGES {
SHARED_ALBUMS = '/shared-albums', SHARED_ALBUMS = '/shared-albums',
DEDUPLICATE = '/deduplicate', DEDUPLICATE = '/deduplicate',
} }
export const getAlbumSiteHost = () =>
process.env.NODE_ENV === 'production'
? 'albums.ente.io'
: `${window.location.hostname}:3002`;

View file

@ -0,0 +1,4 @@
export enum THEME_COLOR {
LIGHT = 'light',
DARK = 'dark',
}

View file

@ -12,6 +12,7 @@ export const FORMAT_MISSED_BY_FILE_TYPE_LIB = [
{ fileType: FILE_TYPE.IMAGE, exactType: 'jpg', mimeType: 'image/jpeg' }, { fileType: FILE_TYPE.IMAGE, exactType: 'jpg', mimeType: 'image/jpeg' },
{ fileType: FILE_TYPE.VIDEO, exactType: 'webm', mimeType: 'video/webm' }, { fileType: FILE_TYPE.VIDEO, exactType: 'webm', mimeType: 'video/webm' },
{ fileType: FILE_TYPE.VIDEO, exactType: 'mod', mimeType: 'video/mpeg' }, { fileType: FILE_TYPE.VIDEO, exactType: 'mod', mimeType: 'video/mpeg' },
{ fileType: FILE_TYPE.VIDEO, exactType: 'mp4', mimeType: 'video/mp4' },
]; ];
// this is the chunk size of the un-encrypted file which is read and encrypted before uploading it as a single part. // this is the chunk size of the un-encrypted file which is read and encrypted before uploading it as a single part.
@ -52,6 +53,7 @@ export enum UPLOAD_RESULT {
UPLOADED_WITH_STATIC_THUMBNAIL, UPLOADED_WITH_STATIC_THUMBNAIL,
ADDED_SYMLINK, ADDED_SYMLINK,
CANCELLED, CANCELLED,
SKIPPED_VIDEOS,
} }
export enum PICKED_UPLOAD_TYPE { export enum PICKED_UPLOAD_TYPE {
@ -76,6 +78,7 @@ export const USE_CF_PROXY = false;
export const DEFAULT_IMPORT_SUGGESTION: ImportSuggestion = { export const DEFAULT_IMPORT_SUGGESTION: ImportSuggestion = {
rootFolderName: '', rootFolderName: '',
hasNestedFolders: false, hasNestedFolders: false,
hasRootLevelFileWithFolder: false,
}; };
export const BLACK_THUMBNAIL_BASE64 = export const BLACK_THUMBNAIL_BASE64 =

View file

@ -16,6 +16,7 @@ import LoadingBar from 'react-top-loading-bar';
import DialogBox from 'components/DialogBox'; import DialogBox from 'components/DialogBox';
import { styled, ThemeProvider } from '@mui/material/styles'; import { styled, ThemeProvider } from '@mui/material/styles';
import darkThemeOptions from 'themes/darkThemeOptions'; import darkThemeOptions from 'themes/darkThemeOptions';
import lightThemeOptions from 'themes/lightThemeOptions';
import { CssBaseline, useMediaQuery } from '@mui/material'; import { CssBaseline, useMediaQuery } from '@mui/material';
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
import * as types from 'styled-components/cssprop'; // need to css prop on styled component import * as types from 'styled-components/cssprop'; // need to css prop on styled component
@ -44,6 +45,9 @@ import ArrowForward from '@mui/icons-material/ArrowForward';
import { AppUpdateInfo } from 'types/electron'; import { AppUpdateInfo } from 'types/electron';
import { getSentryUserID } from 'utils/user'; import { getSentryUserID } from 'utils/user';
import { User } from 'types/user'; import { User } from 'types/user';
import { SetTheme } from 'types/theme';
import { useLocalState } from 'hooks/useLocalState';
import { THEME_COLOR } from 'constants/theme';
export const MessageContainer = styled('div')` export const MessageContainer = styled('div')`
background-color: #111; background-color: #111;
@ -77,6 +81,8 @@ type AppContextType = {
watchFolderFiles: FileList; watchFolderFiles: FileList;
setWatchFolderFiles: (files: FileList) => void; setWatchFolderFiles: (files: FileList) => void;
isMobile: boolean; isMobile: boolean;
theme: THEME_COLOR;
setTheme: SetTheme;
}; };
export enum FLASH_MESSAGE_TYPE { export enum FLASH_MESSAGE_TYPE {
@ -119,6 +125,7 @@ export default function App({ Component, err }) {
const closeNotification = () => setNotificationView(false); const closeNotification = () => setNotificationView(false);
const [notificationAttributes, setNotificationAttributes] = const [notificationAttributes, setNotificationAttributes] =
useState<NotificationAttributes>(null); useState<NotificationAttributes>(null);
const [theme, setTheme] = useLocalState(LS_KEYS.THEME, THEME_COLOR.DARK);
useEffect(() => { useEffect(() => {
HTTPService.getInterceptors().response.use( HTTPService.getInterceptors().response.use(
@ -266,7 +273,12 @@ export default function App({ Component, err }) {
/> />
</Head> </Head>
<ThemeProvider theme={darkThemeOptions}> <ThemeProvider
theme={
theme === THEME_COLOR.DARK
? darkThemeOptions
: lightThemeOptions
}>
<CssBaseline enableColorScheme /> <CssBaseline enableColorScheme />
{showNavbar && <AppNavbar />} {showNavbar && <AppNavbar />}
<MessageContainer> <MessageContainer>
@ -325,6 +337,8 @@ export default function App({ Component, err }) {
setWatchFolderFiles, setWatchFolderFiles,
isMobile, isMobile,
setNotificationAttributes, setNotificationAttributes,
theme,
setTheme,
}}> }}>
{loading ? ( {loading ? (
<VerticallyCentered> <VerticallyCentered>

View file

@ -2,10 +2,9 @@ import React, { useState, useEffect } from 'react';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
import { getData, LS_KEYS, setData } from 'utils/storage/localStorage'; import { getData, LS_KEYS, setData } from 'utils/storage/localStorage';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import CryptoWorker, { import {
saveKeyInSessionStore, saveKeyInSessionStore,
generateAndSaveIntermediateKeyAttributes, generateAndSaveIntermediateKeyAttributes,
B64EncryptionResult,
} from 'utils/crypto'; } from 'utils/crypto';
import { getActualKey } from 'utils/common/key'; import { getActualKey } from 'utils/common/key';
import { setKeys } from 'services/userService'; import { setKeys } from 'services/userService';
@ -14,12 +13,13 @@ import SetPasswordForm, {
} from 'components/SetPasswordForm'; } from 'components/SetPasswordForm';
import { SESSION_KEYS } from 'utils/storage/sessionStorage'; import { SESSION_KEYS } from 'utils/storage/sessionStorage';
import { PAGES } from 'constants/pages'; import { PAGES } from 'constants/pages';
import { KEK, UpdatedKey, User } from 'types/user'; import { KEK, KeyAttributes, UpdatedKey, User } from 'types/user';
import LinkButton from 'components/pages/gallery/LinkButton'; import LinkButton from 'components/pages/gallery/LinkButton';
import VerticallyCentered from 'components/Container'; import VerticallyCentered from 'components/Container';
import FormPaper from 'components/Form/FormPaper'; import FormPaper from 'components/Form/FormPaper';
import FormPaperFooter from 'components/Form/FormPaper/Footer'; import FormPaperFooter from 'components/Form/FormPaper/Footer';
import FormPaperTitle from 'components/Form/FormPaper/Title'; import FormPaperTitle from 'components/Form/FormPaper/Title';
import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker';
export default function ChangePassword() { export default function ChangePassword() {
const [token, setToken] = useState<string>(); const [token, setToken] = useState<string>();
@ -40,10 +40,10 @@ export default function ChangePassword() {
passphrase, passphrase,
setFieldError setFieldError
) => { ) => {
const cryptoWorker = await new CryptoWorker(); const cryptoWorker = await ComlinkCryptoWorker.getInstance();
const key: string = await getActualKey(); const key = await getActualKey();
const keyAttributes = getData(LS_KEYS.KEY_ATTRIBUTES); const keyAttributes: KeyAttributes = getData(LS_KEYS.KEY_ATTRIBUTES);
const kekSalt: string = await cryptoWorker.generateSaltToDeriveKey(); const kekSalt = await cryptoWorker.generateSaltToDeriveKey();
let kek: KEK; let kek: KEK;
try { try {
kek = await cryptoWorker.deriveSensitiveKey(passphrase, kekSalt); kek = await cryptoWorker.deriveSensitiveKey(passphrase, kekSalt);
@ -51,8 +51,10 @@ export default function ChangePassword() {
setFieldError('confirm', constants.PASSWORD_GENERATION_FAILED); setFieldError('confirm', constants.PASSWORD_GENERATION_FAILED);
return; return;
} }
const encryptedKeyAttributes: B64EncryptionResult = const encryptedKeyAttributes = await cryptoWorker.encryptToB64(
await cryptoWorker.encryptToB64(key, kek.key); key,
kek.key
);
const updatedKey: UpdatedKey = { const updatedKey: UpdatedKey = {
kekSalt, kekSalt,
encryptedKey: encryptedKeyAttributes.encryptedData, encryptedKey: encryptedKeyAttributes.encryptedData,

View file

@ -34,7 +34,7 @@ import {
setIsFirstLogin, setIsFirstLogin,
setJustSignedUp, setJustSignedUp,
} from 'utils/storage'; } from 'utils/storage';
import { isTokenValid, logoutUser } from 'services/userService'; import { isTokenValid, logoutUser, validateKey } from 'services/userService';
import { useDropzone } from 'react-dropzone'; import { useDropzone } from 'react-dropzone';
import EnteSpinner from 'components/EnteSpinner'; import EnteSpinner from 'components/EnteSpinner';
import { LoadingOverlay } from 'components/LoadingOverlay'; import { LoadingOverlay } from 'components/LoadingOverlay';
@ -42,7 +42,7 @@ import PhotoFrame from 'components/PhotoFrame';
import { import {
changeFilesVisibility, changeFilesVisibility,
downloadFiles, downloadFiles,
getNonTrashedUniqueUserFiles, getNonTrashedFiles,
getSelectedFiles, getSelectedFiles,
mergeMetadata, mergeMetadata,
sortFiles, sortFiles,
@ -87,7 +87,7 @@ import FixCreationTime, {
} from 'components/FixCreationTime'; } from 'components/FixCreationTime';
import { Collection, CollectionSummaries } from 'types/collection'; import { Collection, CollectionSummaries } from 'types/collection';
import { EnteFile } from 'types/file'; import { EnteFile } from 'types/file';
import { GalleryContextType, SelectedState } from 'types/gallery'; import { GalleryContextType, SelectedState, SetFiles } from 'types/gallery';
import { VISIBILITY_STATE } from 'types/magicMetadata'; import { VISIBILITY_STATE } from 'types/magicMetadata';
import Collections from 'components/Collections'; import Collections from 'components/Collections';
import { GalleryNavbar } from 'components/pages/gallery/Navbar'; import { GalleryNavbar } from 'components/pages/gallery/Navbar';
@ -124,12 +124,45 @@ export const GalleryContext = createContext<GalleryContextType>(
defaultGalleryContext defaultGalleryContext
); );
type FilesFn = EnteFile[] | ((files: EnteFile[]) => EnteFile[]);
export default function Gallery() { export default function Gallery() {
const router = useRouter(); const router = useRouter();
const [user, setUser] = useState(null); const [user, setUser] = useState(null);
const [collections, setCollections] = useState<Collection[]>(null); const [collections, setCollections] = useState<Collection[]>(null);
const [files, setFilesOriginal] = useState<EnteFile[]>(null);
const filesUpdateInProgress = useRef(false);
const newerFilesFN = useRef<FilesFn>(null);
const setFilesOriginalWithReSyncIfRequired: SetFiles = (filesFn) => {
setFilesOriginal(filesFn);
filesUpdateInProgress.current = false;
if (newerFilesFN.current) {
const newerFiles = newerFilesFN.current;
setTimeout(() => setFiles(newerFiles), 0);
newerFilesFN.current = null;
}
};
const setFiles: SetFiles = async (filesFn) => {
if (filesUpdateInProgress.current) {
newerFilesFN.current = filesFn;
return;
}
filesUpdateInProgress.current = true;
if (!files?.length || files.length < 5000) {
setFilesOriginalWithReSyncIfRequired(filesFn);
} else {
const waitTime = getData(LS_KEYS.WAIT_TIME) ?? 5000;
setTimeout(
() => setFilesOriginalWithReSyncIfRequired(filesFn),
waitTime
);
}
};
const [files, setFiles] = useState<EnteFile[]>(null);
const [favItemIds, setFavItemIds] = useState<Set<number>>(); const [favItemIds, setFavItemIds] = useState<Set<number>>();
const [isFirstLoad, setIsFirstLoad] = useState(false); const [isFirstLoad, setIsFirstLoad] = useState(false);
@ -225,7 +258,12 @@ export default function Gallery() {
router.push(PAGES.ROOT); router.push(PAGES.ROOT);
return; return;
} }
preloadImage('/images/subscription-card-background');
const main = async () => { const main = async () => {
const valid = await validateKey();
if (!valid) {
return;
}
setActiveCollection(ALL_SECTION); setActiveCollection(ALL_SECTION);
setIsFirstLoad(isFirstLogin()); setIsFirstLoad(isFirstLogin());
setIsFirstFetch(true); setIsFirstFetch(true);
@ -245,7 +283,6 @@ export default function Gallery() {
setIsFirstLoad(false); setIsFirstLoad(false);
setJustSignedUp(false); setJustSignedUp(false);
setIsFirstFetch(false); setIsFirstFetch(false);
preloadImage('/images/subscription-card-background');
}; };
main(); main();
}, []); }, []);
@ -307,7 +344,7 @@ export default function Gallery() {
searchResultSummary={searchResultSummary} searchResultSummary={searchResultSummary}
/> />
), ),
itemType: ITEM_TYPE.OTHER, itemType: ITEM_TYPE.HEADER,
}); });
} }
}, [isInSearchMode, searchResultSummary]); }, [isInSearchMode, searchResultSummary]);
@ -329,6 +366,7 @@ export default function Gallery() {
let files = await syncFiles(collections, setFiles); let files = await syncFiles(collections, setFiles);
const trash = await syncTrash(collections, setFiles, files); const trash = await syncTrash(collections, setFiles, files);
files = [...files, ...getTrashedFiles(trash)]; files = [...files, ...getTrashedFiles(trash)];
setFiles(sortFiles(files));
} catch (e) { } catch (e) {
logError(e, 'syncWithRemote failed'); logError(e, 'syncWithRemote failed');
switch (e.message) { switch (e.message) {
@ -511,7 +549,6 @@ export default function Gallery() {
if (newSearch?.collection) { if (newSearch?.collection) {
setActiveCollection(newSearch?.collection); setActiveCollection(newSearch?.collection);
} else { } else {
setActiveCollection(ALL_SECTION);
setSearch(newSearch); setSearch(newSearch);
} }
if (!newSearch?.collection && !newSearch?.file) { if (!newSearch?.collection && !newSearch?.file) {
@ -522,13 +559,6 @@ export default function Gallery() {
} }
}; };
const closeCollectionSelector = (closeBtnClick?: boolean) => {
if (closeBtnClick === true) {
appContext.resetSharedFiles();
}
setCollectionSelectorView(false);
};
const fixTimeHelper = async () => { const fixTimeHelper = async () => {
const selectedFiles = getSelectedFiles(selected, files); const selectedFiles = getSelectedFiles(selected, files);
setFixCreationTimeAttributes({ files: selectedFiles }); setFixCreationTimeAttributes({ files: selectedFiles });
@ -552,6 +582,10 @@ export default function Gallery() {
setUploadTypeSelectorView(true); setUploadTypeSelectorView(true);
}; };
const closeCollectionSelector = () => {
setCollectionSelectorView(false);
};
return ( return (
<GalleryContext.Provider <GalleryContext.Provider
value={{ value={{
@ -611,7 +645,7 @@ export default function Gallery() {
openUploader={openUploader} openUploader={openUploader}
isInSearchMode={isInSearchMode} isInSearchMode={isInSearchMode}
collections={collections} collections={collections}
files={getNonTrashedUniqueUserFiles(files)} files={getNonTrashedFiles(files)}
setActiveCollection={setActiveCollection} setActiveCollection={setActiveCollection}
updateSearch={updateSearch} updateSearch={updateSearch}
/> />

View file

@ -10,12 +10,13 @@ import SignUp from 'components/SignUp';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
import localForage from 'utils/storage/localForage'; import localForage from 'utils/storage/localForage';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
import { getAlbumSiteHost, PAGES } from 'constants/pages'; import { PAGES } from 'constants/pages';
import { EnteLogo } from 'components/EnteLogo'; import { EnteLogo } from 'components/EnteLogo';
import isElectron from 'is-electron'; import isElectron from 'is-electron';
import safeStorageService from 'services/electron/safeStorage'; import safeStorageService from 'services/electron/safeStorage';
import { saveKeyInSessionStore } from 'utils/crypto'; import { saveKeyInSessionStore } from 'utils/crypto';
import { getKey, SESSION_KEYS } from 'utils/storage/sessionStorage'; import { getKey, SESSION_KEYS } from 'utils/storage/sessionStorage';
import { getAlbumsURL } from 'utils/common/apiUtil';
const Container = styled('div')` const Container = styled('div')`
display: flex; display: flex;
@ -101,10 +102,10 @@ export default function LandingPage() {
useEffect(() => { useEffect(() => {
appContext.showNavBar(false); appContext.showNavBar(false);
const currentURL = new URL(window.location.href); const currentURL = new URL(window.location.href);
const ALBUM_SITE_HOST = getAlbumSiteHost(); const albumsURL = new URL(getAlbumsURL());
currentURL.pathname = router.pathname; currentURL.pathname = router.pathname;
if ( if (
currentURL.host === ALBUM_SITE_HOST && currentURL.host === albumsURL.host &&
currentURL.pathname !== PAGES.SHARED_ALBUMS currentURL.pathname !== PAGES.SHARED_ALBUMS
) { ) {
handleAlbumsRedirect(currentURL); handleAlbumsRedirect(currentURL);

View file

@ -8,10 +8,7 @@ import {
} from 'utils/storage/localStorage'; } from 'utils/storage/localStorage';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { PAGES } from 'constants/pages'; import { PAGES } from 'constants/pages';
import CryptoWorker, { import { decryptAndStoreToken, saveKeyInSessionStore } from 'utils/crypto';
decryptAndStoreToken,
saveKeyInSessionStore,
} from 'utils/crypto';
import SingleInputForm, { import SingleInputForm, {
SingleInputFormProps, SingleInputFormProps,
} from 'components/SingleInputForm'; } from 'components/SingleInputForm';
@ -24,6 +21,7 @@ import FormPaper from 'components/Form/FormPaper';
import FormPaperTitle from 'components/Form/FormPaper/Title'; import FormPaperTitle from 'components/Form/FormPaper/Title';
import FormPaperFooter from 'components/Form/FormPaper/Footer'; import FormPaperFooter from 'components/Form/FormPaper/Footer';
import LinkButton from 'components/pages/gallery/LinkButton'; import LinkButton from 'components/pages/gallery/LinkButton';
import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker';
const bip39 = require('bip39'); const bip39 = require('bip39');
// mobile client library only supports english. // mobile client library only supports english.
bip39.setDefaultWordlist('english'); bip39.setDefaultWordlist('english');
@ -72,8 +70,8 @@ export default function Recover() {
} }
recoveryKey = bip39.mnemonicToEntropy(recoveryKey); recoveryKey = bip39.mnemonicToEntropy(recoveryKey);
} }
const cryptoWorker = await new CryptoWorker(); const cryptoWorker = await ComlinkCryptoWorker.getInstance();
const masterKey: string = await cryptoWorker.decryptB64( const masterKey = await cryptoWorker.decryptB64(
keyAttributes.masterKeyEncryptedWithRecoveryKey, keyAttributes.masterKeyEncryptedWithRecoveryKey,
keyAttributes.masterKeyDecryptionNonce, keyAttributes.masterKeyDecryptionNonce,
await cryptoWorker.fromHex(recoveryKey) await cryptoWorker.fromHex(recoveryKey)

View file

@ -18,15 +18,11 @@ import { EnteFile } from 'types/file';
import { mergeMetadata, sortFiles } from 'utils/file'; import { mergeMetadata, sortFiles } from 'utils/file';
import { AppContext } from 'pages/_app'; import { AppContext } from 'pages/_app';
import { AbuseReportForm } from 'components/pages/sharedAlbum/AbuseReportForm'; import { AbuseReportForm } from 'components/pages/sharedAlbum/AbuseReportForm';
import { import { PublicCollectionGalleryContext } from 'utils/publicCollectionGallery';
defaultPublicCollectionGalleryContext,
PublicCollectionGalleryContext,
} from 'utils/publicCollectionGallery';
import { CustomError, parseSharingErrorCodes } from 'utils/error'; import { CustomError, parseSharingErrorCodes } from 'utils/error';
import VerticallyCentered from 'components/Container'; import VerticallyCentered, { CenteredFlex } from 'components/Container';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
import EnteSpinner from 'components/EnteSpinner'; import EnteSpinner from 'components/EnteSpinner';
import CryptoWorker from 'utils/crypto';
import { PAGES } from 'constants/pages'; import { PAGES } from 'constants/pages';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import SingleInputForm, { import SingleInputForm, {
@ -41,6 +37,17 @@ import FormContainer from 'components/Form/FormContainer';
import FormPaper from 'components/Form/FormPaper'; import FormPaper from 'components/Form/FormPaper';
import FormPaperTitle from 'components/Form/FormPaper/Title'; import FormPaperTitle from 'components/Form/FormPaper/Title';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import Uploader from 'components/Upload/Uploader';
import { LoadingOverlay } from 'components/LoadingOverlay';
import FullScreenDropZone from 'components/FullScreenDropZone';
import useFileInput from 'hooks/useFileInput';
import { useDropzone } from 'react-dropzone';
import UploadSelectorInputs from 'components/UploadSelectorInputs';
import { logoutUser } from 'services/userService';
import UploadButton from 'components/Upload/UploadButton';
import bs58 from 'bs58';
import AddPhotoAlternateOutlined from '@mui/icons-material/AddPhotoAlternateOutlined';
import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker';
const Loader = () => ( const Loader = () => (
<VerticallyCentered> <VerticallyCentered>
@ -49,7 +56,6 @@ const Loader = () => (
</EnteSpinner> </EnteSpinner>
</VerticallyCentered> </VerticallyCentered>
); );
const bs58 = require('bs58');
export default function PublicCollectionGallery() { export default function PublicCollectionGallery() {
const token = useRef<string>(null); const token = useRef<string>(null);
// passwordJWTToken refers to the jwt token which is used for album protected by password. // passwordJWTToken refers to the jwt token which is used for album protected by password.
@ -70,6 +76,58 @@ export default function PublicCollectionGallery() {
const [photoListHeader, setPhotoListHeader] = const [photoListHeader, setPhotoListHeader] =
useState<TimeStampListItem>(null); useState<TimeStampListItem>(null);
const [photoListFooter, setPhotoListFooter] =
useState<TimeStampListItem>(null);
const [uploadTypeSelectorView, setUploadTypeSelectorView] = useState(false);
const [blockingLoad, setBlockingLoad] = useState(false);
const [shouldDisableDropzone, setShouldDisableDropzone] = useState(false);
const {
getRootProps: getDragAndDropRootProps,
getInputProps: getDragAndDropInputProps,
acceptedFiles: dragAndDropFiles,
} = useDropzone({
noClick: true,
noKeyboard: true,
disabled: shouldDisableDropzone,
});
const {
selectedFiles: webFileSelectorFiles,
open: openFileSelector,
getInputProps: getFileSelectorInputProps,
} = useFileInput({
directory: false,
});
const {
selectedFiles: webFolderSelectorFiles,
open: openFolderSelector,
getInputProps: getFolderSelectorInputProps,
} = useFileInput({
directory: true,
});
const openUploader = () => {
setUploadTypeSelectorView(true);
};
const closeUploadTypeSelectorView = () => {
setUploadTypeSelectorView(false);
};
const showPublicLinkExpiredMessage = () =>
appContext.setDialogMessage({
title: constants.LINK_EXPIRED,
content: constants.LINK_EXPIRED_MESSAGE,
nonClosable: true,
proceed: {
text: constants.LOGIN,
action: logoutUser,
variant: 'accent',
},
});
useEffect(() => { useEffect(() => {
const currentURL = new URL(window.location.href); const currentURL = new URL(window.location.href);
if (currentURL.pathname !== PAGES.ROOT) { if (currentURL.pathname !== PAGES.ROOT) {
@ -91,7 +149,8 @@ export default function PublicCollectionGallery() {
} }
const main = async () => { const main = async () => {
try { try {
const worker = await new CryptoWorker(); const cryptoWorker = await ComlinkCryptoWorker.getInstance();
url.current = window.location.href; url.current = window.location.href;
const currentURL = new URL(url.current); const currentURL = new URL(url.current);
const t = currentURL.searchParams.get('t'); const t = currentURL.searchParams.get('t');
@ -101,8 +160,8 @@ export default function PublicCollectionGallery() {
} }
const dck = const dck =
ck.length < 50 ck.length < 50
? await worker.toB64(bs58.decode(ck)) ? await cryptoWorker.toB64(bs58.decode(ck))
: await worker.fromHex(ck); : await cryptoWorker.fromHex(ck);
token.current = t; token.current = t;
collectionKey.current = dck; collectionKey.current = dck;
url.current = window.location.href; url.current = window.location.href;
@ -111,6 +170,9 @@ export default function PublicCollectionGallery() {
); );
if (localCollection) { if (localCollection) {
setPublicCollection(localCollection); setPublicCollection(localCollection);
const isPasswordProtected =
localCollection?.publicURLs?.[0]?.passwordEnabled;
setIsPasswordProtected(isPasswordProtected);
const collectionUID = getPublicCollectionUID(token.current); const collectionUID = getPublicCollectionUID(token.current);
const localFiles = await getLocalPublicFiles(collectionUID); const localFiles = await getLocalPublicFiles(collectionUID);
const localPublicFiles = sortFiles( const localPublicFiles = sortFiles(
@ -140,15 +202,38 @@ export default function PublicCollectionGallery() {
/> />
</CollectionInfoBarWrapper> </CollectionInfoBarWrapper>
), ),
itemType: ITEM_TYPE.OTHER, itemType: ITEM_TYPE.HEADER,
height: 68, height: 68,
}); });
}, [publicCollection, publicFiles]); }, [publicCollection, publicFiles]);
useEffect(() => {
if (publicCollection?.publicURLs?.[0]?.enableCollect) {
setPhotoListFooter({
item: (
<CenteredFlex sx={{ marginTop: '56px' }}>
<UploadButton
disableShrink
openUploader={openUploader}
text={constants.ADD_MORE_PHOTOS}
color="accent"
icon={<AddPhotoAlternateOutlined />}
/>
</CenteredFlex>
),
itemType: ITEM_TYPE.FOOTER,
height: 104,
});
} else {
setPhotoListFooter(null);
}
}, [publicCollection]);
const syncWithRemote = async () => { const syncWithRemote = async () => {
const collectionUID = getPublicCollectionUID(token.current); const collectionUID = getPublicCollectionUID(token.current);
try { try {
appContext.startLoading(); appContext.startLoading();
setLoading(true);
const collection = await getPublicCollection( const collection = await getPublicCollection(
token.current, token.current,
collectionKey.current collectionKey.current
@ -196,7 +281,7 @@ export default function PublicCollectionGallery() {
setErrorMessage( setErrorMessage(
parsedError.message === CustomError.TOO_MANY_REQUESTS parsedError.message === CustomError.TOO_MANY_REQUESTS
? constants.LINK_TOO_MANY_REQUESTS ? constants.LINK_TOO_MANY_REQUESTS
: constants.LINK_EXPIRED : constants.LINK_EXPIRED_MESSAGE
); );
// share has been disabled // share has been disabled
// local cache should be cleared // local cache should be cleared
@ -211,6 +296,7 @@ export default function PublicCollectionGallery() {
} }
} finally { } finally {
appContext.finishLoading(); appContext.finishLoading();
setLoading(false);
} }
}; };
@ -219,7 +305,7 @@ export default function PublicCollectionGallery() {
setFieldError setFieldError
) => { ) => {
try { try {
const cryptoWorker = await new CryptoWorker(); const cryptoWorker = await ComlinkCryptoWorker.getInstance();
let hashedPassword: string = null; let hashedPassword: string = null;
try { try {
const publicUrl = publicCollection.publicURLs[0]; const publicUrl = publicCollection.publicURLs[0];
@ -297,31 +383,66 @@ export default function PublicCollectionGallery() {
return ( return (
<PublicCollectionGalleryContext.Provider <PublicCollectionGalleryContext.Provider
value={{ value={{
...defaultPublicCollectionGalleryContext,
token: token.current, token: token.current,
passwordToken: passwordJWTToken.current, passwordToken: passwordJWTToken.current,
accessedThroughSharedURL: true, accessedThroughSharedURL: true,
openReportForm, openReportForm,
photoListHeader, photoListHeader,
photoListFooter,
}}> }}>
<SharedAlbumNavbar /> <FullScreenDropZone
<PhotoFrame getDragAndDropRootProps={getDragAndDropRootProps}>
files={publicFiles} <UploadSelectorInputs
syncWithRemote={syncWithRemote} getDragAndDropInputProps={getDragAndDropInputProps}
setSelected={() => null} getFileSelectorInputProps={getFileSelectorInputProps}
selected={{ count: 0, collectionID: null }} getFolderSelectorInputProps={getFolderSelectorInputProps}
isFirstLoad={true} />
activeCollection={ALL_SECTION} <SharedAlbumNavbar
isSharedCollection showUploadButton={
enableDownload={ publicCollection?.publicURLs?.[0]?.enableCollect
publicCollection?.publicURLs?.[0]?.enableDownload ?? true }
} openUploader={openUploader}
/> />
<AbuseReportForm <PhotoFrame
show={abuseReportFormView} files={publicFiles}
close={closeReportForm} syncWithRemote={syncWithRemote}
url={url.current} setSelected={() => null}
/> selected={{ count: 0, collectionID: null }}
isFirstLoad={true}
activeCollection={ALL_SECTION}
isSharedCollection
enableDownload={
publicCollection?.publicURLs?.[0]?.enableDownload ??
true
}
/>
<AbuseReportForm
show={abuseReportFormView}
close={closeReportForm}
url={url.current}
/>
{blockingLoad && (
<LoadingOverlay>
<EnteSpinner />
</LoadingOverlay>
)}
<Uploader
syncWithRemote={syncWithRemote}
uploadCollection={publicCollection}
setLoading={setBlockingLoad}
setShouldDisableDropzone={setShouldDisableDropzone}
setFiles={setPublicFiles}
webFileSelectorFiles={webFileSelectorFiles}
webFolderSelectorFiles={webFolderSelectorFiles}
dragAndDropFiles={dragAndDropFiles}
uploadTypeSelectorView={uploadTypeSelectorView}
closeUploadTypeSelector={closeUploadTypeSelectorView}
showUploadFilesDialog={openFileSelector}
showUploadDirsDialog={openFolderSelector}
showSessionExpiredMessage={showPublicLinkExpiredMessage}
zipUploadDisabled
/>
</FullScreenDropZone>
</PublicCollectionGalleryContext.Provider> </PublicCollectionGalleryContext.Provider>
); );
} }

View file

@ -2,7 +2,6 @@ import React, { useContext, useEffect, useState } from 'react';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
import { getData, LS_KEYS, setData } from 'utils/storage/localStorage'; import { getData, LS_KEYS, setData } from 'utils/storage/localStorage';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import CryptoWorker, { B64EncryptionResult } from 'utils/crypto';
import SingleInputForm, { import SingleInputForm, {
SingleInputFormProps, SingleInputFormProps,
} from 'components/SingleInputForm'; } from 'components/SingleInputForm';
@ -15,6 +14,8 @@ import FormPaper from 'components/Form/FormPaper';
import FormPaperTitle from 'components/Form/FormPaper/Title'; import FormPaperTitle from 'components/Form/FormPaper/Title';
import FormPaperFooter from 'components/Form/FormPaper/Footer'; import FormPaperFooter from 'components/Form/FormPaper/Footer';
import LinkButton from 'components/pages/gallery/LinkButton'; import LinkButton from 'components/pages/gallery/LinkButton';
import { B64EncryptionResult } from 'types/crypto';
import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker';
const bip39 = require('bip39'); const bip39 = require('bip39');
// mobile client library only supports english. // mobile client library only supports english.
bip39.setDefaultWordlist('english'); bip39.setDefaultWordlist('english');
@ -65,8 +66,8 @@ export default function Recover() {
} }
recoveryKey = bip39.mnemonicToEntropy(recoveryKey); recoveryKey = bip39.mnemonicToEntropy(recoveryKey);
} }
const cryptoWorker = await new CryptoWorker(); const cryptoWorker = await ComlinkCryptoWorker.getInstance();
const twoFactorSecret: string = await cryptoWorker.decryptB64( const twoFactorSecret = await cryptoWorker.decryptB64(
encryptedTwoFactorSecret.encryptedData, encryptedTwoFactorSecret.encryptedData,
encryptedTwoFactorSecret.nonce, encryptedTwoFactorSecret.nonce,
await cryptoWorker.fromHex(recoveryKey) await cryptoWorker.fromHex(recoveryKey)

View file

@ -3,9 +3,7 @@ import { getData, LS_KEYS } from 'utils/storage/localStorage';
import localForage from 'utils/storage/localForage'; import localForage from 'utils/storage/localForage';
import { getActualKey, getToken } from 'utils/common/key'; import { getActualKey, getToken } from 'utils/common/key';
import CryptoWorker from 'utils/crypto';
import { getPublicKey } from './userService'; import { getPublicKey } from './userService';
import { B64EncryptionResult } from 'utils/crypto';
import HTTPService from './HTTPService'; import HTTPService from './HTTPService';
import { EnteFile } from 'types/file'; import { EnteFile } from 'types/file';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
@ -28,6 +26,9 @@ import {
CollectionSummaries, CollectionSummaries,
CollectionSummary, CollectionSummary,
CollectionFilesCount, CollectionFilesCount,
EncryptedCollection,
CollectionMagicMetadata,
CollectionMagicMetadataProps,
} from 'types/collection'; } from 'types/collection';
import { import {
COLLECTION_SORT_BY, COLLECTION_SORT_BY,
@ -38,61 +39,77 @@ import {
ALL_SECTION, ALL_SECTION,
CollectionSummaryType, CollectionSummaryType,
} from 'constants/collection'; } from 'constants/collection';
import { UpdateMagicMetadataRequest } from 'types/magicMetadata'; import {
import { EncryptionResult } from 'types/upload'; NEW_COLLECTION_MAGIC_METADATA,
SUB_TYPE,
UpdateMagicMetadataRequest,
} from 'types/magicMetadata';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
import { IsArchived } from 'utils/magicMetadata'; import { IsArchived, updateMagicMetadataProps } from 'utils/magicMetadata';
import { User } from 'types/user'; import { User } from 'types/user';
import { getNonHiddenCollections } from 'utils/collection'; import {
getNonHiddenCollections,
isQuickLinkCollection,
isSharedByMe,
isSharedWithMe,
isSharedOnlyViaLink,
} from 'utils/collection';
import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker';
const ENDPOINT = getEndpoint(); const ENDPOINT = getEndpoint();
const COLLECTION_TABLE = 'collections'; const COLLECTION_TABLE = 'collections';
const COLLECTION_UPDATION_TIME = 'collection-updation-time'; const COLLECTION_UPDATION_TIME = 'collection-updation-time';
const getCollectionWithSecrets = async ( const getCollectionWithSecrets = async (
collection: Collection, collection: EncryptedCollection,
masterKey: string masterKey: string
) => { ): Promise<Collection> => {
const worker = await new CryptoWorker(); const cryptoWorker = await ComlinkCryptoWorker.getInstance();
const userID = getData(LS_KEYS.USER).id; const userID = getData(LS_KEYS.USER).id;
let decryptedKey: string; let collectionKey: string;
if (collection.owner.id === userID) { if (collection.owner.id === userID) {
decryptedKey = await worker.decryptB64( collectionKey = await cryptoWorker.decryptB64(
collection.encryptedKey, collection.encryptedKey,
collection.keyDecryptionNonce, collection.keyDecryptionNonce,
masterKey masterKey
); );
} else { } else {
const keyAttributes = getData(LS_KEYS.KEY_ATTRIBUTES); const keyAttributes = getData(LS_KEYS.KEY_ATTRIBUTES);
const secretKey = await worker.decryptB64( const secretKey = await cryptoWorker.decryptB64(
keyAttributes.encryptedSecretKey, keyAttributes.encryptedSecretKey,
keyAttributes.secretKeyDecryptionNonce, keyAttributes.secretKeyDecryptionNonce,
masterKey masterKey
); );
decryptedKey = await worker.boxSealOpen( collectionKey = await cryptoWorker.boxSealOpen(
collection.encryptedKey, collection.encryptedKey,
keyAttributes.publicKey, keyAttributes.publicKey,
secretKey secretKey
); );
} }
collection.name = const collectionName =
collection.name || collection.name ||
(await worker.decryptToUTF8( (await cryptoWorker.decryptToUTF8(
collection.encryptedName, collection.encryptedName,
collection.nameDecryptionNonce, collection.nameDecryptionNonce,
decryptedKey collectionKey
)); ));
let collectionMagicMetadata: CollectionMagicMetadata;
if (collection.magicMetadata?.data) { if (collection.magicMetadata?.data) {
collection.magicMetadata.data = await worker.decryptMetadata( collectionMagicMetadata = {
collection.magicMetadata.data, ...collection.magicMetadata,
collection.magicMetadata.header, data: await cryptoWorker.decryptMetadata(
decryptedKey collection.magicMetadata.data,
); collection.magicMetadata.header,
collectionKey
),
};
} }
return { return {
...collection, ...collection,
key: decryptedKey, name: collectionName,
key: collectionKey,
magicMetadata: collectionMagicMetadata,
}; };
}; };
@ -109,27 +126,25 @@ const getCollections = async (
}, },
{ 'X-Auth-Token': token } { 'X-Auth-Token': token }
); );
const promises: Promise<Collection>[] = resp.data.collections.map( const decryptedCollections: Collection[] = await Promise.all(
async (collection: Collection) => { resp.data.collections.map(
if (collection.isDeleted) { async (collection: EncryptedCollection) => {
return collection; if (collection.isDeleted) {
return collection;
}
try {
return await getCollectionWithSecrets(collection, key);
} catch (e) {
logError(e, `decryption failed for collection`, {
collectionID: collection.id,
});
return collection;
}
} }
let collectionWithSecrets = collection; )
try {
collectionWithSecrets = await getCollectionWithSecrets(
collection,
key
);
} catch (e) {
logError(e, `decryption failed for collection`, {
collectionID: collection.id,
});
}
return collectionWithSecrets;
}
); );
// only allow deleted or collection with key, filtering out collection whose decryption failed // only allow deleted or collection with key, filtering out collection whose decryption failed
const collections = (await Promise.all(promises)).filter( const collections = decryptedCollections.filter(
(collection) => collection.isDeleted || collection.key (collection) => collection.isDeleted || collection.key
); );
return collections; return collections;
@ -274,25 +289,15 @@ export const createCollection = async (
return collection; return collection;
} }
} }
const worker = await new CryptoWorker(); const cryptoWorker = await ComlinkCryptoWorker.getInstance();
const encryptionKey = await getActualKey(); const encryptionKey = await getActualKey();
const token = getToken(); const token = getToken();
const collectionKey: string = await worker.generateEncryptionKey(); const collectionKey = await cryptoWorker.generateEncryptionKey();
const { const { encryptedData: encryptedKey, nonce: keyDecryptionNonce } =
encryptedData: encryptedKey, await cryptoWorker.encryptToB64(collectionKey, encryptionKey);
nonce: keyDecryptionNonce, const { encryptedData: encryptedName, nonce: nameDecryptionNonce } =
}: B64EncryptionResult = await worker.encryptToB64( await cryptoWorker.encryptUTF8(collectionName, collectionKey);
collectionKey, const newCollection: EncryptedCollection = {
encryptionKey
);
const {
encryptedData: encryptedName,
nonce: nameDecryptionNonce,
}: B64EncryptionResult = await worker.encryptUTF8(
collectionName,
collectionKey
);
const newCollection: Collection = {
id: null, id: null,
owner: null, owner: null,
encryptedKey, encryptedKey,
@ -306,15 +311,12 @@ export const createCollection = async (
isDeleted: false, isDeleted: false,
magicMetadata: null, magicMetadata: null,
}; };
let createdCollection: Collection = await postCollection( const createdCollection = await postCollection(newCollection, token);
newCollection, const decryptedCreatedCollection = await getCollectionWithSecrets(
token
);
createdCollection = await getCollectionWithSecrets(
createdCollection, createdCollection,
encryptionKey encryptionKey
); );
return createdCollection; return decryptedCreatedCollection;
} catch (e) { } catch (e) {
logError(e, 'create collection failed'); logError(e, 'create collection failed');
throw e; throw e;
@ -322,9 +324,9 @@ export const createCollection = async (
}; };
const postCollection = async ( const postCollection = async (
collectionData: Collection, collectionData: EncryptedCollection,
token: string token: string
): Promise<Collection> => { ): Promise<EncryptedCollection> => {
try { try {
const response = await HTTPService.post( const response = await HTTPService.post(
`${ENDPOINT}/collections`, `${ENDPOINT}/collections`,
@ -457,19 +459,19 @@ const encryptWithNewCollectionKey = async (
files: EnteFile[] files: EnteFile[]
): Promise<EncryptedFileKey[]> => { ): Promise<EncryptedFileKey[]> => {
const fileKeysEncryptedWithNewCollection: EncryptedFileKey[] = []; const fileKeysEncryptedWithNewCollection: EncryptedFileKey[] = [];
const worker = await new CryptoWorker(); const cryptoWorker = await ComlinkCryptoWorker.getInstance();
for (const file of files) { for (const file of files) {
const newEncryptedKey: B64EncryptionResult = await worker.encryptToB64( const newEncryptedKey = await cryptoWorker.encryptToB64(
file.key, file.key,
newCollection.key newCollection.key
); );
file.encryptedKey = newEncryptedKey.encryptedData; const encryptedKey = newEncryptedKey.encryptedData;
file.keyDecryptionNonce = newEncryptedKey.nonce; const keyDecryptionNonce = newEncryptedKey.nonce;
fileKeysEncryptedWithNewCollection.push({ fileKeysEncryptedWithNewCollection.push({
id: file.id, id: file.id,
encryptedKey: file.encryptedKey, encryptedKey,
keyDecryptionNonce: file.keyDecryptionNonce, keyDecryptionNonce,
}); });
} }
return fileKeysEncryptedWithNewCollection; return fileKeysEncryptedWithNewCollection;
@ -513,26 +515,41 @@ export const deleteCollection = async (collectionID: number) => {
} }
}; };
export const leaveSharedAlbum = async (collectionID: number) => {
try {
const token = getToken();
await HTTPService.post(
`${ENDPOINT}/collections/leave/${collectionID}`,
null,
null,
{ 'X-Auth-Token': token }
);
} catch (e) {
logError(e, constants.LEAVE_SHARED_ALBUM_FAILED);
throw e;
}
};
export const updateCollectionMagicMetadata = async (collection: Collection) => { export const updateCollectionMagicMetadata = async (collection: Collection) => {
const token = getToken(); const token = getToken();
if (!token) { if (!token) {
return; return;
} }
const worker = await new CryptoWorker(); const cryptoWorker = await ComlinkCryptoWorker.getInstance();
const { file: encryptedMagicMetadata }: EncryptionResult = const { file: encryptedMagicMetadata } = await cryptoWorker.encryptMetadata(
await worker.encryptMetadata( collection.magicMetadata.data,
collection.magicMetadata.data, collection.key
collection.key );
);
const reqBody: UpdateMagicMetadataRequest = { const reqBody: UpdateMagicMetadataRequest = {
id: collection.id, id: collection.id,
magicMetadata: { magicMetadata: {
version: collection.magicMetadata.version, version: collection.magicMetadata.version,
count: collection.magicMetadata.count, count: collection.magicMetadata.count,
data: encryptedMagicMetadata.encryptedData as unknown as string, data: encryptedMagicMetadata.encryptedData,
header: encryptedMagicMetadata.decryptionHeader, header: encryptedMagicMetadata.decryptionHeader,
}, },
}; };
@ -559,15 +576,14 @@ export const renameCollection = async (
collection: Collection, collection: Collection,
newCollectionName: string newCollectionName: string
) => { ) => {
if (isQuickLinkCollection(collection)) {
// Convert quick link collction to normal collection on rename
await updateCollectionSubType(collection, SUB_TYPE.DEFAULT);
}
const token = getToken(); const token = getToken();
const worker = await new CryptoWorker(); const cryptoWorker = await ComlinkCryptoWorker.getInstance();
const { const { encryptedData: encryptedName, nonce: nameDecryptionNonce } =
encryptedData: encryptedName, await cryptoWorker.encryptUTF8(newCollectionName, collection.key);
nonce: nameDecryptionNonce,
}: B64EncryptionResult = await worker.encryptUTF8(
newCollectionName,
collection.key
);
const collectionRenameRequest = { const collectionRenameRequest = {
collectionID: collection.id, collectionID: collection.id,
encryptedName, encryptedName,
@ -582,16 +598,34 @@ export const renameCollection = async (
} }
); );
}; };
const updateCollectionSubType = async (
collection: Collection,
subType: SUB_TYPE
) => {
const updatedMagicMetadataProps: CollectionMagicMetadataProps = {
subType: subType,
};
const updatedCollection = {
...collection,
magicMetadata: await updateMagicMetadataProps(
collection.magicMetadata ?? NEW_COLLECTION_MAGIC_METADATA,
collection.key,
updatedMagicMetadataProps
),
} as Collection;
await updateCollectionMagicMetadata(updatedCollection);
};
export const shareCollection = async ( export const shareCollection = async (
collection: Collection, collection: Collection,
withUserEmail: string withUserEmail: string
) => { ) => {
try { try {
const worker = await new CryptoWorker(); const cryptoWorker = await ComlinkCryptoWorker.getInstance();
const token = getToken(); const token = getToken();
const publicKey: string = await getPublicKey(withUserEmail); const publicKey: string = await getPublicKey(withUserEmail);
const encryptedKey: string = await worker.boxSeal( const encryptedKey = await cryptoWorker.boxSeal(
collection.key, collection.key,
publicKey publicKey
); );
@ -736,11 +770,6 @@ export function sortCollectionSummaries(
return collectionSummaries return collectionSummaries
.sort((a, b) => { .sort((a, b) => {
switch (sortBy) { switch (sortBy) {
case COLLECTION_SORT_BY.CREATION_TIME_DESCENDING:
return compareCollectionsLatestFile(
b.latestFile,
a.latestFile
);
case COLLECTION_SORT_BY.CREATION_TIME_ASCENDING: case COLLECTION_SORT_BY.CREATION_TIME_ASCENDING:
return ( return (
-1 * -1 *
@ -798,12 +827,15 @@ export function getCollectionSummaries(
latestFile: collectionLatestFiles.get(collection.id), latestFile: collectionLatestFiles.get(collection.id),
fileCount: collectionFilesCount.get(collection.id), fileCount: collectionFilesCount.get(collection.id),
updationTime: collection.updationTime, updationTime: collection.updationTime,
type: type: isSharedWithMe(collection, user)
collection.owner.id !== user.id ? CollectionSummaryType.incomingShare
? CollectionSummaryType.shared : isSharedByMe(collection)
: IsArchived(collection) ? CollectionSummaryType.outgoingShare
? CollectionSummaryType.archived : isSharedOnlyViaLink(collection)
: CollectionSummaryType[collection.type], ? CollectionSummaryType.sharedOnlyViaLink
: IsArchived(collection)
? CollectionSummaryType.archived
: CollectionSummaryType[collection.type],
}); });
} }
} }

View file

@ -1,6 +1,5 @@
import { getToken } from 'utils/common/key'; import { getToken } from 'utils/common/key';
import { getFileURL, getThumbnailURL } from 'utils/common/apiUtil'; import { getFileURL, getThumbnailURL } from 'utils/common/apiUtil';
import CryptoWorker from 'utils/crypto';
import { import {
generateStreamFromArrayBuffer, generateStreamFromArrayBuffer,
getRenderableFileURL, getRenderableFileURL,
@ -14,6 +13,8 @@ import { FILE_TYPE } from 'constants/file';
import { CustomError } from 'utils/error'; import { CustomError } from 'utils/error';
import { openThumbnailCache } from './cacheService'; import { openThumbnailCache } from './cacheService';
import QueueProcessor, { PROCESSING_STRATEGY } from './queueProcessor'; import QueueProcessor, { PROCESSING_STRATEGY } from './queueProcessor';
import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker';
import { addLogLine } from 'utils/logging';
const MAX_PARALLEL_DOWNLOADS = 10; const MAX_PARALLEL_DOWNLOADS = 10;
@ -31,11 +32,17 @@ class DownloadManager {
public async getThumbnail(file: EnteFile) { public async getThumbnail(file: EnteFile) {
try { try {
addLogLine(`[${file.id}] [DownloadManager] getThumbnail called`);
const token = getToken(); const token = getToken();
if (!token) { if (!token) {
return null; return null;
} }
if (!this.thumbnailObjectURLPromise.get(file.id)) { if (this.thumbnailObjectURLPromise.has(file.id)) {
addLogLine(
`[${file.id}] [DownloadManager] getThumbnail promise cache hit, returning existing promise`
);
}
if (!this.thumbnailObjectURLPromise.has(file.id)) {
const downloadPromise = async () => { const downloadPromise = async () => {
const thumbnailCache = await openThumbnailCache(); const thumbnailCache = await openThumbnailCache();
@ -43,8 +50,14 @@ class DownloadManager {
file.id.toString() file.id.toString()
); );
if (cacheResp) { if (cacheResp) {
addLogLine(
`[${file.id}] [DownloadManager] in memory cache hit, using localCache files`
);
return URL.createObjectURL(await cacheResp.blob()); return URL.createObjectURL(await cacheResp.blob());
} }
addLogLine(
`[${file.id}] [DownloadManager] in memory cache miss, DownloadManager getThumbnail download started`
);
const thumb = const thumb =
await this.thumbnailDownloadRequestsProcessor.queueUpRequest( await this.thumbnailDownloadRequestsProcessor.queueUpRequest(
() => this.downloadThumb(token, file) () => this.downloadThumb(token, file)
@ -65,7 +78,7 @@ class DownloadManager {
return await this.thumbnailObjectURLPromise.get(file.id); return await this.thumbnailObjectURLPromise.get(file.id);
} catch (e) { } catch (e) {
this.thumbnailObjectURLPromise.delete(file.id); this.thumbnailObjectURLPromise.delete(file.id);
logError(e, 'get preview Failed'); logError(e, 'get DownloadManager preview Failed');
throw e; throw e;
} }
} }
@ -80,10 +93,10 @@ class DownloadManager {
if (typeof resp.data === 'undefined') { if (typeof resp.data === 'undefined') {
throw Error(CustomError.REQUEST_FAILED); throw Error(CustomError.REQUEST_FAILED);
} }
const worker = await new CryptoWorker(); const cryptoWorker = await ComlinkCryptoWorker.getInstance();
const decrypted: Uint8Array = await worker.decryptThumbnail( const decrypted = await cryptoWorker.decryptThumbnail(
new Uint8Array(resp.data), new Uint8Array(resp.data),
await worker.fromB64(file.thumbnail.decryptionHeader), await cryptoWorker.fromB64(file.thumbnail.decryptionHeader),
file.key file.key
); );
return decrypted; return decrypted;
@ -93,6 +106,7 @@ class DownloadManager {
const fileKey = forPreview ? `${file.id}_preview` : `${file.id}`; const fileKey = forPreview ? `${file.id}_preview` : `${file.id}`;
try { try {
const getFilePromise = async () => { const getFilePromise = async () => {
addLogLine(`[${file.id}] [DownloadManager] downloading file`);
const fileStream = await this.downloadFile(file); const fileStream = await this.downloadFile(file);
const fileBlob = await new Response(fileStream).blob(); const fileBlob = await new Response(fileStream).blob();
if (forPreview) { if (forPreview) {
@ -105,6 +119,11 @@ class DownloadManager {
return { converted: [fileURL], original: [fileURL] }; return { converted: [fileURL], original: [fileURL] };
} }
}; };
if (this.fileObjectURLPromise.has(fileKey)) {
addLogLine(
`[${file.id}] [DownloadManager] getFile promise cache hit, returning existing promise`
);
}
if (!this.fileObjectURLPromise.get(fileKey)) { if (!this.fileObjectURLPromise.get(fileKey)) {
this.fileObjectURLPromise.set(fileKey, getFilePromise()); this.fileObjectURLPromise.set(fileKey, getFilePromise());
} }
@ -112,7 +131,7 @@ class DownloadManager {
return fileURLs; return fileURLs;
} catch (e) { } catch (e) {
this.fileObjectURLPromise.delete(fileKey); this.fileObjectURLPromise.delete(fileKey);
logError(e, 'Failed to get File'); logError(e, 'download manager Failed to get File');
throw e; throw e;
} }
}; };
@ -122,7 +141,8 @@ class DownloadManager {
} }
async downloadFile(file: EnteFile) { async downloadFile(file: EnteFile) {
const worker = await new CryptoWorker(); const cryptoWorker = await ComlinkCryptoWorker.getInstance();
const token = getToken(); const token = getToken();
if (!token) { if (!token) {
return null; return null;
@ -140,9 +160,9 @@ class DownloadManager {
if (typeof resp.data === 'undefined') { if (typeof resp.data === 'undefined') {
throw Error(CustomError.REQUEST_FAILED); throw Error(CustomError.REQUEST_FAILED);
} }
const decrypted: any = await worker.decryptFile( const decrypted = await cryptoWorker.decryptFile(
new Uint8Array(resp.data), new Uint8Array(resp.data),
await worker.fromB64(file.file.decryptionHeader), await cryptoWorker.fromB64(file.file.decryptionHeader),
file.key file.key
); );
return generateStreamFromArrayBuffer(decrypted); return generateStreamFromArrayBuffer(decrypted);
@ -155,12 +175,15 @@ class DownloadManager {
const reader = resp.body.getReader(); const reader = resp.body.getReader();
const stream = new ReadableStream({ const stream = new ReadableStream({
async start(controller) { async start(controller) {
const decryptionHeader = await worker.fromB64( const decryptionHeader = await cryptoWorker.fromB64(
file.file.decryptionHeader file.file.decryptionHeader
); );
const fileKey = await worker.fromB64(file.key); const fileKey = await cryptoWorker.fromB64(file.key);
const { pullState, decryptionChunkSize } = const { pullState, decryptionChunkSize } =
await worker.initDecryption(decryptionHeader, fileKey); await cryptoWorker.initDecryption(
decryptionHeader,
fileKey
);
let data = new Uint8Array(); let data = new Uint8Array();
// The following function handles each data chunk // The following function handles each data chunk
function push() { function push() {
@ -179,7 +202,7 @@ class DownloadManager {
decryptionChunkSize decryptionChunkSize
); );
const { decryptedData } = const { decryptedData } =
await worker.decryptChunk( await cryptoWorker.decryptChunk(
fileData, fileData,
pullState pullState
); );
@ -192,7 +215,10 @@ class DownloadManager {
} else { } else {
if (data) { if (data) {
const { decryptedData } = const { decryptedData } =
await worker.decryptChunk(data, pullState); await cryptoWorker.decryptChunk(
data,
pullState
);
controller.enqueue(decryptedData); controller.enqueue(decryptedData);
data = null; data = null;
} }

View file

@ -34,6 +34,11 @@ class ElectronService {
return this.electronAPIs.getAppVersion(); return this.electronAPIs.getAppVersion();
} }
} }
logRendererProcessMemoryUsage(message: string) {
if (this.electronAPIs?.logRendererProcessMemoryUsage) {
return this.electronAPIs.logRendererProcessMemoryUsage(message);
}
}
} }
export default new ElectronService(); export default new ElectronService();

View file

@ -1,46 +0,0 @@
import { ElectronAPIs } from 'types/electron';
import { makeHumanReadableStorage } from 'utils/billing';
import { addLogLine } from 'utils/logging';
import { logError } from 'utils/sentry';
class ElectronHEICConverter {
private electronAPIs: ElectronAPIs;
private allElectronAPIExists: boolean;
constructor() {
this.electronAPIs = globalThis['ElectronAPIs'];
this.allElectronAPIExists = !!this.electronAPIs?.convertHEIC;
}
apiExists() {
return this.allElectronAPIExists;
}
async convert(fileBlob: Blob): Promise<Blob> {
try {
if (this.allElectronAPIExists) {
const startTime = Date.now();
const inputFileData = new Uint8Array(
await fileBlob.arrayBuffer()
);
const convertedFileData = await this.electronAPIs.convertHEIC(
inputFileData
);
addLogLine(
`originalFileSize:${makeHumanReadableStorage(
fileBlob?.size
)},convertedFileSize:${makeHumanReadableStorage(
convertedFileData?.length
)}, native heic conversion time: ${
Date.now() - startTime
}ms `
);
return new Blob([convertedFileData]);
}
} catch (e) {
logError(e, 'failed to convert heic natively');
throw e;
}
}
}
export default new ElectronHEICConverter();

View file

@ -0,0 +1,77 @@
import { ElectronAPIs } from 'types/electron';
import { ElectronFile } from 'types/upload';
import { convertBytesToHumanReadable } from 'utils/file/size';
import { addLogLine } from 'utils/logging';
import { logError } from 'utils/sentry';
class ElectronImageProcessorService {
private electronAPIs: ElectronAPIs;
constructor() {
this.electronAPIs = globalThis['ElectronAPIs'];
}
convertAPIExists() {
return !!this.electronAPIs?.convertHEIC;
}
generateImageThumbnailAPIExists() {
return !!this.electronAPIs?.generateImageThumbnail;
}
async convertHEIC(fileBlob: Blob): Promise<Blob> {
try {
if (!this.electronAPIs?.convertHEIC) {
throw new Error('convertHEIC API not available');
}
const startTime = Date.now();
const inputFileData = new Uint8Array(await fileBlob.arrayBuffer());
const convertedFileData = await this.electronAPIs.convertHEIC(
inputFileData
);
addLogLine(
`originalFileSize:${convertBytesToHumanReadable(
fileBlob?.size
)},convertedFileSize:${convertBytesToHumanReadable(
convertedFileData?.length
)}, native heic conversion time: ${Date.now() - startTime}ms `
);
return new Blob([convertedFileData]);
} catch (e) {
logError(e, 'failed to convert heic natively');
throw e;
}
}
async generateImageThumbnail(
inputFile: File | ElectronFile,
maxDimension: number,
maxSize: number
): Promise<Uint8Array> {
try {
if (!this.electronAPIs?.generateImageThumbnail) {
throw new Error('generateImageThumbnail API not available');
}
const startTime = Date.now();
const thumb = await this.electronAPIs.generateImageThumbnail(
inputFile,
maxDimension,
maxSize
);
addLogLine(
`originalFileSize:${convertBytesToHumanReadable(
inputFile?.size
)},thumbFileSize:${convertBytesToHumanReadable(
thumb?.length
)}, native thumbnail generation time: ${
Date.now() - startTime
}ms `
);
return thumb;
} catch (e) {
logError(e, 'failed to generate image thumbnail natively');
throw e;
}
}
}
export default new ElectronImageProcessorService();

View file

@ -244,10 +244,7 @@ class ExportService {
file, file,
RecordType.FAILED RecordType.FAILED
); );
console.log(
`export failed for fileID:${file.id}, reason:`,
e
);
logError( logError(
e, e,
'download and save failed for file during export' 'download and save failed for file during export'
@ -486,16 +483,16 @@ class ExportService {
motionPhoto.videoNameTitle, motionPhoto.videoNameTitle,
file.id file.id
); );
this.saveMediaFile(collectionPath, videoSaveName, videoStream); await this.saveMediaFile(collectionPath, videoSaveName, videoStream);
await this.saveMetadataFile(collectionPath, videoSaveName, file); await this.saveMetadataFile(collectionPath, videoSaveName, file);
} }
private saveMediaFile( private async saveMediaFile(
collectionFolderPath: string, collectionFolderPath: string,
fileSaveName: string, fileSaveName: string,
fileStream: ReadableStream<any> fileStream: ReadableStream<any>
) { ) {
this.electronAPIs.saveStreamToDisk( await this.electronAPIs.saveStreamToDisk(
getFileSavePath(collectionFolderPath, fileSaveName), getFileSavePath(collectionFolderPath, fileSaveName),
fileStream fileStream
); );
@ -624,7 +621,6 @@ class ExportService {
oldFileSavePath, oldFileSavePath,
newFileSavePath newFileSavePath
); );
console.log(oldFileMetadataSavePath, newFileMetadataSavePath);
await this.electronAPIs.checkExistsAndRename( await this.electronAPIs.checkExistsAndRename(
oldFileMetadataSavePath, oldFileMetadataSavePath,
newFileMetadataSavePath newFileMetadataSavePath

View file

@ -1,7 +1,7 @@
import isElectron from 'is-electron'; import isElectron from 'is-electron';
import { ElectronFFmpeg } from 'services/electron/ffmpeg'; import { ElectronFFmpeg } from 'services/electron/ffmpeg';
import { ElectronFile } from 'types/upload'; import { ElectronFile } from 'types/upload';
import { FFmpegWorker } from 'utils/comlink'; import ComlinkFFmpegWorker from 'utils/comlink/ComlinkFFmpegWorker';
export interface IFFmpeg { export interface IFFmpeg {
run: ( run: (
@ -13,16 +13,16 @@ export interface IFFmpeg {
class FFmpegFactory { class FFmpegFactory {
private client: IFFmpeg; private client: IFFmpeg;
async getFFmpegClient() { async getFFmpegClient() {
if (!this.client) { if (!this.client) {
if (isElectron()) { if (isElectron()) {
this.client = new ElectronFFmpeg(); this.client = new ElectronFFmpeg();
} else { } else {
this.client = await new FFmpegWorker(); this.client = await ComlinkFFmpegWorker.getInstance();
} }
} }
return this.client; return this.client;
} }
} }
export default new FFmpegFactory(); export default new FFmpegFactory();

View file

@ -2,7 +2,6 @@ import { getEndpoint } from 'utils/common/apiUtil';
import localForage from 'utils/storage/localForage'; import localForage from 'utils/storage/localForage';
import { getToken } from 'utils/common/key'; import { getToken } from 'utils/common/key';
import { EncryptionResult } from 'types/upload';
import { Collection } from 'types/collection'; import { Collection } from 'types/collection';
import HTTPService from './HTTPService'; import HTTPService from './HTTPService';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
@ -12,14 +11,14 @@ import {
preservePhotoswipeProps, preservePhotoswipeProps,
sortFiles, sortFiles,
} from 'utils/file'; } from 'utils/file';
import CryptoWorker from 'utils/crypto'; import { EnteFile, EncryptedEnteFile, TrashRequest } from 'types/file';
import { EnteFile, TrashRequest } from 'types/file';
import { SetFiles } from 'types/gallery'; import { SetFiles } from 'types/gallery';
import { MAX_TRASH_BATCH_SIZE } from 'constants/file'; import { MAX_TRASH_BATCH_SIZE } from 'constants/file';
import { BulkUpdateMagicMetadataRequest } from 'types/magicMetadata'; import { BulkUpdateMagicMetadataRequest } from 'types/magicMetadata';
import { addLogLine } from 'utils/logging'; import { addLogLine } from 'utils/logging';
import { isCollectionHidden } from 'utils/collection'; import { isCollectionHidden } from 'utils/collection';
import { CustomError } from 'utils/error'; import { CustomError } from 'utils/error';
import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker';
const ENDPOINT = getEndpoint(); const ENDPOINT = getEndpoint();
const FILES_TABLE = 'files'; const FILES_TABLE = 'files';
@ -132,11 +131,12 @@ export const getFiles = async (
decryptedFiles = [ decryptedFiles = [
...decryptedFiles, ...decryptedFiles,
...(await Promise.all( ...(await Promise.all(
resp.data.diff.map(async (file: EnteFile) => { resp.data.diff.map(async (file: EncryptedEnteFile) => {
if (!file.isDeleted) { if (!file.isDeleted) {
file = await decryptFile(file, collection.key); return await decryptFile(file, collection.key);
} else {
return file;
} }
return file;
}) as Promise<EnteFile>[] }) as Promise<EnteFile>[]
)), )),
]; ];
@ -250,16 +250,19 @@ export const updateFileMagicMetadata = async (files: EnteFile[]) => {
return; return;
} }
const reqBody: BulkUpdateMagicMetadataRequest = { metadataList: [] }; const reqBody: BulkUpdateMagicMetadataRequest = { metadataList: [] };
const worker = await new CryptoWorker(); const cryptoWorker = await ComlinkCryptoWorker.getInstance();
for (const file of files) { for (const file of files) {
const { file: encryptedMagicMetadata }: EncryptionResult = const { file: encryptedMagicMetadata } =
await worker.encryptMetadata(file.magicMetadata.data, file.key); await cryptoWorker.encryptMetadata(
file.magicMetadata.data,
file.key
);
reqBody.metadataList.push({ reqBody.metadataList.push({
id: file.id, id: file.id,
magicMetadata: { magicMetadata: {
version: file.magicMetadata.version, version: file.magicMetadata.version,
count: file.magicMetadata.count, count: file.magicMetadata.count,
data: encryptedMagicMetadata.encryptedData as unknown as string, data: encryptedMagicMetadata.encryptedData,
header: encryptedMagicMetadata.decryptionHeader, header: encryptedMagicMetadata.decryptionHeader,
}, },
}); });
@ -284,16 +287,19 @@ export const updateFilePublicMagicMetadata = async (files: EnteFile[]) => {
return; return;
} }
const reqBody: BulkUpdateMagicMetadataRequest = { metadataList: [] }; const reqBody: BulkUpdateMagicMetadataRequest = { metadataList: [] };
const worker = await new CryptoWorker(); const cryptoWorker = await ComlinkCryptoWorker.getInstance();
for (const file of files) { for (const file of files) {
const { file: encryptedPubMagicMetadata }: EncryptionResult = const { file: encryptedPubMagicMetadata } =
await worker.encryptMetadata(file.pubMagicMetadata.data, file.key); await cryptoWorker.encryptMetadata(
file.pubMagicMetadata.data,
file.key
);
reqBody.metadataList.push({ reqBody.metadataList.push({
id: file.id, id: file.id,
magicMetadata: { magicMetadata: {
version: file.pubMagicMetadata.version, version: file.pubMagicMetadata.version,
count: file.pubMagicMetadata.count, count: file.pubMagicMetadata.count,
data: encryptedPubMagicMetadata.encryptedData as unknown as string, data: encryptedPubMagicMetadata.encryptedData,
header: encryptedPubMagicMetadata.decryptionHeader, header: encryptedPubMagicMetadata.decryptionHeader,
}, },
}); });

View file

@ -1,14 +1,15 @@
import isElectron from 'is-electron';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
import WasmHEICConverterService from './wasmHeicConverter/wasmHEICConverterService'; import WasmHEICConverterService from './wasmHeicConverter/wasmHEICConverterService';
import ElectronHEICConvertor from 'services/electron/heicConvertor'; import ElectronImageProcessorService from 'services/electron/imageProcessor';
class HeicConversionService { class HeicConversionService {
async convert(heicFileData: Blob): Promise<Blob> { async convert(heicFileData: Blob): Promise<Blob> {
try { try {
if (isElectron() && ElectronHEICConvertor.apiExists()) { if (ElectronImageProcessorService.convertAPIExists()) {
try { try {
return await ElectronHEICConvertor.convert(heicFileData); return await ElectronImageProcessorService.convertHEIC(
heicFileData
);
} catch (e) { } catch (e) {
return await WasmHEICConverterService.convert(heicFileData); return await WasmHEICConverterService.convert(heicFileData);
} }

View file

@ -5,14 +5,16 @@ import { getToken } from 'utils/common/key';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
import { getEndpoint } from 'utils/common/apiUtil'; import { getEndpoint } from 'utils/common/apiUtil';
import HTTPService from 'services/HTTPService'; import HTTPService from 'services/HTTPService';
import CryptoWorker from 'utils/crypto';
import uploadHttpClient from 'services/upload/uploadHttpClient'; import uploadHttpClient from 'services/upload/uploadHttpClient';
import { SetProgressTracker } from 'components/FixLargeThumbnail'; import { SetProgressTracker } from 'components/FixLargeThumbnail';
import { getFileType } from 'services/typeDetectionService'; import { getFileType } from 'services/typeDetectionService';
import { getLocalTrash, getTrashedFiles } from './trashService'; import { getLocalTrash, getTrashedFiles } from './trashService';
import { EncryptionResult, UploadURL } from 'types/upload'; import { UploadURL } from 'types/upload';
import { fileAttribute } from 'types/file'; import { FileAttributes } from 'types/file';
import { USE_CF_PROXY } from 'constants/upload'; import { USE_CF_PROXY } from 'constants/upload';
import { Remote } from 'comlink';
import { DedicatedCryptoWorker } from 'worker/crypto.worker';
import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker';
const ENDPOINT = getEndpoint(); const ENDPOINT = getEndpoint();
const REPLACE_THUMBNAIL_THRESHOLD = 500 * 1024; // 500KB const REPLACE_THUMBNAIL_THRESHOLD = 500 * 1024; // 500KB
@ -44,7 +46,7 @@ export async function replaceThumbnail(
let completedWithError = false; let completedWithError = false;
try { try {
const token = getToken(); const token = getToken();
const worker = await new CryptoWorker(); const cryptoWorker = await ComlinkCryptoWorker.getInstance();
const files = await getLocalFiles(); const files = await getLocalFiles();
const trash = await getLocalTrash(); const trash = await getLocalTrash();
const trashFiles = getTrashedFiles(trash); const trashFiles = getTrashedFiles(trash);
@ -83,7 +85,7 @@ export async function replaceThumbnail(
fileTypeInfo fileTypeInfo
); );
const newUploadedThumbnail = await uploadThumbnail( const newUploadedThumbnail = await uploadThumbnail(
worker, cryptoWorker,
file.key, file.key,
newThumbnail, newThumbnail,
uploadURLs.pop() uploadURLs.pop()
@ -102,24 +104,26 @@ export async function replaceThumbnail(
} }
export async function uploadThumbnail( export async function uploadThumbnail(
worker, worker: Remote<DedicatedCryptoWorker>,
fileKey: string, fileKey: string,
updatedThumbnail: Uint8Array, updatedThumbnail: Uint8Array,
uploadURL: UploadURL uploadURL: UploadURL
): Promise<fileAttribute> { ): Promise<FileAttributes> {
const { file: encryptedThumbnail }: EncryptionResult = const { file: encryptedThumbnail } = await worker.encryptThumbnail(
await worker.encryptThumbnail(updatedThumbnail, fileKey); updatedThumbnail,
fileKey
);
let thumbnailObjectKey: string = null; let thumbnailObjectKey: string = null;
if (USE_CF_PROXY) { if (USE_CF_PROXY) {
thumbnailObjectKey = await uploadHttpClient.putFileV2( thumbnailObjectKey = await uploadHttpClient.putFileV2(
uploadURL, uploadURL,
encryptedThumbnail.encryptedData as Uint8Array, encryptedThumbnail.encryptedData,
() => {} () => {}
); );
} else { } else {
thumbnailObjectKey = await uploadHttpClient.putFile( thumbnailObjectKey = await uploadHttpClient.putFile(
uploadURL, uploadURL,
encryptedThumbnail.encryptedData as Uint8Array, encryptedThumbnail.encryptedData,
() => {} () => {}
); );
} }
@ -131,7 +135,7 @@ export async function uploadThumbnail(
export async function updateThumbnail( export async function updateThumbnail(
fileID: number, fileID: number,
newThumbnail: fileAttribute newThumbnail: FileAttributes
) { ) {
try { try {
const token = getToken(); const token = getToken();

View file

@ -2,7 +2,6 @@ import {
getPublicCollectionFileURL, getPublicCollectionFileURL,
getPublicCollectionThumbnailURL, getPublicCollectionThumbnailURL,
} from 'utils/common/apiUtil'; } from 'utils/common/apiUtil';
import CryptoWorker from 'utils/crypto';
import { import {
generateStreamFromArrayBuffer, generateStreamFromArrayBuffer,
getRenderableFileURL, getRenderableFileURL,
@ -15,6 +14,8 @@ import { logError } from 'utils/sentry';
import { FILE_TYPE } from 'constants/file'; import { FILE_TYPE } from 'constants/file';
import { CustomError } from 'utils/error'; import { CustomError } from 'utils/error';
import QueueProcessor from './queueProcessor'; import QueueProcessor from './queueProcessor';
import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker';
import { addLogLine } from 'utils/logging';
class PublicCollectionDownloadManager { class PublicCollectionDownloadManager {
private fileObjectURLPromise = new Map< private fileObjectURLPromise = new Map<
@ -30,11 +31,17 @@ class PublicCollectionDownloadManager {
token: string, token: string,
passwordToken: string passwordToken: string
) { ) {
addLogLine(`[${file.id}] [PublicDownloadManger] getThumbnail called`);
try { try {
if (!token) { if (!token) {
return null; return null;
} }
if (!this.thumbnailObjectURLPromise.get(file.id)) { if (this.thumbnailObjectURLPromise.has(file.id)) {
addLogLine(
`[${file.id}] [PublicDownloadManger] getThumbnail promise cache hit, returning existing promise`
);
}
if (!this.thumbnailObjectURLPromise.has(file.id)) {
const downloadPromise = async () => { const downloadPromise = async () => {
const thumbnailCache = await (async () => { const thumbnailCache = await (async () => {
try { try {
@ -50,8 +57,14 @@ class PublicCollectionDownloadManager {
); );
if (cacheResp) { if (cacheResp) {
addLogLine(
`[${file.id}] [PublicDownloadManger] in memory cache hit, using localCache files`
);
return URL.createObjectURL(await cacheResp.blob()); return URL.createObjectURL(await cacheResp.blob());
} }
addLogLine(
`[${file.id}] [PublicDownloadManger] in memory cache miss, getThumbnail download started`
);
const thumb = const thumb =
await this.thumbnailDownloadRequestsProcessor.queueUpRequest( await this.thumbnailDownloadRequestsProcessor.queueUpRequest(
() => this.downloadThumb(token, passwordToken, file) () => this.downloadThumb(token, passwordToken, file)
@ -73,7 +86,7 @@ class PublicCollectionDownloadManager {
return await this.thumbnailObjectURLPromise.get(file.id); return await this.thumbnailObjectURLPromise.get(file.id);
} catch (e) { } catch (e) {
this.thumbnailObjectURLPromise.delete(file.id); this.thumbnailObjectURLPromise.delete(file.id);
logError(e, 'get preview Failed'); logError(e, 'get publicDownloadManger preview Failed');
throw e; throw e;
} }
} }
@ -97,10 +110,10 @@ class PublicCollectionDownloadManager {
if (typeof resp.data === 'undefined') { if (typeof resp.data === 'undefined') {
throw Error(CustomError.REQUEST_FAILED); throw Error(CustomError.REQUEST_FAILED);
} }
const worker = await new CryptoWorker(); const cryptoWorker = await ComlinkCryptoWorker.getInstance();
const decrypted: Uint8Array = await worker.decryptThumbnail( const decrypted = await cryptoWorker.decryptThumbnail(
new Uint8Array(resp.data), new Uint8Array(resp.data),
await worker.fromB64(file.thumbnail.decryptionHeader), await cryptoWorker.fromB64(file.thumbnail.decryptionHeader),
file.key file.key
); );
return decrypted; return decrypted;
@ -115,6 +128,9 @@ class PublicCollectionDownloadManager {
const fileKey = forPreview ? `${file.id}_preview` : `${file.id}`; const fileKey = forPreview ? `${file.id}_preview` : `${file.id}`;
try { try {
const getFilePromise = async () => { const getFilePromise = async () => {
addLogLine(
`[${file.id}] [PublicDownloadManager] downloading file`
);
const fileStream = await this.downloadFile( const fileStream = await this.downloadFile(
token, token,
passwordToken, passwordToken,
@ -131,6 +147,11 @@ class PublicCollectionDownloadManager {
return { converted: [fileURL], original: [fileURL] }; return { converted: [fileURL], original: [fileURL] };
} }
}; };
if (this.fileObjectURLPromise.has(fileKey)) {
addLogLine(
`[${file.id}] [PublicDownloadManager] getFile promise cache hit, returning existing promise`
);
}
if (!this.fileObjectURLPromise.get(fileKey)) { if (!this.fileObjectURLPromise.get(fileKey)) {
this.fileObjectURLPromise.set(fileKey, getFilePromise()); this.fileObjectURLPromise.set(fileKey, getFilePromise());
} }
@ -138,7 +159,7 @@ class PublicCollectionDownloadManager {
return fileURLs; return fileURLs;
} catch (e) { } catch (e) {
this.fileObjectURLPromise.delete(fileKey); this.fileObjectURLPromise.delete(fileKey);
logError(e, 'Failed to get File'); logError(e, 'public download manager Failed to get File');
throw e; throw e;
} }
}; };
@ -148,7 +169,7 @@ class PublicCollectionDownloadManager {
} }
async downloadFile(token: string, passwordToken: string, file: EnteFile) { async downloadFile(token: string, passwordToken: string, file: EnteFile) {
const worker = await new CryptoWorker(); const cryptoWorker = await ComlinkCryptoWorker.getInstance();
if (!token) { if (!token) {
return null; return null;
} }
@ -170,9 +191,9 @@ class PublicCollectionDownloadManager {
if (typeof resp.data === 'undefined') { if (typeof resp.data === 'undefined') {
throw Error(CustomError.REQUEST_FAILED); throw Error(CustomError.REQUEST_FAILED);
} }
const decrypted: any = await worker.decryptFile( const decrypted = await cryptoWorker.decryptFile(
new Uint8Array(resp.data), new Uint8Array(resp.data),
await worker.fromB64(file.file.decryptionHeader), await cryptoWorker.fromB64(file.file.decryptionHeader),
file.key file.key
); );
return generateStreamFromArrayBuffer(decrypted); return generateStreamFromArrayBuffer(decrypted);
@ -188,12 +209,15 @@ class PublicCollectionDownloadManager {
const reader = resp.body.getReader(); const reader = resp.body.getReader();
const stream = new ReadableStream({ const stream = new ReadableStream({
async start(controller) { async start(controller) {
const decryptionHeader = await worker.fromB64( const decryptionHeader = await cryptoWorker.fromB64(
file.file.decryptionHeader file.file.decryptionHeader
); );
const fileKey = await worker.fromB64(file.key); const fileKey = await cryptoWorker.fromB64(file.key);
const { pullState, decryptionChunkSize } = const { pullState, decryptionChunkSize } =
await worker.initDecryption(decryptionHeader, fileKey); await cryptoWorker.initDecryption(
decryptionHeader,
fileKey
);
let data = new Uint8Array(); let data = new Uint8Array();
// The following function handles each data chunk // The following function handles each data chunk
function push() { function push() {
@ -212,7 +236,7 @@ class PublicCollectionDownloadManager {
decryptionChunkSize decryptionChunkSize
); );
const { decryptedData } = const { decryptedData } =
await worker.decryptChunk( await cryptoWorker.decryptChunk(
fileData, fileData,
pullState pullState
); );
@ -225,7 +249,10 @@ class PublicCollectionDownloadManager {
} else { } else {
if (data) { if (data) {
const { decryptedData } = const { decryptedData } =
await worker.decryptChunk(data, pullState); await cryptoWorker.decryptChunk(
data,
pullState
);
controller.enqueue(decryptedData); controller.enqueue(decryptedData);
data = null; data = null;
} }

View file

@ -1,18 +1,18 @@
import { getEndpoint } from 'utils/common/apiUtil'; import { getEndpoint } from 'utils/common/apiUtil';
import localForage from 'utils/storage/localForage'; import localForage from 'utils/storage/localForage';
import { Collection } from 'types/collection'; import { Collection, EncryptedCollection } from 'types/collection';
import HTTPService from './HTTPService'; import HTTPService from './HTTPService';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
import { decryptFile, mergeMetadata, sortFiles } from 'utils/file'; import { decryptFile, mergeMetadata, sortFiles } from 'utils/file';
import { EnteFile } from 'types/file'; import { EncryptedEnteFile, EnteFile } from 'types/file';
import { import {
AbuseReportDetails, AbuseReportDetails,
AbuseReportRequest, AbuseReportRequest,
LocalSavedPublicCollectionFiles, LocalSavedPublicCollectionFiles,
} from 'types/publicCollection'; } from 'types/publicCollection';
import CryptoWorker from 'utils/crypto';
import { REPORT_REASON } from 'constants/publicCollection'; import { REPORT_REASON } from 'constants/publicCollection';
import { CustomError, parseSharingErrorCodes } from 'utils/error'; import { CustomError, parseSharingErrorCodes } from 'utils/error';
import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker';
const ENDPOINT = getEndpoint(); const ENDPOINT = getEndpoint();
const PUBLIC_COLLECTION_FILES_TABLE = 'public-collection-files'; const PUBLIC_COLLECTION_FILES_TABLE = 'public-collection-files';
@ -20,12 +20,29 @@ const PUBLIC_COLLECTIONS_TABLE = 'public-collections';
export const getPublicCollectionUID = (token: string) => `${token}`; export const getPublicCollectionUID = (token: string) => `${token}`;
const getPublicCollectionSyncTimeUID = (collectionUID: string) => const getPublicCollectionLastSyncTimeKey = (collectionUID: string) =>
`public-${collectionUID}-time`; `public-${collectionUID}-time`;
const getPublicCollectionPasswordKey = (collectionUID: string) => const getPublicCollectionPasswordKey = (collectionUID: string) =>
`public-${collectionUID}-passkey`; `public-${collectionUID}-passkey`;
const getPublicCollectionUploaderNameKey = (collectionUID: string) =>
`public-${collectionUID}-uploaderName`;
export const getPublicCollectionUploaderName = async (collectionUID: string) =>
await localForage.getItem<string>(
getPublicCollectionUploaderNameKey(collectionUID)
);
export const savePublicCollectionUploaderName = async (
collectionUID: string,
uploaderName: string
) =>
await localForage.setItem(
getPublicCollectionUploaderNameKey(collectionUID),
uploaderName
);
export const getLocalPublicFiles = async (collectionUID: string) => { export const getLocalPublicFiles = async (collectionUID: string) => {
const localSavedPublicCollectionFiles = const localSavedPublicCollectionFiles =
( (
@ -129,15 +146,15 @@ const dedupeCollectionFiles = (
const getPublicCollectionLastSyncTime = async (collectionUID: string) => const getPublicCollectionLastSyncTime = async (collectionUID: string) =>
(await localForage.getItem<number>( (await localForage.getItem<number>(
getPublicCollectionSyncTimeUID(collectionUID) getPublicCollectionLastSyncTimeKey(collectionUID)
)) ?? 0; )) ?? 0;
const setPublicCollectionLastSyncTime = async ( const savePublicCollectionLastSyncTime = async (
collectionUID: string, collectionUID: string,
time: number time: number
) => ) =>
await localForage.setItem( await localForage.setItem(
getPublicCollectionSyncTimeUID(collectionUID), getPublicCollectionLastSyncTimeKey(collectionUID),
time time
); );
@ -191,7 +208,7 @@ export const syncPublicFiles = async (
files.push(file); files.push(file);
} }
await savePublicCollectionFiles(collectionUID, files); await savePublicCollectionFiles(collectionUID, files);
await setPublicCollectionLastSyncTime( await savePublicCollectionLastSyncTime(
collectionUID, collectionUID,
collection.updationTime collection.updationTime
); );
@ -200,7 +217,6 @@ export const syncPublicFiles = async (
const parsedError = parseSharingErrorCodes(e); const parsedError = parseSharingErrorCodes(e);
logError(e, 'failed to sync shared collection files'); logError(e, 'failed to sync shared collection files');
if (parsedError.message === CustomError.TOKEN_EXPIRED) { if (parsedError.message === CustomError.TOKEN_EXPIRED) {
console.log('invalid token or password');
throw e; throw e;
} }
} }
@ -243,11 +259,12 @@ const getPublicFiles = async (
decryptedFiles = [ decryptedFiles = [
...decryptedFiles, ...decryptedFiles,
...(await Promise.all( ...(await Promise.all(
resp.data.diff.map(async (file: EnteFile) => { resp.data.diff.map(async (file: EncryptedEnteFile) => {
if (!file.isDeleted) { if (!file.isDeleted) {
file = await decryptFile(file, collection.key); return await decryptFile(file, collection.key);
} else {
return file;
} }
return file;
}) as Promise<EnteFile>[] }) as Promise<EnteFile>[]
)), )),
]; ];
@ -323,14 +340,14 @@ export const verifyPublicCollectionPassword = async (
}; };
const decryptCollectionName = async ( const decryptCollectionName = async (
collection: Collection, collection: EncryptedCollection,
collectionKey: string collectionKey: string
) => { ) => {
const worker = await new CryptoWorker(); const cryptoWorker = await ComlinkCryptoWorker.getInstance();
return (collection.name = return (collection.name =
collection.name || collection.name ||
(await worker.decryptToUTF8( (await cryptoWorker.decryptToUTF8(
collection.encryptedName, collection.encryptedName,
collection.nameDecryptionNonce, collection.nameDecryptionNonce,
collectionKey collectionKey
@ -379,7 +396,9 @@ export const removePublicCollectionWithFiles = async (
export const removePublicFiles = async (collectionUID: string) => { export const removePublicFiles = async (collectionUID: string) => {
await localForage.removeItem(getPublicCollectionPasswordKey(collectionUID)); await localForage.removeItem(getPublicCollectionPasswordKey(collectionUID));
await localForage.removeItem(getPublicCollectionSyncTimeUID(collectionUID)); await localForage.removeItem(
getPublicCollectionLastSyncTimeKey(collectionUID)
);
const publicCollectionFiles = const publicCollectionFiles =
(await localForage.getItem<LocalSavedPublicCollectionFiles[]>( (await localForage.getItem<LocalSavedPublicCollectionFiles[]>(

View file

@ -17,6 +17,7 @@ import {
} from 'types/search'; } from 'types/search';
import { FILE_TYPE } from 'constants/file'; import { FILE_TYPE } from 'constants/file';
import { getFormattedDate, isInsideBox, isSameDayAnyYear } from 'utils/search'; import { getFormattedDate, isInsideBox, isSameDayAnyYear } from 'utils/search';
import { getUniqueFiles } from 'utils/file';
const ENDPOINT = getEndpoint(); const ENDPOINT = getEndpoint();
@ -43,8 +44,8 @@ export const getAutoCompleteSuggestions =
searchQuery: convertSuggestionToSearchQuery(suggestion), searchQuery: convertSuggestionToSearchQuery(suggestion),
})) }))
.map(({ suggestion, searchQuery }) => { .map(({ suggestion, searchQuery }) => {
const resultFiles = files.filter((file) => const resultFiles = getUniqueFiles(
isSearchedFile(file, searchQuery) files.filter((file) => isSearchedFile(file, searchQuery))
); );
return { return {
...suggestion, ...suggestion,
@ -114,7 +115,7 @@ function searchCollection(
} }
function searchFiles(searchPhrase: string, files: EnteFile[]) { function searchFiles(searchPhrase: string, files: EnteFile[]) {
return files return getUniqueFiles(files)
.map((file) => ({ .map((file) => ({
title: file.metadata.title, title: file.metadata.title,
id: file.id, id: file.id,

View file

@ -14,7 +14,7 @@ import { getCollection } from './collectionService';
import { EnteFile } from 'types/file'; import { EnteFile } from 'types/file';
import HTTPService from './HTTPService'; import HTTPService from './HTTPService';
import { Trash, TrashItem } from 'types/trash'; import { EncryptedTrashItem, Trash } from 'types/trash';
const TRASH = 'file-trash'; const TRASH = 'file-trash';
const TRASH_TIME = 'trash-time'; const TRASH_TIME = 'trash-time';
@ -99,7 +99,8 @@ export const updateTrash = async (
'X-Auth-Token': token, 'X-Auth-Token': token,
} }
); );
for (const trashItem of resp.data.diff as TrashItem[]) { // #Perf: This can be optimized by running the decryption in parallel
for (const trashItem of resp.data.diff as EncryptedTrashItem[]) {
const collectionID = trashItem.file.collectionID; const collectionID = trashItem.file.collectionID;
let collection = collections.get(collectionID); let collection = collections.get(collectionID);
if (!collection) { if (!collection) {
@ -110,19 +111,21 @@ export const updateTrash = async (
]); ]);
} }
if (!trashItem.isDeleted && !trashItem.isRestored) { if (!trashItem.isDeleted && !trashItem.isRestored) {
trashItem.file = await decryptFile( const decryptedFile = await decryptFile(
trashItem.file, trashItem.file,
collection.key collection.key
); );
updatedTrash.push({ ...trashItem, file: decryptedFile });
} else {
updatedTrash = updatedTrash.filter(
(item) => item.file.id !== trashItem.file.id
);
} }
updatedTrash.push(trashItem);
} }
if (resp.data.diff.length) { if (resp.data.diff.length) {
time = resp.data.diff.slice(-1)[0].updatedAt; time = resp.data.diff.slice(-1)[0].updatedAt;
} }
updatedTrash = removeDuplicates(updatedTrash);
updatedTrash = removeRestoredOrDeletedTrashItems(updatedTrash);
setFiles( setFiles(
preservePhotoswipeProps( preservePhotoswipeProps(
@ -145,28 +148,6 @@ export const updateTrash = async (
return currentTrash; return currentTrash;
}; };
function removeDuplicates(trash: Trash) {
const latestVersionTrashItems = new Map<number, TrashItem>();
trash.forEach(({ file, updatedAt, ...rest }) => {
if (
!latestVersionTrashItems.has(file.id) ||
latestVersionTrashItems.get(file.id).updatedAt < updatedAt
) {
latestVersionTrashItems.set(file.id, { file, updatedAt, ...rest });
}
});
trash = [];
// eslint-disable-next-line @typescript-eslint/no-unused-vars
for (const [_, trashedFile] of latestVersionTrashItems) {
trash.push(trashedFile);
}
return trash;
}
function removeRestoredOrDeletedTrashItems(trash: Trash) {
return trash.filter((item) => !item.isDeleted && !item.isRestored);
}
export function getTrashedFiles(trash: Trash) { export function getTrashedFiles(trash: Trash) {
return mergeMetadata( return mergeMetadata(
trash.map((trashedFile) => ({ trash.map((trashedFile) => ({

View file

@ -1,4 +1,5 @@
import { DataStream, EncryptionResult, isDataStream } from 'types/upload'; import { EncryptionResult } from 'types/crypto';
import { DataStream, isDataStream } from 'types/upload';
async function encryptFileStream(worker, fileData: DataStream) { async function encryptFileStream(worker, fileData: DataStream) {
const { stream, chunkCount } = fileData; const { stream, chunkCount } = fileData;
@ -33,7 +34,7 @@ async function encryptFileStream(worker, fileData: DataStream) {
export async function encryptFiledata( export async function encryptFiledata(
worker, worker,
filedata: Uint8Array | DataStream filedata: Uint8Array | DataStream
): Promise<EncryptionResult> { ): Promise<EncryptionResult<Uint8Array | DataStream>> {
return isDataStream(filedata) return isDataStream(filedata)
? await encryptFileStream(worker, filedata) ? await encryptFileStream(worker, filedata)
: await worker.encryptFile(filedata); : await worker.encryptFile(filedata);

View file

@ -3,9 +3,7 @@ import {
FileTypeInfo, FileTypeInfo,
FileInMemory, FileInMemory,
Metadata, Metadata,
B64EncryptionResult,
EncryptedFile, EncryptedFile,
EncryptionResult,
FileWithMetadata, FileWithMetadata,
ParsedMetadataJSONMap, ParsedMetadataJSONMap,
DataStream, DataStream,
@ -22,6 +20,9 @@ import {
getUint8ArrayView, getUint8ArrayView,
} from '../readerService'; } from '../readerService';
import { generateThumbnail } from './thumbnailService'; import { generateThumbnail } from './thumbnailService';
import { DedicatedCryptoWorker } from 'worker/crypto.worker';
import { Remote } from 'comlink';
import { EncryptedMagicMetadata } from 'types/magicMetadata';
const EDITED_FILE_SUFFIX = '-edited'; const EDITED_FILE_SUFFIX = '-edited';
@ -68,10 +69,11 @@ export async function readFile(
} }
export async function extractFileMetadata( export async function extractFileMetadata(
worker,
parsedMetadataJSONMap: ParsedMetadataJSONMap, parsedMetadataJSONMap: ParsedMetadataJSONMap,
rawFile: File | ElectronFile,
collectionID: number, collectionID: number,
fileTypeInfo: FileTypeInfo fileTypeInfo: FileTypeInfo,
rawFile: File | ElectronFile
) { ) {
const originalName = getFileOriginalName(rawFile); const originalName = getFileOriginalName(rawFile);
const googleMetadata = const googleMetadata =
@ -79,6 +81,7 @@ export async function extractFileMetadata(
getMetadataJSONMapKey(collectionID, originalName) getMetadataJSONMapKey(collectionID, originalName)
) ?? {}; ) ?? {};
const extractedMetadata: Metadata = await extractMetadata( const extractedMetadata: Metadata = await extractMetadata(
worker,
rawFile, rawFile,
fileTypeInfo fileTypeInfo
); );
@ -93,7 +96,7 @@ export async function extractFileMetadata(
} }
export async function encryptFile( export async function encryptFile(
worker: any, worker: Remote<DedicatedCryptoWorker>,
file: FileWithMetadata, file: FileWithMetadata,
encryptionKey: string encryptionKey: string
): Promise<EncryptedFile> { ): Promise<EncryptedFile> {
@ -103,21 +106,38 @@ export async function encryptFile(
file.filedata file.filedata
); );
const { file: encryptedThumbnail }: EncryptionResult = const { file: encryptedThumbnail } = await worker.encryptThumbnail(
await worker.encryptThumbnail(file.thumbnail, fileKey); file.thumbnail,
const { file: encryptedMetadata }: EncryptionResult = fileKey
await worker.encryptMetadata(file.metadata, fileKey);
const encryptedKey: B64EncryptionResult = await worker.encryptToB64(
fileKey,
encryptionKey
); );
const { file: encryptedMetadata } = await worker.encryptMetadata(
file.metadata,
fileKey
);
let encryptedPubMagicMetadata: EncryptedMagicMetadata;
if (file.pubMagicMetadata) {
const { file: encryptedPubMagicMetadataData } =
await worker.encryptMetadata(
file.pubMagicMetadata.data,
fileKey
);
encryptedPubMagicMetadata = {
version: file.pubMagicMetadata.version,
count: file.pubMagicMetadata.count,
data: encryptedPubMagicMetadataData.encryptedData,
header: encryptedPubMagicMetadataData.decryptionHeader,
};
}
const encryptedKey = await worker.encryptToB64(fileKey, encryptionKey);
const result: EncryptedFile = { const result: EncryptedFile = {
file: { file: {
file: encryptedFiledata, file: encryptedFiledata,
thumbnail: encryptedThumbnail, thumbnail: encryptedThumbnail,
metadata: encryptedMetadata, metadata: encryptedMetadata,
pubMagicMetadata: encryptedPubMagicMetadata,
localID: file.localID, localID: file.localID,
}, },
fileKey: encryptedKey, fileKey: encryptedKey,

View file

@ -1,11 +1,13 @@
import { FILE_READER_CHUNK_SIZE } from 'constants/upload'; import { FILE_READER_CHUNK_SIZE } from 'constants/upload';
import { getFileStream, getElectronFileStream } from 'services/readerService'; import { getFileStream, getElectronFileStream } from 'services/readerService';
import { ElectronFile, DataStream } from 'types/upload'; import { ElectronFile, DataStream } from 'types/upload';
import CryptoWorker from 'utils/crypto'; import { CustomError } from 'utils/error';
import { addLogLine, getFileNameSize } from 'utils/logging';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
export async function getFileHash(file: File | ElectronFile) { export async function getFileHash(worker, file: File | ElectronFile) {
try { try {
addLogLine(`getFileHash called for ${getFileNameSize(file)}`);
let filedata: DataStream; let filedata: DataStream;
if (file instanceof File) { if (file instanceof File) {
filedata = getFileStream(file, FILE_READER_CHUNK_SIZE); filedata = getFileStream(file, FILE_READER_CHUNK_SIZE);
@ -15,22 +17,29 @@ export async function getFileHash(file: File | ElectronFile) {
FILE_READER_CHUNK_SIZE FILE_READER_CHUNK_SIZE
); );
} }
const cryptoWorker = await new CryptoWorker(); const hashState = await worker.initChunkHashing();
const hashState = await cryptoWorker.initChunkHashing();
const reader = filedata.stream.getReader(); const streamReader = filedata.stream.getReader();
// eslint-disable-next-line no-constant-condition for (let i = 0; i < filedata.chunkCount; i++) {
while (true) { const { done, value: chunk } = await streamReader.read();
const { done, value: chunk } = await reader.read();
if (done) { if (done) {
break; throw Error(CustomError.CHUNK_LESS_THAN_EXPECTED);
} }
await cryptoWorker.hashFileChunk(hashState, Uint8Array.from(chunk)); await worker.hashFileChunk(hashState, Uint8Array.from(chunk));
} }
const hash = await cryptoWorker.completeChunkHashing(hashState); const { done } = await streamReader.read();
if (!done) {
throw Error(CustomError.CHUNK_MORE_THAN_EXPECTED);
}
const hash = await worker.completeChunkHashing(hashState);
addLogLine(
`file hashing completed successfully ${getFileNameSize(file)}`
);
return hash; return hash;
} catch (e) { } catch (e) {
logError(e, 'getFileHash failed'); logError(e, 'getFileHash failed');
throw e; addLogLine(
`file hashing failed ${getFileNameSize(file)} ,${e.message} `
);
} }
} }

View file

@ -1,20 +1,23 @@
import { FILE_TYPE } from 'constants/file'; import { FILE_TYPE } from 'constants/file';
import { LIVE_PHOTO_ASSET_SIZE_LIMIT } from 'constants/upload'; import { LIVE_PHOTO_ASSET_SIZE_LIMIT } from 'constants/upload';
import { encodeMotionPhoto } from 'services/motionPhotoService'; import { encodeMotionPhoto } from 'services/motionPhotoService';
import { getFileType } from 'services/typeDetectionService';
import { import {
ElectronFile, ElectronFile,
FileTypeInfo, FileTypeInfo,
FileWithCollection, FileWithCollection,
LivePhotoAssets, LivePhotoAssets,
Metadata, ParsedMetadataJSONMap,
} from 'types/upload'; } from 'types/upload';
import { CustomError } from 'utils/error'; import { CustomError } from 'utils/error';
import { isImageOrVideo, splitFilenameAndExtension } from 'utils/file'; import { getFileTypeFromExtensionForLivePhotoClustering } from 'utils/file/livePhoto';
import { splitFilenameAndExtension, isImageOrVideo } from 'utils/file';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
import { getUint8ArrayView } from '../readerService'; import { getUint8ArrayView } from '../readerService';
import { extractFileMetadata } from './fileService';
import { getFileHash } from './hashService';
import { generateThumbnail } from './thumbnailService'; import { generateThumbnail } from './thumbnailService';
import uploadService from './uploadService'; import uploadCancelService from './uploadCancelService';
import UploadService from './uploadService';
interface LivePhotoIdentifier { interface LivePhotoIdentifier {
collectionID: number; collectionID: number;
@ -23,56 +26,58 @@ interface LivePhotoIdentifier {
size: number; size: number;
} }
interface Asset {
file: File | ElectronFile;
metadata: Metadata;
fileTypeInfo: FileTypeInfo;
}
const ENTE_LIVE_PHOTO_FORMAT = 'elp';
const UNDERSCORE_THREE = '_3'; const UNDERSCORE_THREE = '_3';
const UNDERSCORE = '_'; const UNDERSCORE = '_';
export function getLivePhotoFileType( export async function getLivePhotoFileType(
imageFileTypeInfo: FileTypeInfo, livePhotoAssets: LivePhotoAssets
videoTypeInfo: FileTypeInfo ): Promise<FileTypeInfo> {
): FileTypeInfo { const imageFileTypeInfo = await getFileType(livePhotoAssets.image);
const videoFileTypeInfo = await getFileType(livePhotoAssets.video);
return { return {
fileType: FILE_TYPE.LIVE_PHOTO, fileType: FILE_TYPE.LIVE_PHOTO,
exactType: `${imageFileTypeInfo.exactType}+${videoTypeInfo.exactType}`, exactType: `${imageFileTypeInfo.exactType}+${videoFileTypeInfo.exactType}`,
imageType: imageFileTypeInfo.exactType, imageType: imageFileTypeInfo.exactType,
videoType: videoTypeInfo.exactType, videoType: videoFileTypeInfo.exactType,
}; };
} }
export function getLivePhotoMetadata( export async function extractLivePhotoMetadata(
imageMetadata: Metadata, worker,
videoMetadata: Metadata parsedMetadataJSONMap: ParsedMetadataJSONMap,
collectionID: number,
fileTypeInfo: FileTypeInfo,
livePhotoAssets: LivePhotoAssets
) { ) {
const imageFileTypeInfo: FileTypeInfo = {
fileType: FILE_TYPE.IMAGE,
exactType: fileTypeInfo.imageType,
};
const imageMetadata = await extractFileMetadata(
worker,
parsedMetadataJSONMap,
collectionID,
imageFileTypeInfo,
livePhotoAssets.image
);
const videoHash = await getFileHash(worker, livePhotoAssets.video);
return { return {
...imageMetadata, ...imageMetadata,
title: getLivePhotoName(imageMetadata.title), title: getLivePhotoName(livePhotoAssets),
fileType: FILE_TYPE.LIVE_PHOTO, fileType: FILE_TYPE.LIVE_PHOTO,
imageHash: imageMetadata.hash, imageHash: imageMetadata.hash,
videoHash: videoMetadata.hash, videoHash: videoHash,
hash: undefined, hash: undefined,
}; };
} }
export function getLivePhotoFilePath(imageAsset: Asset): string {
return getLivePhotoName((imageAsset.file as any).path);
}
export function getLivePhotoSize(livePhotoAssets: LivePhotoAssets) { export function getLivePhotoSize(livePhotoAssets: LivePhotoAssets) {
return livePhotoAssets.image.size + livePhotoAssets.video.size; return livePhotoAssets.image.size + livePhotoAssets.video.size;
} }
export function getLivePhotoName(imageTitle: string) { export function getLivePhotoName(livePhotoAssets: LivePhotoAssets) {
return `${ return livePhotoAssets.image.name;
splitFilenameAndExtension(imageTitle)[0]
}.${ENTE_LIVE_PHOTO_FORMAT}`;
} }
export async function readLivePhoto( export async function readLivePhoto(
@ -103,7 +108,7 @@ export async function readLivePhoto(
}; };
} }
export function clusterLivePhotoFiles(mediaFiles: FileWithCollection[]) { export async function clusterLivePhotoFiles(mediaFiles: FileWithCollection[]) {
try { try {
const analysedMediaFiles: FileWithCollection[] = []; const analysedMediaFiles: FileWithCollection[] = [];
mediaFiles mediaFiles
@ -120,59 +125,48 @@ export function clusterLivePhotoFiles(mediaFiles: FileWithCollection[]) {
); );
let index = 0; let index = 0;
while (index < mediaFiles.length - 1) { while (index < mediaFiles.length - 1) {
if (uploadCancelService.isUploadCancelationRequested()) {
throw Error(CustomError.UPLOAD_CANCELLED);
}
const firstMediaFile = mediaFiles[index]; const firstMediaFile = mediaFiles[index];
const secondMediaFile = mediaFiles[index + 1]; const secondMediaFile = mediaFiles[index + 1];
const { const firstFileType =
fileTypeInfo: firstFileTypeInfo, getFileTypeFromExtensionForLivePhotoClustering(
metadata: firstFileMetadata, firstMediaFile.file.name
} = UploadService.getFileMetadataAndFileTypeInfo( );
firstMediaFile.localID const secondFileType =
); getFileTypeFromExtensionForLivePhotoClustering(
const { secondMediaFile.file.name
fileTypeInfo: secondFileFileInfo, );
metadata: secondFileMetadata,
} = UploadService.getFileMetadataAndFileTypeInfo(
secondMediaFile.localID
);
const firstFileIdentifier: LivePhotoIdentifier = { const firstFileIdentifier: LivePhotoIdentifier = {
collectionID: firstMediaFile.collectionID, collectionID: firstMediaFile.collectionID,
fileType: firstFileTypeInfo.fileType, fileType: firstFileType,
name: firstMediaFile.file.name, name: firstMediaFile.file.name,
size: firstMediaFile.file.size, size: firstMediaFile.file.size,
}; };
const secondFileIdentifier: LivePhotoIdentifier = { const secondFileIdentifier: LivePhotoIdentifier = {
collectionID: secondMediaFile.collectionID, collectionID: secondMediaFile.collectionID,
fileType: secondFileFileInfo.fileType, fileType: secondFileType,
name: secondMediaFile.file.name, name: secondMediaFile.file.name,
size: secondMediaFile.file.size, size: secondMediaFile.file.size,
}; };
const firstAsset = {
file: firstMediaFile.file,
metadata: firstFileMetadata,
fileTypeInfo: firstFileTypeInfo,
};
const secondAsset = {
file: secondMediaFile.file,
metadata: secondFileMetadata,
fileTypeInfo: secondFileFileInfo,
};
if ( if (
areFilesLivePhotoAssets( areFilesLivePhotoAssets(
firstFileIdentifier, firstFileIdentifier,
secondFileIdentifier secondFileIdentifier
) )
) { ) {
let imageAsset: Asset; let imageFile: File | ElectronFile;
let videoAsset: Asset; let videoFile: File | ElectronFile;
if ( if (
firstFileTypeInfo.fileType === FILE_TYPE.IMAGE && firstFileType === FILE_TYPE.IMAGE &&
secondFileFileInfo.fileType === FILE_TYPE.VIDEO secondFileType === FILE_TYPE.VIDEO
) { ) {
imageAsset = firstAsset; imageFile = firstMediaFile.file;
videoAsset = secondAsset; videoFile = secondMediaFile.file;
} else { } else {
videoAsset = firstAsset; videoFile = firstMediaFile.file;
imageAsset = secondAsset; imageFile = secondMediaFile.file;
} }
const livePhotoLocalID = firstMediaFile.localID; const livePhotoLocalID = firstMediaFile.localID;
analysedMediaFiles.push({ analysedMediaFiles.push({
@ -180,25 +174,10 @@ export function clusterLivePhotoFiles(mediaFiles: FileWithCollection[]) {
collectionID: firstMediaFile.collectionID, collectionID: firstMediaFile.collectionID,
isLivePhoto: true, isLivePhoto: true,
livePhotoAssets: { livePhotoAssets: {
image: imageAsset.file, image: imageFile,
video: videoAsset.file, video: videoFile,
}, },
}); });
const livePhotoFileTypeInfo: FileTypeInfo =
getLivePhotoFileType(
imageAsset.fileTypeInfo,
videoAsset.fileTypeInfo
);
const livePhotoMetadata: Metadata = getLivePhotoMetadata(
imageAsset.metadata,
videoAsset.metadata
);
const livePhotoPath = getLivePhotoFilePath(imageAsset);
uploadService.setFileMetadataAndFileTypeInfo(livePhotoLocalID, {
fileTypeInfo: { ...livePhotoFileTypeInfo },
metadata: { ...livePhotoMetadata },
filePath: livePhotoPath,
});
index += 2; index += 2;
} else { } else {
analysedMediaFiles.push({ analysedMediaFiles.push({
@ -216,8 +195,12 @@ export function clusterLivePhotoFiles(mediaFiles: FileWithCollection[]) {
} }
return analysedMediaFiles; return analysedMediaFiles;
} catch (e) { } catch (e) {
logError(e, 'failed to cluster live photo'); if (e.message === CustomError.UPLOAD_CANCELLED) {
throw e; throw e;
} else {
logError(e, 'failed to cluster live photo');
throw e;
}
} }
} }

View file

@ -0,0 +1,17 @@
import {
NEW_FILE_MAGIC_METADATA,
FilePublicMagicMetadataProps,
FilePublicMagicMetadata,
} from 'types/magicMetadata';
import { updateMagicMetadataProps } from 'utils/magicMetadata';
export async function constructPublicMagicMetadata(
publicMagicMetadataProps: FilePublicMagicMetadataProps
): Promise<FilePublicMagicMetadata> {
const pubMagicMetadata = await updateMagicMetadataProps(
NEW_FILE_MAGIC_METADATA,
null,
publicMagicMetadataProps
);
return pubMagicMetadata;
}

View file

@ -30,6 +30,7 @@ const NULL_PARSED_METADATA_JSON: ParsedMetadataJSON = {
}; };
export async function extractMetadata( export async function extractMetadata(
worker,
receivedFile: File | ElectronFile, receivedFile: File | ElectronFile,
fileTypeInfo: FileTypeInfo fileTypeInfo: FileTypeInfo
) { ) {
@ -39,7 +40,7 @@ export async function extractMetadata(
} else if (fileTypeInfo.fileType === FILE_TYPE.VIDEO) { } else if (fileTypeInfo.fileType === FILE_TYPE.VIDEO) {
extractedMetadata = await getVideoMetadata(receivedFile); extractedMetadata = await getVideoMetadata(receivedFile);
} }
const fileHash = await getFileHash(receivedFile); const fileHash = await getFileHash(worker, receivedFile);
const metadata: Metadata = { const metadata: Metadata = {
title: receivedFile.name, title: receivedFile.name,

View file

@ -0,0 +1,116 @@
import HTTPService from 'services/HTTPService';
import { getEndpoint } from 'utils/common/apiUtil';
import { logError } from 'utils/sentry';
import { EnteFile } from 'types/file';
import { CustomError, handleUploadError } from 'utils/error';
import { UploadFile, UploadURL, MultipartUploadURLs } from 'types/upload';
import { retryHTTPCall } from 'utils/upload/uploadRetrier';
const ENDPOINT = getEndpoint();
const MAX_URL_REQUESTS = 50;
class PublicUploadHttpClient {
private uploadURLFetchInProgress = null;
async uploadFile(
uploadFile: UploadFile,
token: string,
passwordToken: string
): Promise<EnteFile> {
try {
if (!token) {
throw Error(CustomError.TOKEN_MISSING);
}
const response = await retryHTTPCall(
() =>
HTTPService.post(
`${ENDPOINT}/public-collection/file`,
uploadFile,
null,
{
'X-Auth-Access-Token': token,
...(passwordToken && {
'X-Auth-Access-Token-JWT': passwordToken,
}),
}
),
handleUploadError
);
return response.data;
} catch (e) {
logError(e, 'upload public File Failed');
throw e;
}
}
async fetchUploadURLs(
count: number,
urlStore: UploadURL[],
token: string,
passwordToken: string
): Promise<void> {
try {
if (!this.uploadURLFetchInProgress) {
try {
if (!token) {
throw Error(CustomError.TOKEN_MISSING);
}
this.uploadURLFetchInProgress = HTTPService.get(
`${ENDPOINT}/public-collection/upload-urls`,
{
count: Math.min(MAX_URL_REQUESTS, count * 2),
},
{
'X-Auth-Access-Token': token,
...(passwordToken && {
'X-Auth-Access-Token-JWT': passwordToken,
}),
}
);
const response = await this.uploadURLFetchInProgress;
for (const url of response.data['urls']) {
urlStore.push(url);
}
} finally {
this.uploadURLFetchInProgress = null;
}
}
return this.uploadURLFetchInProgress;
} catch (e) {
logError(e, 'fetch public upload-url failed ');
throw e;
}
}
async fetchMultipartUploadURLs(
count: number,
token: string,
passwordToken: string
): Promise<MultipartUploadURLs> {
try {
if (!token) {
throw Error(CustomError.TOKEN_MISSING);
}
const response = await HTTPService.get(
`${ENDPOINT}/public-collection/multipart-upload-urls`,
{
count,
},
{
'X-Auth-Access-Token': token,
...(passwordToken && {
'X-Auth-Access-Token-JWT': passwordToken,
}),
}
);
return response.data['urls'];
} catch (e) {
logError(e, 'fetch public multipart-upload-url failed');
throw e;
}
}
}
export default new PublicUploadHttpClient();

View file

@ -3,6 +3,7 @@ import { CustomError, errorWithContext } from 'utils/error';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
import { BLACK_THUMBNAIL_BASE64 } from 'constants/upload'; import { BLACK_THUMBNAIL_BASE64 } from 'constants/upload';
import * as FFmpegService from 'services/ffmpeg/ffmpegService'; import * as FFmpegService from 'services/ffmpeg/ffmpegService';
import ElectronImageProcessorService from 'services/electron/imageProcessor';
import { convertBytesToHumanReadable } from 'utils/file/size'; import { convertBytesToHumanReadable } from 'utils/file/size';
import { isExactTypeHEIC } from 'utils/file'; import { isExactTypeHEIC } from 'utils/file';
import { ElectronFile, FileTypeInfo } from 'types/upload'; import { ElectronFile, FileTypeInfo } from 'types/upload';
@ -30,42 +31,24 @@ export async function generateThumbnail(
try { try {
addLogLine(`generating thumbnail for ${getFileNameSize(file)}`); addLogLine(`generating thumbnail for ${getFileNameSize(file)}`);
let hasStaticThumbnail = false; let hasStaticThumbnail = false;
let canvas = document.createElement('canvas');
let thumbnail: Uint8Array; let thumbnail: Uint8Array;
try { try {
if (fileTypeInfo.fileType === FILE_TYPE.IMAGE) { if (fileTypeInfo.fileType === FILE_TYPE.IMAGE) {
const isHEIC = isExactTypeHEIC(fileTypeInfo.exactType); thumbnail = await generateImageThumbnail(file, fileTypeInfo);
canvas = await generateImageThumbnail(file, isHEIC);
} else { } else {
try { thumbnail = await generateVideoThumbnail(file, fileTypeInfo);
addLogLine( }
`ffmpeg generateThumbnail called for ${getFileNameSize( if (thumbnail.length > 1.5 * MAX_THUMBNAIL_SIZE) {
file logError(
)}` Error('thumbnail_too_large'),
); 'thumbnail greater than max limit',
{
const thumbFile = thumbnailSize: convertBytesToHumanReadable(
await FFmpegService.generateVideoThumbnail(file); thumbnail.length
addLogLine( ),
`ffmpeg thumbnail successfully generated ${getFileNameSize( }
file );
)}`
);
canvas = await generateImageThumbnail(thumbFile, false);
} catch (e) {
addLogLine(
`ffmpeg thumbnail generated failed ${getFileNameSize(
file
)} error: ${e.message}`
);
logError(e, 'failed to generate thumbnail using ffmpeg', {
fileFormat: fileTypeInfo.exactType,
});
canvas = await generateVideoThumbnail(file);
}
} }
const thumbnailBlob = await thumbnailCanvasToBlob(canvas);
thumbnail = await getUint8ArrayView(thumbnailBlob);
if (thumbnail.length === 0) { if (thumbnail.length === 0) {
throw Error('EMPTY THUMBNAIL'); throw Error('EMPTY THUMBNAIL');
} }
@ -93,16 +76,42 @@ export async function generateThumbnail(
} }
} }
export async function generateImageThumbnail( async function generateImageThumbnail(
file: File | ElectronFile, file: File | ElectronFile,
isHEIC: boolean fileTypeInfo: FileTypeInfo
) {
if (ElectronImageProcessorService.generateImageThumbnailAPIExists()) {
try {
return await ElectronImageProcessorService.generateImageThumbnail(
file,
MAX_THUMBNAIL_DIMENSION,
MAX_THUMBNAIL_SIZE
);
} catch (e) {
logError(
e,
'Error generating thumbnail using electron image processor',
{
fileFormat: fileTypeInfo.exactType,
}
);
return await generateImageThumbnailUsingCanvas(file, fileTypeInfo);
}
} else {
return await generateImageThumbnailUsingCanvas(file, fileTypeInfo);
}
}
export async function generateImageThumbnailUsingCanvas(
file: File | ElectronFile,
fileTypeInfo: FileTypeInfo
) { ) {
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
const canvasCTX = canvas.getContext('2d'); const canvasCTX = canvas.getContext('2d');
let imageURL = null; let imageURL = null;
let timeout = null; let timeout = null;
const isHEIC = isExactTypeHEIC(fileTypeInfo.exactType);
if (isHEIC) { if (isHEIC) {
addLogLine(`HEICConverter called for ${getFileNameSize(file)}`); addLogLine(`HEICConverter called for ${getFileNameSize(file)}`);
const convertedBlob = await HeicConversionService.convert( const convertedBlob = await HeicConversionService.convert(
@ -151,10 +160,42 @@ export async function generateImageThumbnail(
WAIT_TIME_THUMBNAIL_GENERATION WAIT_TIME_THUMBNAIL_GENERATION
); );
}); });
return canvas; const thumbnailBlob = await getCompressedThumbnailBlobFromCanvas(canvas);
return await getUint8ArrayView(thumbnailBlob);
} }
export async function generateVideoThumbnail(file: File | ElectronFile) { async function generateVideoThumbnail(
file: File | ElectronFile,
fileTypeInfo: FileTypeInfo
) {
let thumbnail: Uint8Array;
try {
addLogLine(
`ffmpeg generateThumbnail called for ${getFileNameSize(file)}`
);
const thumbnail = await FFmpegService.generateVideoThumbnail(file);
addLogLine(
`ffmpeg thumbnail successfully generated ${getFileNameSize(file)}`
);
return getUint8ArrayView(thumbnail);
} catch (e) {
addLogLine(
`ffmpeg thumbnail generated failed ${getFileNameSize(
file
)} error: ${e.message}`
);
logError(e, 'failed to generate thumbnail using ffmpeg', {
fileFormat: fileTypeInfo.exactType,
});
thumbnail = await generateVideoThumbnailUsingCanvas(file);
}
return thumbnail;
}
export async function generateVideoThumbnailUsingCanvas(
file: File | ElectronFile
) {
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
const canvasCTX = canvas.getContext('2d'); const canvasCTX = canvas.getContext('2d');
@ -205,10 +246,11 @@ export async function generateVideoThumbnail(file: File | ElectronFile) {
WAIT_TIME_THUMBNAIL_GENERATION WAIT_TIME_THUMBNAIL_GENERATION
); );
}); });
return canvas; const thumbnailBlob = await getCompressedThumbnailBlobFromCanvas(canvas);
return await getUint8ArrayView(thumbnailBlob);
} }
async function thumbnailCanvasToBlob(canvas: HTMLCanvasElement) { async function getCompressedThumbnailBlobFromCanvas(canvas: HTMLCanvasElement) {
let thumbnailBlob: Blob = null; let thumbnailBlob: Blob = null;
let prevSize = Number.MAX_SAFE_INTEGER; let prevSize = Number.MAX_SAFE_INTEGER;
let quality = MAX_QUALITY; let quality = MAX_QUALITY;
@ -234,13 +276,6 @@ async function thumbnailCanvasToBlob(canvas: HTMLCanvasElement) {
percentageSizeDiff(thumbnailBlob.size, prevSize) >= percentageSizeDiff(thumbnailBlob.size, prevSize) >=
MIN_COMPRESSION_PERCENTAGE_SIZE_DIFF MIN_COMPRESSION_PERCENTAGE_SIZE_DIFF
); );
if (thumbnailBlob.size > MAX_THUMBNAIL_SIZE) {
logError(
Error('thumbnail_too_large'),
'thumbnail greater than max limit',
{ thumbnailSize: convertBytesToHumanReadable(thumbnailBlob.size) }
);
}
return thumbnailBlob; return thumbnailBlob;
} }

View file

@ -1,6 +1,5 @@
import { getLocalFiles } from '../fileService'; import { getLocalFiles } from '../fileService';
import { SetFiles } from 'types/gallery'; import { SetFiles } from 'types/gallery';
import { getDedicatedCryptoWorker } from 'utils/crypto';
import { import {
sortFiles, sortFiles,
preservePhotoswipeProps, preservePhotoswipeProps,
@ -18,23 +17,15 @@ import UIService from './uiService';
import UploadService from './uploadService'; import UploadService from './uploadService';
import { CustomError } from 'utils/error'; import { CustomError } from 'utils/error';
import { Collection } from 'types/collection'; import { Collection } from 'types/collection';
import { EnteFile } from 'types/file'; import { EncryptedEnteFile, EnteFile } from 'types/file';
import { import {
ElectronFile,
FileWithCollection, FileWithCollection,
Metadata,
MetadataAndFileTypeInfo,
MetadataAndFileTypeInfoMap,
ParsedMetadataJSON, ParsedMetadataJSON,
ParsedMetadataJSONMap, ParsedMetadataJSONMap,
PublicUploadProps,
} from 'types/upload'; } from 'types/upload';
import {
UPLOAD_RESULT, import { UPLOAD_RESULT, UPLOAD_STAGES } from 'constants/upload';
MAX_FILE_SIZE_SUPPORTED,
UPLOAD_STAGES,
} from 'constants/upload';
import { ComlinkWorker } from 'utils/comlink';
import { FILE_TYPE } from 'constants/file';
import uiService from './uiService'; import uiService from './uiService';
import { addLogLine, getFileNameSize } from 'utils/logging'; import { addLogLine, getFileNameSize } from 'utils/logging';
import isElectron from 'is-electron'; import isElectron from 'is-electron';
@ -42,14 +33,23 @@ import ImportService from 'services/importService';
import watchFolderService from 'services/watchFolder/watchFolderService'; import watchFolderService from 'services/watchFolder/watchFolderService';
import { ProgressUpdater } from 'types/upload/ui'; import { ProgressUpdater } from 'types/upload/ui';
import uploadCancelService from './uploadCancelService'; import uploadCancelService from './uploadCancelService';
import { DedicatedCryptoWorker } from 'worker/crypto.worker';
import { ComlinkWorker } from 'utils/comlink/comlinkWorker';
import { Remote } from 'comlink';
import {
getLocalPublicFiles,
getPublicCollectionUID,
} from 'services/publicCollectionService';
import { getDedicatedCryptoWorker } from 'utils/comlink/ComlinkCryptoWorker';
const MAX_CONCURRENT_UPLOADS = 4; const MAX_CONCURRENT_UPLOADS = 4;
const FILE_UPLOAD_COMPLETED = 100; const FILE_UPLOAD_COMPLETED = 100;
class UploadManager { class UploadManager {
private cryptoWorkers = new Array<ComlinkWorker>(MAX_CONCURRENT_UPLOADS); private cryptoWorkers = new Array<
ComlinkWorker<typeof DedicatedCryptoWorker>
>(MAX_CONCURRENT_UPLOADS);
private parsedMetadataJSONMap: ParsedMetadataJSONMap; private parsedMetadataJSONMap: ParsedMetadataJSONMap;
private metadataAndFileTypeInfoMap: MetadataAndFileTypeInfoMap;
private filesToBeUploaded: FileWithCollection[]; private filesToBeUploaded: FileWithCollection[];
private remainingFiles: FileWithCollection[] = []; private remainingFiles: FileWithCollection[] = [];
private failedFiles: FileWithCollection[]; private failedFiles: FileWithCollection[];
@ -58,12 +58,18 @@ class UploadManager {
private setFiles: SetFiles; private setFiles: SetFiles;
private collections: Map<number, Collection>; private collections: Map<number, Collection>;
private uploadInProgress: boolean; private uploadInProgress: boolean;
private publicUploadProps: PublicUploadProps;
private uploaderName: string;
public async init(progressUpdater: ProgressUpdater, setFiles: SetFiles) { public async init(
UIService.init(progressUpdater); progressUpdater: ProgressUpdater,
this.setFiles = setFiles; setFiles: SetFiles,
publicCollectProps: PublicUploadProps
) {
UIService.init(progressUpdater); UIService.init(progressUpdater);
UploadService.init(publicCollectProps);
this.setFiles = setFiles; this.setFiles = setFiles;
this.publicUploadProps = publicCollectProps;
} }
public isUploadRunning() { public isUploadRunning() {
@ -75,10 +81,8 @@ class UploadManager {
this.remainingFiles = []; this.remainingFiles = [];
this.failedFiles = []; this.failedFiles = [];
this.parsedMetadataJSONMap = new Map<string, ParsedMetadataJSON>(); this.parsedMetadataJSONMap = new Map<string, ParsedMetadataJSON>();
this.metadataAndFileTypeInfoMap = new Map<
number, this.uploaderName = null;
MetadataAndFileTypeInfo
>();
} }
prepareForNewUpload() { prepareForNewUpload() {
@ -89,10 +93,17 @@ class UploadManager {
} }
async updateExistingFilesAndCollections(collections: Collection[]) { async updateExistingFilesAndCollections(collections: Collection[]) {
this.existingFiles = await getLocalFiles(); if (this.publicUploadProps.accessedThroughSharedURL) {
this.userOwnedNonTrashedExistingFiles = getUserOwnedNonTrashedFiles( this.existingFiles = await getLocalPublicFiles(
this.existingFiles getPublicCollectionUID(this.publicUploadProps.token)
); );
this.userOwnedNonTrashedExistingFiles = this.existingFiles;
} else {
this.existingFiles = await getLocalFiles();
this.userOwnedNonTrashedExistingFiles = getUserOwnedNonTrashedFiles(
this.existingFiles
);
}
this.collections = new Map( this.collections = new Map(
collections.map((collection) => [collection.id, collection]) collections.map((collection) => [collection.id, collection])
); );
@ -100,7 +111,8 @@ class UploadManager {
public async queueFilesForUpload( public async queueFilesForUpload(
filesWithCollectionToUploadIn: FileWithCollection[], filesWithCollectionToUploadIn: FileWithCollection[],
collections: Collection[] collections: Collection[],
uploaderName?: string
) { ) {
try { try {
if (this.uploadInProgress) { if (this.uploadInProgress) {
@ -108,6 +120,7 @@ class UploadManager {
} }
this.uploadInProgress = true; this.uploadInProgress = true;
await this.updateExistingFilesAndCollections(collections); await this.updateExistingFilesAndCollections(collections);
this.uploaderName = uploaderName;
addLogLine( addLogLine(
`received ${filesWithCollectionToUploadIn.length} files to upload` `received ${filesWithCollectionToUploadIn.length} files to upload`
); );
@ -134,50 +147,18 @@ class UploadManager {
); );
} }
if (mediaFiles.length) { if (mediaFiles.length) {
UIService.setUploadStage(UPLOAD_STAGES.EXTRACTING_METADATA);
await this.extractMetadataFromFiles(mediaFiles);
UploadService.setMetadataAndFileTypeInfoMap(
this.metadataAndFileTypeInfoMap
);
// filter out files whose metadata detection failed or those that have been skipped because the files are too large,
// as they will be rejected during upload and are not valid upload files which we need to clustering
const rejectedFileLocalIDs = new Set(
[...this.metadataAndFileTypeInfoMap.entries()].map(
([localID, metadataAndFileTypeInfo]) => {
if (
!metadataAndFileTypeInfo.metadata ||
!metadataAndFileTypeInfo.fileTypeInfo
) {
return localID;
}
}
)
);
const rejectedFiles = [];
const filesWithMetadata = [];
mediaFiles.forEach((m) => {
if (rejectedFileLocalIDs.has(m.localID)) {
rejectedFiles.push(m);
} else {
filesWithMetadata.push(m);
}
});
addLogLine(`clusterLivePhotoFiles started`); addLogLine(`clusterLivePhotoFiles started`);
const analysedMediaFiles = const analysedMediaFiles =
UploadService.clusterLivePhotoFiles(filesWithMetadata); await UploadService.clusterLivePhotoFiles(mediaFiles);
addLogLine(`clusterLivePhotoFiles ended`); addLogLine(`clusterLivePhotoFiles ended`);
const allFiles = [...rejectedFiles, ...analysedMediaFiles];
addLogLine( addLogLine(
`got live photos: ${mediaFiles.length !== allFiles.length}` `got live photos: ${
mediaFiles.length !== analysedMediaFiles.length
}`
); );
uiService.setFilenames( uiService.setFilenames(
new Map<number, string>( new Map<number, string>(
allFiles.map((mediaFile) => [ analysedMediaFiles.map((mediaFile) => [
mediaFile.localID, mediaFile.localID,
UploadService.getAssetName(mediaFile), UploadService.getAssetName(mediaFile),
]) ])
@ -185,10 +166,10 @@ class UploadManager {
); );
UIService.setHasLivePhoto( UIService.setHasLivePhoto(
mediaFiles.length !== allFiles.length mediaFiles.length !== analysedMediaFiles.length
); );
await this.uploadMediaFiles(allFiles); await this.uploadMediaFiles(analysedMediaFiles);
} }
} catch (e) { } catch (e) {
if (e.message === CustomError.UPLOAD_CANCELLED) { if (e.message === CustomError.UPLOAD_CANCELLED) {
@ -203,7 +184,7 @@ class UploadManager {
UIService.setUploadStage(UPLOAD_STAGES.FINISH); UIService.setUploadStage(UPLOAD_STAGES.FINISH);
UIService.setPercentComplete(FILE_UPLOAD_COMPLETED); UIService.setPercentComplete(FILE_UPLOAD_COMPLETED);
for (let i = 0; i < MAX_CONCURRENT_UPLOADS; i++) { for (let i = 0; i < MAX_CONCURRENT_UPLOADS; i++) {
this.cryptoWorkers[i]?.worker.terminate(); this.cryptoWorkers[i]?.terminate();
} }
this.uploadInProgress = false; this.uploadInProgress = false;
} }
@ -273,99 +254,6 @@ class UploadManager {
} }
} }
private async extractMetadataFromFiles(mediaFiles: FileWithCollection[]) {
try {
addLogLine(`extractMetadataFromFiles executed`);
UIService.reset(mediaFiles.length);
for (const { file, localID, collectionID } of mediaFiles) {
UIService.setFileProgress(localID, 0);
if (uploadCancelService.isUploadCancelationRequested()) {
throw Error(CustomError.UPLOAD_CANCELLED);
}
let fileTypeInfo = null;
let metadata = null;
let filePath = null;
try {
addLogLine(
`metadata extraction started ${getFileNameSize(file)} `
);
const result = await this.extractFileTypeAndMetadata(
file,
collectionID
);
fileTypeInfo = result.fileTypeInfo;
metadata = result.metadata;
filePath = result.filePath;
addLogLine(
`metadata extraction successful${getFileNameSize(
file
)} `
);
} catch (e) {
if (e.message === CustomError.UPLOAD_CANCELLED) {
throw e;
} else {
// and don't break for subsequent files just log and move on
logError(e, 'extractFileTypeAndMetadata failed');
addLogLine(
`metadata extraction failed ${getFileNameSize(
file
)} error: ${e.message}`
);
}
}
this.metadataAndFileTypeInfoMap.set(localID, {
fileTypeInfo: fileTypeInfo && { ...fileTypeInfo },
metadata: metadata && { ...metadata },
filePath: filePath,
});
UIService.removeFromInProgressList(localID);
UIService.increaseFileUploaded();
}
} catch (e) {
if (e.message !== CustomError.UPLOAD_CANCELLED) {
logError(e, 'error extracting metadata');
}
throw e;
}
}
private async extractFileTypeAndMetadata(
file: File | ElectronFile,
collectionID: number
) {
if (file.size >= MAX_FILE_SIZE_SUPPORTED) {
addLogLine(
`${getFileNameSize(file)} rejected because of large size`
);
return { fileTypeInfo: null, metadata: null };
}
const fileTypeInfo = await UploadService.getFileType(file);
if (fileTypeInfo.fileType === FILE_TYPE.OTHERS) {
addLogLine(
`${getFileNameSize(
file
)} rejected because of unknown file format`
);
return { fileTypeInfo, metadata: null };
}
addLogLine(` extracting ${getFileNameSize(file)} metadata`);
let metadata: Metadata;
try {
metadata = await UploadService.extractFileMetadata(
file,
collectionID,
fileTypeInfo
);
const filePath = (file as any).path as string;
return { fileTypeInfo, metadata, filePath };
} catch (e) {
logError(e, 'failed to extract file metadata');
return { fileTypeInfo, metadata: null, filePath: null };
}
}
private async uploadMediaFiles(mediaFiles: FileWithCollection[]) { private async uploadMediaFiles(mediaFiles: FileWithCollection[]) {
addLogLine(`uploadMediaFiles called`); addLogLine(`uploadMediaFiles called`);
this.filesToBeUploaded = [...this.filesToBeUploaded, ...mediaFiles]; this.filesToBeUploaded = [...this.filesToBeUploaded, ...mediaFiles];
@ -386,21 +274,14 @@ class UploadManager {
i < MAX_CONCURRENT_UPLOADS && this.filesToBeUploaded.length > 0; i < MAX_CONCURRENT_UPLOADS && this.filesToBeUploaded.length > 0;
i++ i++
) { ) {
const cryptoWorker = getDedicatedCryptoWorker(); this.cryptoWorkers[i] = getDedicatedCryptoWorker();
if (!cryptoWorker) { const worker = await new this.cryptoWorkers[i].remote();
throw Error(CustomError.FAILED_TO_LOAD_WEB_WORKER); uploadProcesses.push(this.uploadNextFileInQueue(worker));
}
this.cryptoWorkers[i] = cryptoWorker;
uploadProcesses.push(
this.uploadNextFileInQueue(
await new this.cryptoWorkers[i].comlink()
)
);
} }
await Promise.all(uploadProcesses); await Promise.all(uploadProcesses);
} }
private async uploadNextFileInQueue(worker: any) { private async uploadNextFileInQueue(worker: Remote<DedicatedCryptoWorker>) {
while (this.filesToBeUploaded.length > 0) { while (this.filesToBeUploaded.length > 0) {
if (uploadCancelService.isUploadCancelationRequested()) { if (uploadCancelService.isUploadCancelationRequested()) {
throw Error(CustomError.UPLOAD_CANCELLED); throw Error(CustomError.UPLOAD_CANCELLED);
@ -412,7 +293,9 @@ class UploadManager {
const { fileUploadResult, uploadedFile } = await uploader( const { fileUploadResult, uploadedFile } = await uploader(
worker, worker,
this.userOwnedNonTrashedExistingFiles, this.userOwnedNonTrashedExistingFiles,
fileWithCollection fileWithCollection,
this.uploaderName,
this.publicUploadProps?.accessedThroughSharedURL
); );
const finalUploadResult = await this.postUploadTask( const finalUploadResult = await this.postUploadTask(
@ -431,7 +314,7 @@ class UploadManager {
async postUploadTask( async postUploadTask(
fileUploadResult: UPLOAD_RESULT, fileUploadResult: UPLOAD_RESULT,
uploadedFile: EnteFile | null, uploadedFile: EncryptedEnteFile | EnteFile | null,
fileWithCollection: FileWithCollection fileWithCollection: FileWithCollection
) { ) {
try { try {
@ -446,22 +329,23 @@ class UploadManager {
this.failedFiles.push(fileWithCollection); this.failedFiles.push(fileWithCollection);
break; break;
case UPLOAD_RESULT.ALREADY_UPLOADED: case UPLOAD_RESULT.ALREADY_UPLOADED:
decryptedFile = uploadedFile; decryptedFile = uploadedFile as EnteFile;
break; break;
case UPLOAD_RESULT.ADDED_SYMLINK: case UPLOAD_RESULT.ADDED_SYMLINK:
decryptedFile = uploadedFile; decryptedFile = uploadedFile as EnteFile;
fileUploadResult = UPLOAD_RESULT.UPLOADED; fileUploadResult = UPLOAD_RESULT.UPLOADED;
break; break;
case UPLOAD_RESULT.UPLOADED: case UPLOAD_RESULT.UPLOADED:
case UPLOAD_RESULT.UPLOADED_WITH_STATIC_THUMBNAIL: case UPLOAD_RESULT.UPLOADED_WITH_STATIC_THUMBNAIL:
decryptedFile = await decryptFile( decryptedFile = await decryptFile(
uploadedFile, uploadedFile as EncryptedEnteFile,
fileWithCollection.collection.key fileWithCollection.collection.key
); );
break; break;
case UPLOAD_RESULT.UNSUPPORTED: case UPLOAD_RESULT.UNSUPPORTED:
case UPLOAD_RESULT.TOO_LARGE: case UPLOAD_RESULT.TOO_LARGE:
case UPLOAD_RESULT.CANCELLED: case UPLOAD_RESULT.CANCELLED:
case UPLOAD_RESULT.SKIPPED_VIDEOS:
// no-op // no-op
break; break;
default: default:
@ -479,7 +363,7 @@ class UploadManager {
await this.watchFolderCallback( await this.watchFolderCallback(
fileUploadResult, fileUploadResult,
fileWithCollection, fileWithCollection,
uploadedFile uploadedFile as EncryptedEnteFile
); );
return fileUploadResult; return fileUploadResult;
} catch (e) { } catch (e) {
@ -491,7 +375,7 @@ class UploadManager {
private async watchFolderCallback( private async watchFolderCallback(
fileUploadResult: UPLOAD_RESULT, fileUploadResult: UPLOAD_RESULT,
fileWithCollection: FileWithCollection, fileWithCollection: FileWithCollection,
uploadedFile: EnteFile uploadedFile: EncryptedEnteFile
) { ) {
if (isElectron()) { if (isElectron()) {
await watchFolderService.onFileUpload( await watchFolderService.onFileUpload(
@ -508,13 +392,17 @@ class UploadManager {
uploadCancelService.requestUploadCancelation(); uploadCancelService.requestUploadCancelation();
} }
async getFailedFilesWithCollections() { getFailedFilesWithCollections() {
return { return {
files: this.failedFiles, files: this.failedFiles,
collections: [...this.collections.values()], collections: [...this.collections.values()],
}; };
} }
getUploaderName() {
return this.uploaderName;
}
private updateExistingFiles(decryptedFile: EnteFile) { private updateExistingFiles(decryptedFile: EnteFile) {
if (!decryptedFile) { if (!decryptedFile) {
throw Error("decrypted file can't be undefined"); throw Error("decrypted file can't be undefined");

View file

@ -5,26 +5,25 @@ import { extractFileMetadata, getFilename } from './fileService';
import { getFileType } from '../typeDetectionService'; import { getFileType } from '../typeDetectionService';
import { CustomError, handleUploadError } from 'utils/error'; import { CustomError, handleUploadError } from 'utils/error';
import { import {
B64EncryptionResult,
BackupedFile, BackupedFile,
ElectronFile,
EncryptedFile, EncryptedFile,
FileTypeInfo, FileTypeInfo,
FileWithCollection, FileWithCollection,
FileWithMetadata, FileWithMetadata,
isDataStream, isDataStream,
Metadata, Metadata,
MetadataAndFileTypeInfo,
MetadataAndFileTypeInfoMap,
ParsedMetadataJSON, ParsedMetadataJSON,
ParsedMetadataJSONMap, ParsedMetadataJSONMap,
ProcessedFile, ProcessedFile,
PublicUploadProps,
UploadAsset, UploadAsset,
UploadFile, UploadFile,
UploadURL, UploadURL,
} from 'types/upload'; } from 'types/upload';
import { import {
clusterLivePhotoFiles, clusterLivePhotoFiles,
extractLivePhotoMetadata,
getLivePhotoFileType,
getLivePhotoName, getLivePhotoName,
getLivePhotoSize, getLivePhotoSize,
readLivePhoto, readLivePhoto,
@ -33,6 +32,12 @@ import { encryptFile, getFileSize, readFile } from './fileService';
import { uploadStreamUsingMultipart } from './multiPartUploadService'; import { uploadStreamUsingMultipart } from './multiPartUploadService';
import UIService from './uiService'; import UIService from './uiService';
import { USE_CF_PROXY } from 'constants/upload'; import { USE_CF_PROXY } from 'constants/upload';
import { Remote } from 'comlink';
import { DedicatedCryptoWorker } from 'worker/crypto.worker';
import publicUploadHttpClient from './publicUploadHttpClient';
import { constructPublicMagicMetadata } from './magicMetadataService';
import { FilePublicMagicMetadataProps } from 'types/magicMetadata';
import { B64EncryptionResult } from 'types/crypto';
class UploadService { class UploadService {
private uploadURLs: UploadURL[] = []; private uploadURLs: UploadURL[] = [];
@ -40,26 +45,32 @@ class UploadService {
string, string,
ParsedMetadataJSON ParsedMetadataJSON
>(); >();
private metadataAndFileTypeInfoMap: MetadataAndFileTypeInfoMap = new Map<
number, private uploaderName: string;
MetadataAndFileTypeInfo
>();
private pendingUploadCount: number = 0; private pendingUploadCount: number = 0;
private publicUploadProps: PublicUploadProps = undefined;
init(publicUploadProps: PublicUploadProps) {
this.publicUploadProps = publicUploadProps;
}
async setFileCount(fileCount: number) { async setFileCount(fileCount: number) {
this.pendingUploadCount = fileCount; this.pendingUploadCount = fileCount;
await this.preFetchUploadURLs(); this.preFetchUploadURLs();
} }
setParsedMetadataJSONMap(parsedMetadataJSONMap: ParsedMetadataJSONMap) { setParsedMetadataJSONMap(parsedMetadataJSONMap: ParsedMetadataJSONMap) {
this.parsedMetadataJSONMap = parsedMetadataJSONMap; this.parsedMetadataJSONMap = parsedMetadataJSONMap;
} }
setMetadataAndFileTypeInfoMap( setUploaderName(uploaderName: string) {
metadataAndFileTypeInfoMap: MetadataAndFileTypeInfoMap this.uploaderName = uploaderName;
) { }
this.metadataAndFileTypeInfoMap = metadataAndFileTypeInfoMap;
getUploaderName() {
return this.uploaderName;
} }
reducePendingUploadCount() { reducePendingUploadCount() {
@ -72,14 +83,16 @@ class UploadService {
: getFileSize(file); : getFileSize(file);
} }
getAssetName({ isLivePhoto, file, livePhotoAssets }: FileWithCollection) { getAssetName({ isLivePhoto, file, livePhotoAssets }: UploadAsset) {
return isLivePhoto return isLivePhoto
? getLivePhotoName(livePhotoAssets.image.name) ? getLivePhotoName(livePhotoAssets)
: getFilename(file); : getFilename(file);
} }
async getFileType(file: File | ElectronFile) { getAssetFileType({ isLivePhoto, file, livePhotoAssets }: UploadAsset) {
return getFileType(file); return isLivePhoto
? getLivePhotoFileType(livePhotoAssets)
: getFileType(file);
} }
async readAsset( async readAsset(
@ -91,39 +104,41 @@ class UploadService {
: await readFile(fileTypeInfo, file); : await readFile(fileTypeInfo, file);
} }
async extractFileMetadata( async extractAssetMetadata(
file: File | ElectronFile, worker,
{ isLivePhoto, file, livePhotoAssets }: UploadAsset,
collectionID: number, collectionID: number,
fileTypeInfo: FileTypeInfo fileTypeInfo: FileTypeInfo
): Promise<Metadata> { ): Promise<Metadata> {
return extractFileMetadata( return isLivePhoto
this.parsedMetadataJSONMap, ? extractLivePhotoMetadata(
file, worker,
collectionID, this.parsedMetadataJSONMap,
fileTypeInfo collectionID,
); fileTypeInfo,
} livePhotoAssets
)
getFileMetadataAndFileTypeInfo(localID: number) { : await extractFileMetadata(
return this.metadataAndFileTypeInfoMap.get(localID); worker,
} this.parsedMetadataJSONMap,
collectionID,
setFileMetadataAndFileTypeInfo( fileTypeInfo,
localID: number, file
metadataAndFileTypeInfo: MetadataAndFileTypeInfo );
) {
return this.metadataAndFileTypeInfoMap.set(
localID,
metadataAndFileTypeInfo
);
} }
clusterLivePhotoFiles(mediaFiles: FileWithCollection[]) { clusterLivePhotoFiles(mediaFiles: FileWithCollection[]) {
return clusterLivePhotoFiles(mediaFiles); return clusterLivePhotoFiles(mediaFiles);
} }
constructPublicMagicMetadata(
publicMagicMetadataProps: FilePublicMagicMetadataProps
) {
return constructPublicMagicMetadata(publicMagicMetadataProps);
}
async encryptAsset( async encryptAsset(
worker: any, worker: Remote<DedicatedCryptoWorker>,
file: FileWithMetadata, file: FileWithMetadata,
encryptionKey: string encryptionKey: string
): Promise<EncryptedFile> { ): Promise<EncryptedFile> {
@ -146,13 +161,13 @@ class UploadService {
if (USE_CF_PROXY) { if (USE_CF_PROXY) {
fileObjectKey = await UploadHttpClient.putFileV2( fileObjectKey = await UploadHttpClient.putFileV2(
fileUploadURL, fileUploadURL,
file.file.encryptedData, file.file.encryptedData as Uint8Array,
progressTracker progressTracker
); );
} else { } else {
fileObjectKey = await UploadHttpClient.putFile( fileObjectKey = await UploadHttpClient.putFile(
fileUploadURL, fileUploadURL,
file.file.encryptedData, file.file.encryptedData as Uint8Array,
progressTracker progressTracker
); );
} }
@ -162,13 +177,13 @@ class UploadService {
if (USE_CF_PROXY) { if (USE_CF_PROXY) {
thumbnailObjectKey = await UploadHttpClient.putFileV2( thumbnailObjectKey = await UploadHttpClient.putFileV2(
thumbnailUploadURL, thumbnailUploadURL,
file.thumbnail.encryptedData as Uint8Array, file.thumbnail.encryptedData,
null null
); );
} else { } else {
thumbnailObjectKey = await UploadHttpClient.putFile( thumbnailObjectKey = await UploadHttpClient.putFile(
thumbnailUploadURL, thumbnailUploadURL,
file.thumbnail.encryptedData as Uint8Array, file.thumbnail.encryptedData,
null null
); );
} }
@ -183,6 +198,7 @@ class UploadService {
objectKey: thumbnailObjectKey, objectKey: thumbnailObjectKey,
}, },
metadata: file.metadata, metadata: file.metadata,
pubMagicMetadata: file.pubMagicMetadata,
}; };
return backupedFile; return backupedFile;
} catch (e) { } catch (e) {
@ -225,11 +241,32 @@ class UploadService {
} }
} }
async uploadFile(uploadFile: UploadFile) {
if (this.publicUploadProps.accessedThroughSharedURL) {
return publicUploadHttpClient.uploadFile(
uploadFile,
this.publicUploadProps.token,
this.publicUploadProps.passwordToken
);
} else {
return UploadHttpClient.uploadFile(uploadFile);
}
}
private async fetchUploadURLs() { private async fetchUploadURLs() {
await UploadHttpClient.fetchUploadURLs( if (this.publicUploadProps.accessedThroughSharedURL) {
this.pendingUploadCount, await publicUploadHttpClient.fetchUploadURLs(
this.uploadURLs this.pendingUploadCount,
); this.uploadURLs,
this.publicUploadProps.token,
this.publicUploadProps.passwordToken
);
} else {
await UploadHttpClient.fetchUploadURLs(
this.pendingUploadCount,
this.uploadURLs
);
}
} }
} }

View file

@ -2,17 +2,26 @@ import { EnteFile } from 'types/file';
import { handleUploadError, CustomError } from 'utils/error'; import { handleUploadError, CustomError } from 'utils/error';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
import { findMatchingExistingFiles } from 'utils/upload'; import { findMatchingExistingFiles } from 'utils/upload';
import UploadHttpClient from './uploadHttpClient';
import UIService from './uiService'; import UIService from './uiService';
import UploadService from './uploadService'; import UploadService from './uploadService';
import { FILE_TYPE } from 'constants/file'; import { FILE_TYPE } from 'constants/file';
import { UPLOAD_RESULT, MAX_FILE_SIZE_SUPPORTED } from 'constants/upload'; import { UPLOAD_RESULT, MAX_FILE_SIZE_SUPPORTED } from 'constants/upload';
import { FileWithCollection, BackupedFile, UploadFile } from 'types/upload'; import {
FileWithCollection,
BackupedFile,
UploadFile,
FileWithMetadata,
FileTypeInfo,
} from 'types/upload';
import { addLocalLog, addLogLine } from 'utils/logging'; import { addLocalLog, addLogLine } from 'utils/logging';
import { convertBytesToHumanReadable } from 'utils/file/size'; import { convertBytesToHumanReadable } from 'utils/file/size';
import { sleep } from 'utils/common'; import { sleep } from 'utils/common';
import { addToCollection } from 'services/collectionService'; import { addToCollection } from 'services/collectionService';
import uploadCancelService from './uploadCancelService'; import uploadCancelService from './uploadCancelService';
import { Remote } from 'comlink';
import { DedicatedCryptoWorker } from 'worker/crypto.worker';
import uploadService from './uploadService';
import { FilePublicMagicMetadata } from 'types/magicMetadata';
interface UploadResponse { interface UploadResponse {
fileUploadResult: UPLOAD_RESULT; fileUploadResult: UPLOAD_RESULT;
@ -20,9 +29,11 @@ interface UploadResponse {
} }
export default async function uploader( export default async function uploader(
worker: any, worker: Remote<DedicatedCryptoWorker>,
existingFiles: EnteFile[], existingFiles: EnteFile[],
fileWithCollection: FileWithCollection fileWithCollection: FileWithCollection,
uploaderName: string,
skipVideos: boolean
): Promise<UploadResponse> { ): Promise<UploadResponse> {
const { collection, localID, ...uploadAsset } = fileWithCollection; const { collection, localID, ...uploadAsset } = fileWithCollection;
const fileNameSize = `${UploadService.getAssetName( const fileNameSize = `${UploadService.getAssetName(
@ -32,20 +43,33 @@ export default async function uploader(
addLogLine(`uploader called for ${fileNameSize}`); addLogLine(`uploader called for ${fileNameSize}`);
UIService.setFileProgress(localID, 0); UIService.setFileProgress(localID, 0);
await sleep(0); await sleep(0);
const { fileTypeInfo, metadata } = let fileTypeInfo: FileTypeInfo;
UploadService.getFileMetadataAndFileTypeInfo(localID);
try { try {
const fileSize = UploadService.getAssetSize(uploadAsset); const fileSize = UploadService.getAssetSize(uploadAsset);
if (fileSize >= MAX_FILE_SIZE_SUPPORTED) { if (fileSize >= MAX_FILE_SIZE_SUPPORTED) {
return { fileUploadResult: UPLOAD_RESULT.TOO_LARGE }; return { fileUploadResult: UPLOAD_RESULT.TOO_LARGE };
} }
addLogLine(`getting filetype for ${fileNameSize}`);
fileTypeInfo = await UploadService.getAssetFileType(uploadAsset);
addLogLine(`got filetype for ${fileNameSize}`);
if (fileTypeInfo.fileType === FILE_TYPE.OTHERS) { if (fileTypeInfo.fileType === FILE_TYPE.OTHERS) {
throw Error(CustomError.UNSUPPORTED_FILE_FORMAT); throw Error(CustomError.UNSUPPORTED_FILE_FORMAT);
} }
if (!metadata) { if (skipVideos && fileTypeInfo.fileType === FILE_TYPE.VIDEO) {
throw Error(CustomError.NO_METADATA); addLogLine(
`skipped video upload for public upload ${fileNameSize}`
);
return { fileUploadResult: UPLOAD_RESULT.SKIPPED_VIDEOS };
} }
addLogLine(`extracting metadata ${fileNameSize}`);
const metadata = await UploadService.extractAssetMetadata(
worker,
uploadAsset,
collection.id,
fileTypeInfo
);
const matchingExistingFiles = findMatchingExistingFiles( const matchingExistingFiles = findMatchingExistingFiles(
existingFiles, existingFiles,
metadata metadata
@ -100,11 +124,18 @@ export default async function uploader(
if (file.hasStaticThumbnail) { if (file.hasStaticThumbnail) {
metadata.hasStaticThumbnail = true; metadata.hasStaticThumbnail = true;
} }
const fileWithMetadata = { let pubMagicMetadata: FilePublicMagicMetadata;
if (uploaderName) {
pubMagicMetadata = await uploadService.constructPublicMagicMetadata(
{ uploaderName }
);
}
const fileWithMetadata: FileWithMetadata = {
localID, localID,
filedata: file.filedata, filedata: file.filedata,
thumbnail: file.thumbnail, thumbnail: file.thumbnail,
metadata, metadata,
pubMagicMetadata,
}; };
if (uploadCancelService.isUploadCancelationRequested()) { if (uploadCancelService.isUploadCancelationRequested()) {
@ -132,7 +163,7 @@ export default async function uploader(
encryptedFile.fileKey encryptedFile.fileKey
); );
const uploadedFile = await UploadHttpClient.uploadFile(uploadFile); const uploadedFile = await UploadService.uploadFile(uploadFile);
UIService.increaseFileUploaded(); UIService.increaseFileUploaded();
addLogLine(`${fileNameSize} successfully uploaded`); addLogLine(`${fileNameSize} successfully uploaded`);

View file

@ -6,7 +6,7 @@ import { clearData, getData, LS_KEYS } from 'utils/storage/localStorage';
import localForage from 'utils/storage/localForage'; import localForage from 'utils/storage/localForage';
import { getToken } from 'utils/common/key'; import { getToken } from 'utils/common/key';
import HTTPService from './HTTPService'; import HTTPService from './HTTPService';
import { B64EncryptionResult } from 'utils/crypto'; import { getRecoveryKey } from 'utils/crypto';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
import { import {
KeyAttributes, KeyAttributes,
@ -23,6 +23,7 @@ import { ServerErrorCodes } from 'utils/error';
import isElectron from 'is-electron'; import isElectron from 'is-electron';
import safeStorageService from './electron/safeStorage'; import safeStorageService from './electron/safeStorage';
import { deleteThumbnailCache } from './cacheService'; import { deleteThumbnailCache } from './cacheService';
import { B64EncryptionResult } from 'types/crypto';
const ENDPOINT = getEndpoint(); const ENDPOINT = getEndpoint();
@ -364,3 +365,13 @@ export const deleteAccount = async (challenge: string) => {
throw e; throw e;
} }
}; };
export const validateKey = async () => {
try {
await getRecoveryKey();
return true;
} catch (e) {
await logoutUser();
return false;
}
};

View file

@ -56,13 +56,15 @@ export class WasmFFmpeg {
let tempOutputFilePath: string; let tempOutputFilePath: string;
try { try {
await this.ready; await this.ready;
tempInputFilePath = `${generateTempName(10)}- ${inputFile.name}`; const extension = getFileExtension(inputFile.name);
const tempNameSuffix = extension ? `input.${extension}` : 'input';
tempInputFilePath = `${generateTempName(10, tempNameSuffix)}`;
this.ffmpeg.FS( this.ffmpeg.FS(
'writeFile', 'writeFile',
tempInputFilePath, tempInputFilePath,
await getUint8ArrayView(inputFile) await getUint8ArrayView(inputFile)
); );
tempOutputFilePath = `${generateTempName(10)}-${outputFileName}`; tempOutputFilePath = `${generateTempName(10, outputFileName)}`;
cmd = cmd.map((cmdPart) => { cmd = cmd.map((cmdPart) => {
if (cmdPart === FFMPEG_PLACEHOLDER) { if (cmdPart === FFMPEG_PLACEHOLDER) {
@ -95,3 +97,11 @@ export class WasmFFmpeg {
} }
} }
} }
function getFileExtension(filename: string) {
const lastDotPosition = filename.lastIndexOf('.');
if (lastDotPosition === -1) return null;
else {
return filename.slice(lastDotPosition + 1);
}
}

View file

@ -1,10 +1,12 @@
import QueueProcessor from 'services/queueProcessor'; import QueueProcessor from 'services/queueProcessor';
import { CustomError } from 'utils/error'; import { CustomError } from 'utils/error';
import { createNewConvertWorker } from 'utils/heicConverter';
import { retryAsyncFunction } from 'utils/network'; import { retryAsyncFunction } from 'utils/network';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
import { addLogLine } from 'utils/logging'; import { addLogLine } from 'utils/logging';
import { makeHumanReadableStorage } from 'utils/billing'; import { DedicatedConvertWorker } from 'worker/convert.worker';
import { ComlinkWorker } from 'utils/comlink/comlinkWorker';
import { convertBytesToHumanReadable } from 'utils/file/size';
import { getDedicatedConvertWorker } from 'utils/comlink/ComlinkConvertWorker';
const WORKER_POOL_SIZE = 2; const WORKER_POOL_SIZE = 2;
const MAX_CONVERSION_IN_PARALLEL = 1; const MAX_CONVERSION_IN_PARALLEL = 1;
@ -17,7 +19,7 @@ class HEICConverter {
private convertProcessor = new QueueProcessor<Blob>( private convertProcessor = new QueueProcessor<Blob>(
MAX_CONVERSION_IN_PARALLEL MAX_CONVERSION_IN_PARALLEL
); );
private workerPool: { comlink: any; worker: Worker }[]; private workerPool: ComlinkWorker<typeof DedicatedConvertWorker>[] = [];
private ready: Promise<void>; private ready: Promise<void>;
constructor() { constructor() {
@ -26,14 +28,15 @@ class HEICConverter {
private async init() { private async init() {
this.workerPool = []; this.workerPool = [];
for (let i = 0; i < WORKER_POOL_SIZE; i++) { for (let i = 0; i < WORKER_POOL_SIZE; i++) {
this.workerPool.push(await createNewConvertWorker()); this.workerPool.push(getDedicatedConvertWorker());
} }
} }
async convert(fileBlob: Blob): Promise<Blob> { async convert(fileBlob: Blob): Promise<Blob> {
await this.ready; await this.ready;
const response = this.convertProcessor.queueUpRequest(() => const response = this.convertProcessor.queueUpRequest(() =>
retryAsyncFunction<Blob>(async () => { retryAsyncFunction<Blob>(async () => {
const { comlink, worker } = this.workerPool.shift(); const convertWorker = this.workerPool.shift();
const worker = await new convertWorker.remote();
try { try {
const convertedHEIC = await new Promise<Blob>( const convertedHEIC = await new Promise<Blob>(
(resolve, reject) => { (resolve, reject) => {
@ -43,15 +46,15 @@ class HEICConverter {
reject(Error('wait time exceeded')); reject(Error('wait time exceeded'));
}, WAIT_TIME_IN_MICROSECONDS); }, WAIT_TIME_IN_MICROSECONDS);
const startTime = Date.now(); const startTime = Date.now();
const convertedHEIC: Blob = const convertedHEIC =
await comlink.convertHEIC( await worker.convertHEIC(
fileBlob, fileBlob,
CONVERT_FORMAT CONVERT_FORMAT
); );
addLogLine( addLogLine(
`originalFileSize:${makeHumanReadableStorage( `originalFileSize:${convertBytesToHumanReadable(
fileBlob?.size fileBlob?.size
)},convertedFileSize:${makeHumanReadableStorage( )},convertedFileSize:${convertBytesToHumanReadable(
convertedHEIC?.size convertedHEIC?.size
)}, heic conversion time: ${ )}, heic conversion time: ${
Date.now() - startTime Date.now() - startTime
@ -71,10 +74,10 @@ class HEICConverter {
Error(`converted heic fileSize is Zero`), Error(`converted heic fileSize is Zero`),
'converted heic fileSize is Zero', 'converted heic fileSize is Zero',
{ {
originalFileSize: makeHumanReadableStorage( originalFileSize: convertBytesToHumanReadable(
fileBlob?.size ?? 0 fileBlob?.size ?? 0
), ),
convertedFileSize: makeHumanReadableStorage( convertedFileSize: convertBytesToHumanReadable(
convertedHEIC?.size ?? 0 convertedHEIC?.size ?? 0
), ),
} }
@ -86,12 +89,12 @@ class HEICConverter {
BREATH_TIME_IN_MICROSECONDS BREATH_TIME_IN_MICROSECONDS
); );
}); });
this.workerPool.push({ comlink, worker }); this.workerPool.push(convertWorker);
return convertedHEIC; return convertedHEIC;
} catch (e) { } catch (e) {
logError(e, 'heic conversion failed'); logError(e, 'heic conversion failed');
worker.terminate(); convertWorker.terminate();
this.workerPool.push(await createNewConvertWorker()); this.workerPool.push(getDedicatedConvertWorker());
throw e; throw e;
} }
}, WAIT_TIME_BEFORE_NEXT_ATTEMPT_IN_MICROSECONDS) }, WAIT_TIME_BEFORE_NEXT_ATTEMPT_IN_MICROSECONDS)

View file

@ -1,5 +1,5 @@
import { Collection } from 'types/collection'; import { Collection } from 'types/collection';
import { EnteFile } from 'types/file'; import { EncryptedEnteFile } from 'types/file';
import { ElectronFile, FileWithCollection } from 'types/upload'; import { ElectronFile, FileWithCollection } from 'types/upload';
import { runningInBrowser } from 'utils/common'; import { runningInBrowser } from 'utils/common';
import { removeFromCollection } from '../collectionService'; import { removeFromCollection } from '../collectionService';
@ -33,7 +33,7 @@ class watchFolderService {
private trashingDirQueue: string[] = []; private trashingDirQueue: string[] = [];
private isEventRunning: boolean = false; private isEventRunning: boolean = false;
private uploadRunning: boolean = false; private uploadRunning: boolean = false;
private filePathToUploadedFileIDMap = new Map<string, EnteFile>(); private filePathToUploadedFileIDMap = new Map<string, EncryptedEnteFile>();
private unUploadableFilePaths = new Set<string>(); private unUploadableFilePaths = new Set<string>();
private isPaused = false; private isPaused = false;
private setElectronFiles: (files: ElectronFile[]) => void; private setElectronFiles: (files: ElectronFile[]) => void;
@ -295,10 +295,10 @@ class watchFolderService {
async onFileUpload( async onFileUpload(
fileUploadResult: UPLOAD_RESULT, fileUploadResult: UPLOAD_RESULT,
fileWithCollection: FileWithCollection, fileWithCollection: FileWithCollection,
file: EnteFile file: EncryptedEnteFile
) { ) {
addLocalLog(() => `onFileUpload called`); addLocalLog(() => `onFileUpload called`);
if (!this.isUploadRunning) { if (!this.isUploadRunning()) {
return; return;
} }
if ( if (

Some files were not shown because too many files have changed in this diff Show more