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/exhaustive-deps": "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
. "$(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.
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/>
![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
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`
3. Install dependencies with `yarn install`
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 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/>
@ -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)

View file

@ -26,7 +26,7 @@ module.exports = {
'style-src': "'self' 'unsafe-inline'",
'font-src ': "'self'; script-src 'self' 'unsafe-eval' blob:",
'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'",
// to allow worker
'child-src': "'self' blob:",

View file

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

View file

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

View file

@ -8,5 +8,5 @@
X-Frame-Options: deny
X-XSS-Protection: 1; mode=block
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}>
{constants.SORT_BY_NAME}
</SortByOption>
<SortByOption sortBy={COLLECTION_SORT_BY.CREATION_TIME_DESCENDING}>
{constants.SORT_BY_CREATION_TIME_DESCENDING}
</SortByOption>
<SortByOption sortBy={COLLECTION_SORT_BY.CREATION_TIME_ASCENDING}>
{constants.SORT_BY_CREATION_TIME_ASCENDING}
</SortByOption>

View file

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

View file

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

View file

@ -11,7 +11,9 @@ import TruncateText from 'components/TruncateText';
import { Box } from '@mui/material';
import { CollectionSummaryType } from 'constants/collection';
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 {
active: boolean;
@ -50,7 +52,20 @@ function CollectionCardIcon({ collectionType }) {
<CollectionBarTileIcon>
{collectionType === CollectionSummaryType.favorites && <Favorite />}
{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>
);

View file

@ -2,12 +2,12 @@ import { OverflowMenuOption } from 'components/OverflowMenu/option';
import React from 'react';
import EditIcon from '@mui/icons-material/Edit';
import IosShareIcon from '@mui/icons-material/IosShare';
import FileDownloadOutlinedIcon from '@mui/icons-material/FileDownloadOutlined';
import PeopleIcon from '@mui/icons-material/People';
import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined';
import constants from 'utils/strings/constants';
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 {
IsArchived: boolean;
@ -23,29 +23,13 @@ export function AlbumCollectionOption({
}: Iprops) {
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
onClick={handleCollectionAction(
CollectionActions.SHOW_RENAME_DIALOG,
false
)}
startIcon={<EditIcon />}>
{constants.RENAME}
{constants.RENAME_COLLECTION}
</OverflowMenuOption>
{IsArchived ? (
<OverflowMenuOption
@ -53,13 +37,13 @@ export function AlbumCollectionOption({
CollectionActions.UNARCHIVE
)}
startIcon={<Unarchive />}>
{constants.UNARCHIVE}
{constants.UNARCHIVE_COLLECTION}
</OverflowMenuOption>
) : (
<OverflowMenuOption
onClick={handleCollectionAction(CollectionActions.ARCHIVE)}
startIcon={<ArchiveOutlined />}>
{constants.ARCHIVE}
{constants.ARCHIVE_COLLECTION}
</OverflowMenuOption>
)}
<OverflowMenuOption
@ -68,7 +52,15 @@ export function AlbumCollectionOption({
CollectionActions.CONFIRM_DELETE,
false
)}>
{constants.DELETE}
{constants.DELETE_COLLECTION}
</OverflowMenuOption>
<OverflowMenuOption
onClick={handleCollectionAction(
CollectionActions.SHOW_SHARE_DIALOG,
false
)}
startIcon={<PeopleIcon />}>
{constants.SHARE_COLLECTION}
</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 { CollectionSummaryType } from 'constants/collection';
import { TrashCollectionOption } from './TrashCollectionOption';
import { SharedCollectionOption } from './SharedCollectionOption';
import { QuickOptions } from './QuickOptions';
import MoreHoriz from '@mui/icons-material/MoreHoriz';
import { HorizontalFlex } from 'components/Container';
interface CollectionOptionsProps {
setCollectionNamerAttributes: SetCollectionNamerAttributes;
@ -39,6 +42,8 @@ export enum CollectionActions {
SHOW_SHARE_DIALOG,
CONFIRM_EMPTY_TRASH,
EMPTY_TRASH,
CONFIRM_LEAVE_SHARED_ALBUM,
LEAVE_SHARED_ALBUM,
}
const CollectionOptions = (props: CollectionOptionsProps) => {
@ -93,6 +98,12 @@ const CollectionOptions = (props: CollectionOptionsProps) => {
case CollectionActions.EMPTY_TRASH:
callback = emptyTrash;
break;
case CollectionActions.CONFIRM_LEAVE_SHARED_ALBUM:
callback = confirmLeaveSharedAlbum;
break;
case CollectionActions.LEAVE_SHARED_ALBUM:
callback = leaveSharedAlbum;
break;
default:
logError(
Error('invalid collection action '),
@ -130,6 +141,11 @@ const CollectionOptions = (props: CollectionOptionsProps) => {
redirectToAll();
};
const leaveSharedAlbum = async () => {
await CollectionAPI.leaveSharedAlbum(activeCollection.id);
redirectToAll();
};
const archiveCollection = () => {
changeCollectionVisibility(activeCollection, VISIBILITY_STATE.ARCHIVED);
};
@ -174,7 +190,7 @@ const CollectionOptions = (props: CollectionOptionsProps) => {
const confirmDownloadCollection = () => {
setDialogMessage({
title: constants.CONFIRM_DOWNLOAD_COLLECTION,
title: constants.DOWNLOAD_COLLECTION,
content: constants.DOWNLOAD_COLLECTION_MESSAGE(),
proceed: {
text: constants.DOWNLOAD,
@ -200,26 +216,56 @@ const CollectionOptions = (props: CollectionOptionsProps) => {
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 (
<OverflowMenu
ariaControls={'collection-options'}
triggerButtonIcon={<MoreHoriz />}
triggerButtonProps={{
sx: {
background: (theme) => theme.palette.fill.dark,
},
}}>
{collectionSummaryType === CollectionSummaryType.trash ? (
<TrashCollectionOption
handleCollectionAction={handleCollectionAction}
/>
) : (
<AlbumCollectionOption
IsArchived={IsArchived(activeCollection)}
handleCollectionAction={handleCollectionAction}
/>
<HorizontalFlex sx={{ display: 'inline-flex', gap: '16px' }}>
<QuickOptions
handleCollectionAction={handleCollectionAction}
collectionSummaryType={collectionSummaryType}
/>
{!(collectionSummaryType === CollectionSummaryType.favorites) && (
<OverflowMenu
ariaControls={'collection-options'}
triggerButtonIcon={<MoreHoriz />}
triggerButtonProps={{
sx: {
background: (theme) => theme.palette.fill.dark,
},
}}>
{collectionSummaryType === CollectionSummaryType.trash ? (
<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;
title: string;
fromCollection?: number;
onCancel?: () => void;
}
interface Props {
open: boolean;
onClose: (closeBtnClick?: boolean) => void;
onClose: () => void;
attributes: CollectionSelectorAttributes;
collections: Collection[];
collectionSummaries: CollectionSummaries;
@ -61,15 +62,18 @@ function CollectionSelector({
props.onClose();
};
const onCloseButtonClick = () => props.onClose(true);
const onUserTriggeredClose = () => {
attributes.onCancel?.();
props.onClose();
};
return (
<AllCollectionDialog
onClose={props.onClose}
onClose={onUserTriggeredClose}
open={props.open}
position="center"
fullScreen={appContext.isMobile}>
<DialogTitleWithCloseButton onClose={onCloseButtonClick}>
<DialogTitleWithCloseButton onClose={onUserTriggeredClose}>
{attributes.title}
</DialogTitleWithCloseButton>
<DialogContent>

View file

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

View file

@ -1,13 +1,21 @@
import { Box, Typography } from '@mui/material';
import { AppContext } from 'pages/_app';
import React, { useContext } 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 ManageDownloadAccess({
publicShareProp,
updatePublicShareURLHelper,
collection,
}) {
}: Iprops) {
const appContext = useContext(AppContext);
const handleFileDownloadSetting = () => {
@ -41,7 +49,7 @@ export function ManageDownloadAccess({
<Box>
<Typography mb={0.5}>{constants.FILE_DOWNLOAD}</Typography>
<PublicShareSwitch
checked={publicShareProp?.enableDownload ?? false}
checked={publicShareProp?.enableDownload ?? true}
onChange={handleFileDownloadSetting}
/>
</Box>

View file

@ -1,12 +1,11 @@
import { ManageLinkPassword } from './linkPassword';
import { ManageDeviceLimit } from './deviceLimit';
import { ManageLinkExpiry } from './linkExpiry';
import { PublicLinkSetPassword } from '../setPassword';
import { Stack, Typography } from '@mui/material';
import { GalleryContext } from 'pages/gallery';
import React, { useContext, useState } from 'react';
import { updateShareableURL } from 'services/collectionService';
import { UpdatePublicURL } from 'types/collection';
import { Collection, PublicURL, UpdatePublicURL } from 'types/collection';
import { sleep } from 'utils/common';
import constants from 'utils/strings/constants';
import {
@ -15,19 +14,24 @@ import {
} from '../../styledComponents';
import { ManageDownloadAccess } from './downloadAccess';
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({
publicShareProp,
collection,
setPublicShareProp,
}) {
}: Iprops) {
const galleryContext = useContext(GalleryContext);
const [changePasswordView, setChangePasswordView] = useState(false);
const [sharableLinkError, setSharableLinkError] = useState(null);
const closeConfigurePassword = () => setChangePasswordView(false);
const updatePublicShareURLHelper = async (req: UpdatePublicURL) => {
try {
galleryContext.setBlockingLoad(true);
@ -73,6 +77,13 @@ export default function PublicShareManage({
updatePublicShareURLHelper
}
/>
<ManagePublicCollect
collection={collection}
publicShareProp={publicShareProp}
updatePublicShareURLHelper={
updatePublicShareURLHelper
}
/>
<ManageDownloadAccess
collection={collection}
publicShareProp={publicShareProp}
@ -81,7 +92,6 @@ export default function PublicShareManage({
}
/>
<ManageLinkPassword
setChangePasswordView={setChangePasswordView}
collection={collection}
publicShareProp={publicShareProp}
updatePublicShareURLHelper={
@ -102,14 +112,6 @@ export default function PublicShareManage({
)}
</ManageSectionOptions>
</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 Select from 'react-select';
import { linkExpiryStyle } from 'styles/linkExpiry';
import { PublicURL, Collection, UpdatePublicURL } from 'types/collection';
import { shareExpiryOptions } from 'utils/collection';
import constants from 'utils/strings/constants';
import { formatDateTime } from 'utils/time/format';
import { OptionWithDivider } from './selectComponents/OptionWithDivider';
interface Iprops {
publicShareProp: PublicURL;
collection: Collection;
updatePublicShareURLHelper: (req: UpdatePublicURL) => Promise<void>;
}
export function ManageLinkExpiry({
publicShareProp,
collection,
updatePublicShareURLHelper,
}) {
}: Iprops) {
const updateDeviceExpiry = async (optionFn) => {
return updatePublicShareURLHelper({
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,
} from 'components/SingleInputForm';
import React from 'react';
import CryptoWorker from 'utils/crypto';
import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker';
import constants from 'utils/strings/constants';
export function PublicLinkSetPassword({
@ -28,8 +28,8 @@ export function PublicLinkSetPassword({
};
const enablePublicUrlPassword = async (password: string) => {
const cryptoWorker = await new CryptoWorker();
const kekSalt: string = await cryptoWorker.generateSaltToDeriveKey();
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
const kekSalt = await cryptoWorker.generateSaltToDeriveKey();
const kek = await cryptoWorker.deriveInteractiveKey(password, kekSalt);
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,
});
}, [collectionSummaries, activeCollectionID]);
}, [collectionSummaries, activeCollectionID, isInSearchMode]);
if (shouldBeHidden) {
return <></>;

View file

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

View file

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

View file

@ -23,7 +23,6 @@ const Option = ({
color: value !== Number(selected) ? '#aaa' : '#fff',
}}>
<Form.Check.Input
style={{ marginTop: '6px' }}
id={value.toString()}
type="radio"
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 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 { styled } from '@mui/material';
import DownloadManager from 'services/downloadManager';
@ -32,6 +32,7 @@ import { User } from 'types/user';
import { getData, LS_KEYS } from 'utils/storage/localStorage';
import { useMemo } from 'react';
import { Collection } from 'types/collection';
import { addLogLine } from 'utils/logging';
const Container = styled('div')`
display: block;
@ -113,81 +114,112 @@ const PhotoFrame = ({
const router = useRouter();
const [isSourceLoaded, setIsSourceLoaded] = useState(false);
const filteredData = useMemo(() => {
const idSet = new Set();
const user: User = getData(LS_KEYS.USER);
const updateInProgress = useRef(false);
const updateRequired = useRef(false);
return files
.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;
}
const [filteredData, setFilteredData] = useState<EnteFile[]>([]);
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)) {
useEffect(() => {
const main = () => {
if (updateInProgress.current) {
updateRequired.current = true;
return;
}
updateInProgress.current = true;
const idSet = new Set();
const user: User = getData(LS_KEYS.USER);
const filteredData = files
.map((item, index) => ({
...item,
dataIndex: index,
w: window.innerWidth,
h: window.innerHeight,
title: item.pubMagicMetadata?.data.caption,
}))
.filter((item) => {
if (
activeCollection === ALL_SECTION ||
activeCollection === ARCHIVE_SECTION ||
activeCollection === TRASH_SECTION ||
activeCollection === item.collectionID
deletedFileIds?.has(item.id) &&
activeCollection !== TRASH_SECTION
) {
idSet.add(item.id);
return true;
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) {
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;
});
}, [files, deletedFileIds, search, activeCollection]);
});
setFilteredData(filteredData);
updateInProgress.current = false;
if (updateRequired.current) {
updateRequired.current = false;
setTimeout(() => {
main();
}, 0);
}
};
main();
}, [
files,
deletedFileIds,
search?.date,
search?.location,
activeCollection,
]);
const fileToCollectionsMap = useMemo(() => {
const fileToCollectionsMap = new Map<number, number[]>();
@ -493,12 +525,26 @@ const PhotoFrame = ({
index: number,
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) {
addLogLine(`[${item.id}] doesn't have thumbnail`);
try {
let url: string;
if (galleryContext.thumbs.has(item.id)) {
addLogLine(
`[${item.id}] gallery context cache hit, using cached thumb`
);
url = galleryContext.thumbs.get(item.id);
} else {
addLogLine(
`[${item.id}] gallery context cache miss, calling downloadManager to get thumb`
);
if (
publicCollectionGalleryContext.accessedThroughSharedURL
) {
@ -523,6 +569,9 @@ const PhotoFrame = ({
item.w = newFile.w;
item.h = newFile.h;
addLogLine(
`[${item.id}] calling invalidateCurrItems for thumbnail`
);
try {
instance.invalidateCurrItems();
if (instance.isOpen()) {
@ -540,16 +589,23 @@ const PhotoFrame = ({
}
}
if (!fetching[item.id]) {
addLogLine(`[${item.id}] new file download fetch original request`);
try {
fetching[item.id] = true;
let urls: { original: string[]; converted: string[] };
if (galleryContext.files.has(item.id)) {
addLogLine(
`[${item.id}] gallery context cache hit, using cached file`
);
const mergedURL = galleryContext.files.get(item.id);
urls = {
original: mergedURL.original.split(','),
converted: mergedURL.converted.split(','),
};
} else {
addLogLine(
`[${item.id}] gallery context cache miss, calling downloadManager to get file`
);
appContext.startLoading();
if (
publicCollectionGalleryContext.accessedThroughSharedURL
@ -576,7 +632,8 @@ const PhotoFrame = ({
let convertedVideoURL;
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) {
[originalVideoURL] = urls.original;
[convertedVideoURL] = urls.converted;
@ -600,6 +657,9 @@ const PhotoFrame = ({
item.w = newFile.w;
item.h = newFile.h;
try {
addLogLine(
`[${item.id}] calling invalidateCurrItems for src`
);
instance.invalidateCurrItems();
if (instance.isOpen()) {
instance.updateSize(true);
@ -609,13 +669,12 @@ const PhotoFrame = ({
e,
'updating photoswipe after src url update failed'
);
// ignore
throw e;
}
} catch (e) {
logError(e, 'getSlideData failed get src url failed');
// no-op
} finally {
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 { Box, Link, styled } from '@mui/material';
import { EnteFile } from 'types/file';
@ -25,11 +25,15 @@ import { formatDate } from 'utils/time/format';
const A_DAY = 24 * 60 * 60 * 1000;
const FOOTER_HEIGHT = 90;
const ALBUM_FOOTER_HEIGHT = 75;
export enum ITEM_TYPE {
TIME = 'TIME',
FILE = 'FILE',
SIZE_AND_COUNT = 'SIZE_AND_COUNT',
HEADER = 'HEADER',
FOOTER = 'FOOTER',
MARKETING_FOOTER = 'MARKETING_FOOTER',
OTHER = 'OTHER',
}
@ -128,7 +132,6 @@ const SizeAndCountContainer = styled(DateContainer)`
`;
const FooterContainer = styled(ListItemContainer)`
font-size: 14px;
margin-bottom: 0.75rem;
@media (max-width: 540px) {
font-size: 12px;
@ -141,6 +144,13 @@ const FooterContainer = styled(ListItemContainer)`
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)`
color: #979797;
text-align: center;
@ -171,17 +181,16 @@ export function PhotoList({
resetFetching,
}: Props) {
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(
PublicCollectionGalleryContext
);
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);
let columns = Math.ceil(fittableColumns);
@ -200,59 +209,135 @@ export function PhotoList({
};
useEffect(() => {
let timeStampList: TimeStampListItem[] = [];
const main = () => {
if (refreshInProgress.current) {
shouldRefresh.current = true;
return;
}
refreshInProgress.current = true;
let timeStampList: TimeStampListItem[] = [];
if (galleryContext.photoListHeader) {
timeStampList.push(
getPhotoListHeader(galleryContext.photoListHeader)
);
} else if (publicCollectionGalleryContext.photoListHeader) {
timeStampList.push(
getPhotoListHeader(
publicCollectionGalleryContext.photoListHeader
)
);
}
if (deduplicateContext.isOnDeduplicatePage) {
skipMerge = true;
groupByFileSize(timeStampList);
} else {
groupByTime(timeStampList);
}
if (galleryContext.photoListHeader) {
timeStampList.push(
getPhotoListHeader(galleryContext.photoListHeader)
);
} else if (publicCollectionGalleryContext.photoListHeader) {
timeStampList.push(
getPhotoListHeader(
publicCollectionGalleryContext.photoListHeader
)
);
}
if (deduplicateContext.isOnDeduplicatePage) {
skipMerge = true;
groupByFileSize(timeStampList);
} else {
groupByTime(timeStampList);
}
if (!skipMerge) {
timeStampList = mergeTimeStampList(timeStampList, columns);
}
if (timeStampList.length === 1) {
timeStampList.push(getEmptyListItem());
}
if (
showAppDownloadBanner ||
publicCollectionGalleryContext.accessedThroughSharedURL
) {
if (!skipMerge) {
timeStampList = mergeTimeStampList(timeStampList, columns);
}
if (timeStampList.length === 1) {
timeStampList.push(getEmptyListItem());
}
timeStampList.push(getVacuumItem(timeStampList));
if (publicCollectionGalleryContext.accessedThroughSharedURL) {
if (publicCollectionGalleryContext.photoListFooter) {
timeStampList.push(
getPhotoListFooter(
publicCollectionGalleryContext.photoListFooter
)
);
}
timeStampList.push(getAlbumsFooter());
} else {
} else if (showAppDownloadBanner) {
timeStampList.push(getAppDownloadFooter());
}
}
timeStampListRef.current = timeStampList;
filteredDataCopyRef.current = filteredData;
refreshList();
setTimeStampList(timeStampList);
refreshInProgress.current = false;
if (shouldRefresh.current) {
shouldRefresh.current = false;
setTimeout(main, 0);
}
};
main();
}, [
width,
height,
filteredData,
showAppDownloadBanner,
publicCollectionGalleryContext.accessedThroughSharedURL,
galleryContext.photoListHeader,
publicCollectionGalleryContext.photoListHeader,
deduplicateContext.isOnDeduplicatePage,
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[]) => {
let index = 0;
while (index < filteredData.length) {
@ -295,10 +380,10 @@ export function PhotoList({
const groupByTime = (timeStampList: TimeStampListItem[]) => {
let listItemIndex = 0;
let currentDate = -1;
let currentDate;
filteredData.forEach((item, index) => {
if (
!currentDate ||
!isSameDay(
new Date(item.metadata.creationTime / 1000),
new Date(currentDate)
@ -338,10 +423,13 @@ export function PhotoList({
});
};
const isSameDay = (first, second) =>
first.getFullYear() === second.getFullYear() &&
first.getMonth() === second.getMonth() &&
first.getDate() === second.getDate();
const isSameDay = (first, second) => {
return (
first.getFullYear() === second.getFullYear() &&
first.getMonth() === second.getMonth() &&
first.getDate() === second.getDate()
);
};
const getPhotoListHeader = (photoListHeader) => {
return {
@ -354,6 +442,17 @@ export function PhotoList({
};
};
const getPhotoListFooter = (photoListFooter) => {
return {
...photoListFooter,
item: (
<ListItemContainer span={columns}>
{photoListFooter.item}
</ListItemContainer>
),
};
};
const getEmptyListItem = () => {
return {
itemType: ITEM_TYPE.OTHER,
@ -367,12 +466,17 @@ export function PhotoList({
};
};
const getVacuumItem = (timeStampList) => {
const footerHeight =
publicCollectionGalleryContext.accessedThroughSharedURL
? ALBUM_FOOTER_HEIGHT +
(publicCollectionGalleryContext.photoListFooter?.height ?? 0)
: FOOTER_HEIGHT;
const photoFrameHeight = (() => {
let sum = 0;
const getCurrentItemSize = getItemSize(timeStampList);
for (let i = 0; i < timeStampList.length; i++) {
sum += getCurrentItemSize(i);
if (height - sum <= FOOTER_HEIGHT) {
if (height - sum <= footerHeight) {
break;
}
}
@ -381,17 +485,19 @@ export function PhotoList({
return {
itemType: ITEM_TYPE.OTHER,
item: <></>,
height: Math.max(height - photoFrameHeight - FOOTER_HEIGHT, 0),
height: Math.max(height - photoFrameHeight - footerHeight, 0),
};
};
const getAppDownloadFooter = () => {
return {
itemType: ITEM_TYPE.OTHER,
itemType: ITEM_TYPE.MARKETING_FOOTER,
height: FOOTER_HEIGHT,
item: (
<FooterContainer span={columns}>
<Typography>{constants.INSTALL_MOBILE_APP()}</Typography>
<Typography variant="body2">
{constants.INSTALL_MOBILE_APP()}
</Typography>
</FooterContainer>
),
};
@ -399,17 +505,17 @@ export function PhotoList({
const getAlbumsFooter = () => {
return {
itemType: ITEM_TYPE.OTHER,
height: FOOTER_HEIGHT,
itemType: ITEM_TYPE.MARKETING_FOOTER,
height: ALBUM_FOOTER_HEIGHT,
item: (
<FooterContainer span={columns}>
<p>
{constants.PRESERVED_BY}{' '}
<AlbumFooterContainer span={columns}>
<Typography variant="body2">
{constants.SHARED_USING}{' '}
<Link target="_blank" href={ENTE_WEBSITE_LINK}>
{constants.ENTE_IO}
</Link>
</p>
</FooterContainer>
</Typography>
</AlbumFooterContainer>
),
};
};
@ -527,14 +633,14 @@ export function PhotoList({
switch (listItem.itemType) {
case ITEM_TYPE.TIME:
return listItem.dates ? (
listItem.dates.map((item) => (
<>
listItem.dates
.map((item) => [
<DateContainer key={item.date} span={item.span}>
{item.date}
</DateContainer>
<div />
</>
))
</DateContainer>,
<div key={`${item.date}-gap`} />,
])
.flat()
) : (
<DateContainer span={columns}>
{listItem.date}
@ -551,7 +657,7 @@ export function PhotoList({
case ITEM_TYPE.FILE: {
const ret = listItem.items.map((item, idx) =>
getThumbnail(
filteredDataCopy,
filteredData,
listItem.itemStartIndex + idx,
isScrolling
)

View file

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

View file

@ -11,9 +11,9 @@ import { SmallLoadingSpinner } from '../styledComponents/SmallLoadingSpinner';
import * as Yup from 'yup';
import constants from 'utils/strings/constants';
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;
}

View file

@ -9,11 +9,12 @@ import {
import { FlexWrapper } from 'components/Container';
import { logError } from 'utils/sentry';
import { FILE_TYPE } from 'constants/file';
import { PhotoOutlined, VideoFileOutlined } from '@mui/icons-material';
import InfoItem from './InfoItem';
import { makeHumanReadableStorage } from 'utils/billing';
import Box from '@mui/material/Box';
import { FileNameEditDialog } from './FileNameEditDialog';
import VideocamOutlined from '@mui/icons-material/VideocamOutlined';
import PhotoOutlined from '@mui/icons-material/PhotoOutlined';
const getFileTitle = (filename, extension) => {
if (extension) {
@ -98,10 +99,10 @@ export function RenderFileName({
<>
<InfoItem
icon={
file.metadata.fileType === FILE_TYPE.IMAGE ? (
<PhotoOutlined />
file.metadata.fileType === FILE_TYPE.VIDEO ? (
<VideocamOutlined />
) : (
<VideoFileOutlined />
<PhotoOutlined />
)
}
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 { getEXIFLocation } from 'services/upload/exifService';
import { RenderCaption } from './RenderCaption';
import {
BackupOutlined,
CameraOutlined,
FolderOutlined,
LocationOnOutlined,
TextSnippetOutlined,
} from '@mui/icons-material';
import CopyButton from 'components/CodeBlock/CopyButton';
import { formatDate, formatTime } from 'utils/time/format';
import Titlebar from 'components/Titlebar';
@ -24,6 +18,11 @@ import { Chip } from 'components/Chip';
import LinkButton from 'components/pages/gallery/LinkButton';
import { ExifData } from './ExifData';
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) => (
<EnteDrawer {...props} anchor="right" />

View file

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

View file

@ -6,9 +6,13 @@ import { addLogLine, getDebugLogs } from 'utils/logging';
import SidebarButton from './Button';
import isElectron from 'is-electron';
import ElectronService from 'services/electron/common';
import { testUpload } from 'tests/upload.test';
import Typography from '@mui/material/Typography';
import { isInternalUser } from 'utils/user';
import { testUpload } from '../../../tests/upload.test';
import {
testZipFileReading,
testZipWithRootFileReadingTest,
} from '../../../tests/zip-file-reading.test';
export default function DebugSection() {
const appContext = useContext(AppContext);
@ -63,9 +67,17 @@ export default function DebugSection() {
</Typography>
)}
{isInternalUser() && (
<SidebarButton onClick={testUpload}>
{constants.RUN_TESTS}
</SidebarButton>
<>
<SidebarButton onClick={testUpload}>
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 { CollectionSummaries } from 'types/collection';
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 {
closeSidebar: () => void;
collectionSummaries: CollectionSummaries;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -26,10 +26,15 @@ const SubmitButton: FC<ButtonProps<'button', SubmitButtonProps>> = ({
disabled={disabled || loading || success}
sx={{
my: 4,
'&.Mui-disabled': {
backgroundColor: (theme) => theme.palette.accent.main,
color: (theme) => theme.palette.text.primary,
},
...(loading
? {
'&.Mui-disabled': {
backgroundColor: (theme) =>
theme.palette.accent.main,
color: (theme) => theme.palette.text.primary,
},
}
: {}),
...sx,
}}
{...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 React from 'react';
import { FlexWrapper } from './Container';

View file

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

View file

@ -10,6 +10,7 @@ import { NotUploadSectionHeader } from './styledComponents';
import UploadProgressContext from 'contexts/uploadProgress';
import { dialogCloseHandler } from 'components/DialogBox/TitleWithCloseButton';
import { APP_DOWNLOAD_URL } from 'utils/common';
import { ENTE_WEBSITE_LINK } from 'constants/urls';
export function UploadProgressDialog() {
const { open, onClose, uploadStage, finishedUploads } = useContext(
@ -26,7 +27,8 @@ export function UploadProgressDialog() {
finishedUploads.get(UPLOAD_RESULT.LARGER_THAN_AVAILABLE_STORAGE)
?.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);
} else {
@ -84,6 +86,13 @@ export function UploadProgressDialog() {
uploadResult={UPLOAD_RESULT.FAILED}
sectionTitle={constants.FAILED_UPLOADS}
/>
<ResultSection
uploadResult={UPLOAD_RESULT.SKIPPED_VIDEOS}
sectionTitle={constants.SKIPPED_VIDEOS}
sectionInfo={constants.SKIPPED_VIDEOS_INFO(
ENTE_WEBSITE_LINK
)}
/>
<ResultSection
uploadResult={UPLOAD_RESULT.ALREADY_UPLOADED}
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 { default as FileUploadIcon } from '@mui/icons-material/ImageOutlined';
import { default as FolderUploadIcon } from '@mui/icons-material/PermMediaOutlined';
import GoogleIcon from '@mui/icons-material/Google';
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 { 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({
onHide,
onClose,
show,
uploadFiles,
uploadFolders,
uploadGoogleTakeoutZips,
}) {
hideZipUploadOption,
}: Iprops) {
const publicCollectionGalleryContext = useContext(
PublicCollectionGalleryContext
);
const directlyShowUploadFiles = useRef(isMobileOrTable());
useEffect(() => {
if (
show &&
directlyShowUploadFiles.current &&
publicCollectionGalleryContext.accessedThroughSharedURL
) {
uploadFiles();
onClose();
}
}, [show]);
return (
<Dialog
open={show}
@ -24,9 +53,11 @@ export default function UploadTypeSelector({
[theme.breakpoints.down(360)]: { p: 0 },
}),
}}
onClose={onHide}>
<DialogTitleWithCloseButton onClose={onHide}>
{constants.UPLOAD}
onClose={dialogCloseHandler({ onClose })}>
<DialogTitleWithCloseButton onClose={onClose}>
{publicCollectionGalleryContext.accessedThroughSharedURL
? constants.SELECT_PHOTOS
: constants.UPLOAD}
</DialogTitleWithCloseButton>
<Box p={1.5} pt={0.5}>
<Stack spacing={0.5}>
@ -40,11 +71,13 @@ export default function UploadTypeSelector({
startIcon={<FolderUploadIcon />}>
{constants.UPLOAD_DIRS}
</UploadTypeOption>
<UploadTypeOption
onClick={uploadGoogleTakeoutZips}
startIcon={<GoogleIcon />}>
{constants.UPLOAD_GOOGLE_TAKEOUT}
</UploadTypeOption>
{!hideZipUploadOption && (
<UploadTypeOption
onClick={uploadGoogleTakeoutZips}
startIcon={<GoogleIcon />}>
{constants.UPLOAD_GOOGLE_TAKEOUT}
</UploadTypeOption>
)}
</Stack>
<Typography p={1.5} pt={4} color="text.secondary">
{constants.DRAG_AND_DROP_HINT}

View file

@ -10,7 +10,6 @@ import { SetCollections, SetCollectionSelectorAttributes } from 'types/gallery';
import { GalleryContext } from 'pages/gallery';
import { AppContext } from 'pages/_app';
import { logError } from 'utils/sentry';
import UploadManager from 'services/upload/uploadManager';
import uploadManager from 'services/upload/uploadManager';
import ImportService from 'services/importService';
import isElectron from 'is-electron';
@ -40,7 +39,10 @@ import {
PICKED_UPLOAD_TYPE,
} from 'constants/upload';
import importService from 'services/importService';
import { getDownloadAppMessage } from 'utils/ui';
import {
getDownloadAppMessage,
getRootLevelFileWithFolderNotAllowMessage,
} from 'utils/ui';
import UploadTypeSelector from './UploadTypeSelector';
import {
filterOutSystemFiles,
@ -50,21 +52,28 @@ import {
import { getUserOwnedCollections } from 'utils/collection';
import billingService from 'services/billingService';
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';
interface Props {
syncWithRemote: (force?: boolean, silent?: boolean) => Promise<void>;
closeCollectionSelector: () => void;
closeCollectionSelector?: () => void;
closeUploadTypeSelector: () => void;
setCollectionSelectorAttributes: SetCollectionSelectorAttributes;
setCollectionNamerAttributes: SetCollectionNamerAttributes;
setCollectionSelectorAttributes?: SetCollectionSelectorAttributes;
setCollectionNamerAttributes?: SetCollectionNamerAttributes;
setLoading: SetLoading;
setShouldDisableDropzone: (value: boolean) => void;
showCollectionSelector: () => void;
showCollectionSelector?: () => void;
setFiles: SetFiles;
setCollections: SetCollections;
isFirstUpload: boolean;
setCollections?: SetCollections;
isFirstUpload?: boolean;
uploadTypeSelectorView: boolean;
showSessionExpiredMessage: () => void;
showUploadFilesDialog: () => void;
@ -72,9 +81,17 @@ interface Props {
webFolderSelectorFiles: File[];
webFileSelectorFiles: File[];
dragAndDropFiles: File[];
zipUploadDisabled?: boolean;
uploadCollection?: Collection;
}
export default function Uploader(props: Props) {
const appContext = useContext(AppContext);
const galleryContext = useContext(GalleryContext);
const publicCollectionGalleryContext = useContext(
PublicCollectionGalleryContext
);
const [uploadProgressView, setUploadProgressView] = useState(false);
const [uploadStage, setUploadStage] = useState<UPLOAD_STAGES>(
UPLOAD_STAGES.START
@ -93,11 +110,13 @@ export default function Uploader(props: Props) {
const [hasLivePhotos, setHasLivePhotos] = useState(false);
const [choiceModalView, setChoiceModalView] = useState(false);
const [userNameInputDialogView, setUserNameInputDialogView] =
useState(false);
const [importSuggestion, setImportSuggestion] = useState<ImportSuggestion>(
DEFAULT_IMPORT_SUGGESTION
);
const appContext = useContext(AppContext);
const galleryContext = useContext(GalleryContext);
const [electronFiles, setElectronFiles] = useState<ElectronFile[]>(null);
const [webFiles, setWebFiles] = useState([]);
const toUploadFiles = useRef<File[] | ElectronFile[]>(null);
const isPendingDesktopUpload = useRef(false);
@ -106,20 +125,33 @@ export default function Uploader(props: Props) {
const pickedUploadType = useRef<PICKED_UPLOAD_TYPE>(null);
const zipPaths = useRef<string[]>(null);
const currentUploadPromise = useRef<Promise<void>>(null);
const [electronFiles, setElectronFiles] = useState<ElectronFile[]>(null);
const [webFiles, setWebFiles] = useState([]);
const uploadRunning = useRef(false);
const uploaderNameRef = useRef<string>(null);
const closeUploadProgress = () => setUploadProgressView(false);
const showUserNameInputDialog = () => setUserNameInputDialogView(true);
const setCollectionName = (collectionName: string) => {
isPendingDesktopUpload.current = true;
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(() => {
UploadManager.init(
uploadManager.init(
{
setPercentComplete,
setUploadCounter,
@ -129,12 +161,16 @@ export default function Uploader(props: Props) {
setUploadFilenames: setUploadFileNames,
setHasLivePhotos,
},
props.setFiles
props.setFiles,
publicCollectionGalleryContext
);
if (isElectron() && ImportService.checkAllElectronAPIsExists()) {
ImportService.getPendingUploads().then(
({ files: electronFiles, collectionName, type }) => {
addLogLine(
`found pending desktop upload, resuming uploads`
);
resumeDesktopUpload(type, electronFiles, collectionName);
}
);
@ -145,7 +181,11 @@ export default function Uploader(props: Props) {
appContext.setIsFolderSyncRunning
);
}
}, []);
}, [
publicCollectionGalleryContext.accessedThroughSharedURL,
publicCollectionGalleryContext.token,
publicCollectionGalleryContext.passwordToken,
]);
// this handles the change of selectorFiles changes on web when user selects
// 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 &&
props.webFolderSelectorFiles?.length > 0
) {
addLogLine(`received folder upload request`);
setWebFiles(props.webFolderSelectorFiles);
} else if (
pickedUploadType.current === PICKED_UPLOAD_TYPE.FILES &&
props.webFileSelectorFiles?.length > 0
) {
addLogLine(`received file upload request`);
setWebFiles(props.webFileSelectorFiles);
} else if (props.dragAndDropFiles?.length > 0) {
addLogLine(`received drag and drop upload request`);
setWebFiles(props.dragAndDropFiles);
}
}, [
@ -181,17 +224,37 @@ export default function Uploader(props: Props) {
webFiles?.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 (watchFolderService.isUploadRunning()) {
addLogLine(
'watchFolder upload was running, pausing it to run user upload'
);
// pause watch folder service on user upload
watchFolderService.pauseRunningSync();
} else {
addLogLine(
'an upload is already running, rejecting new upload request'
);
// no-op
// a user upload is already in progress
return;
}
}
if (isCanvasBlocked()) {
addLogLine('canvas blocked, blocking upload');
appContext.setDialogMessage({
title: constants.CANVAS_BLOCKED_TITLE,
@ -236,7 +299,8 @@ export default function Uploader(props: Props) {
handleCollectionCreationAndUpload(
importSuggestion,
props.isFirstUpload,
pickedUploadType.current
pickedUploadType.current,
publicCollectionGalleryContext.accessedThroughSharedURL
);
pickedUploadType.current = null;
props.setLoading(false);
@ -257,14 +321,20 @@ export default function Uploader(props: Props) {
};
const preCollectionCreationAction = async () => {
props.closeCollectionSelector();
props.closeCollectionSelector?.();
props.setShouldDisableDropzone(!uploadManager.shouldAllowNewUpload());
setUploadStage(UPLOAD_STAGES.START);
setUploadProgressView(true);
};
const uploadFilesToExistingCollection = async (collection: Collection) => {
const uploadFilesToExistingCollection = async (
collection: Collection,
uploaderName?: string
) => {
try {
addLogLine(
`upload file to an existing collection - "${collection.name}"`
);
await preCollectionCreationAction();
const filesWithCollectionToUpload: FileWithCollection[] =
toUploadFiles.current.map((file, index) => ({
@ -272,10 +342,11 @@ export default function Uploader(props: Props) {
localID: index,
collectionID: collection.id,
}));
waitInQueueAndUploadFiles(filesWithCollectionToUpload, [
collection,
]);
toUploadFiles.current = null;
waitInQueueAndUploadFiles(
filesWithCollectionToUpload,
[collection],
uploaderName
);
} catch (e) {
logError(e, 'Failed to upload files to existing collections');
}
@ -286,6 +357,9 @@ export default function Uploader(props: Props) {
collectionName?: string
) => {
try {
addLogLine(
`upload file to an new collections strategy:${strategy} ,collectionName:${collectionName}`
);
await preCollectionCreationAction();
let filesWithCollectionToUpload: FileWithCollection[] = [];
const collections: Collection[] = [];
@ -303,6 +377,9 @@ export default function Uploader(props: Props) {
toUploadFiles.current
);
}
addLogLine(
`upload collections - [${[...collectionNameToFilesMap.keys()]}]`
);
try {
const existingCollection = getUserOwnedCollections(
await syncCollections()
@ -350,13 +427,18 @@ export default function Uploader(props: Props) {
const waitInQueueAndUploadFiles = (
filesWithCollectionToUploadIn: FileWithCollection[],
collections: Collection[]
collections: Collection[],
uploaderName?: string
) => {
const currentPromise = currentUploadPromise.current;
currentUploadPromise.current = waitAndRun(
currentPromise,
async () =>
await uploadFiles(filesWithCollectionToUploadIn, collections)
await uploadFiles(
filesWithCollectionToUploadIn,
collections,
uploaderName
)
);
};
@ -374,9 +456,11 @@ export default function Uploader(props: Props) {
const uploadFiles = async (
filesWithCollectionToUploadIn: FileWithCollection[],
collections: Collection[]
collections: Collection[],
uploaderName?: string
) => {
try {
addLogLine('uploadFiles called');
preUploadAction();
if (
isElectron() &&
@ -401,7 +485,8 @@ export default function Uploader(props: Props) {
const shouldCloseUploadProgress =
await uploadManager.queueFilesForUpload(
filesWithCollectionToUploadIn,
collections
collections,
uploaderName
);
if (shouldCloseUploadProgress) {
closeUploadProgress();
@ -418,6 +503,7 @@ export default function Uploader(props: Props) {
}
}
} catch (err) {
logError(err, 'failed to upload files');
showUserFacingError(err.message);
closeUploadProgress();
throw err;
@ -430,13 +516,16 @@ export default function Uploader(props: Props) {
try {
addLogLine('user retrying failed upload');
const filesWithCollections =
await uploadManager.getFailedFilesWithCollections();
uploadManager.getFailedFilesWithCollections();
const uploaderName = uploadManager.getUploaderName();
await preUploadAction();
await uploadManager.queueFilesForUpload(
filesWithCollections.files,
filesWithCollections.collections
filesWithCollections.collections,
uploaderName
);
} catch (err) {
logError(err, 'retry failed files failed');
showUserFacingError(err.message);
closeUploadProgress();
} finally {
@ -478,6 +567,7 @@ export default function Uploader(props: Props) {
const uploadToSingleNewCollection = (collectionName: string) => {
if (collectionName) {
addLogLine(`upload to single collection - "${collectionName}"`);
uploadFilesToNewCollections(
UPLOAD_STRATEGY.SINGLE_COLLECTION,
collectionName
@ -495,45 +585,75 @@ export default function Uploader(props: Props) {
});
};
const handleCollectionCreationAndUpload = (
const handleCollectionCreationAndUpload = async (
importSuggestion: ImportSuggestion,
isFirstUpload: boolean,
pickedUploadType: PICKED_UPLOAD_TYPE
pickedUploadType: PICKED_UPLOAD_TYPE,
accessedThroughSharedURL?: boolean
) => {
if (isPendingDesktopUpload.current) {
isPendingDesktopUpload.current = false;
if (pendingDesktopUploadCollectionName.current) {
uploadToSingleNewCollection(
pendingDesktopUploadCollectionName.current
try {
if (accessedThroughSharedURL) {
addLogLine(
`uploading files to pulbic collection - ${props.uploadCollection.name} - ${props.uploadCollection.id}`
);
pendingDesktopUploadCollectionName.current = null;
} else {
const uploaderName = await getPublicCollectionUploaderName(
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(
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) => {
let files: ElectronFile[];
pickedUploadType.current = type;
@ -547,6 +667,9 @@ export default function Uploader(props: Props) {
zipPaths.current = response.zipPaths;
}
if (files?.length > 0) {
addLogLine(
` desktop upload for type:${type} and fileCount: ${files?.length} requested`
);
setElectronFiles(files);
props.closeUploadTypeSelector();
}
@ -579,26 +702,57 @@ export default function Uploader(props: Props) {
const handleFolderUpload = handleUpload(PICKED_UPLOAD_TYPE.FOLDERS);
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 (
<>
<UploadStrategyChoiceModal
open={choiceModalView}
onClose={() => setChoiceModalView(false)}
uploadToSingleCollection={() =>
uploadToSingleNewCollection(importSuggestion.rootFolderName)
}
uploadToMultipleCollection={() =>
uploadFilesToNewCollections(
UPLOAD_STRATEGY.COLLECTION_PER_FOLDER
)
}
onClose={handleChoiceModalClose}
uploadToSingleCollection={handleUploadToSingleCollection}
uploadToMultipleCollection={handleUploadToMultipleCollections}
/>
<UploadTypeSelector
show={props.uploadTypeSelectorView}
onHide={props.closeUploadTypeSelector}
onClose={props.closeUploadTypeSelector}
uploadFiles={handleFileUpload}
uploadFolders={handleFolderUpload}
uploadGoogleTakeoutZips={handleZipUpload}
hideZipUploadOption={props.zipUploadDisabled}
/>
<UploadProgress
open={uploadProgressView}
@ -613,6 +767,13 @@ export default function Uploader(props: Props) {
finishedUploads={finishedUploads}
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 constants from 'utils/strings/constants';
import CryptoWorker from 'utils/crypto';
import SingleInputForm, {
SingleInputFormProps,
} from 'components/SingleInputForm';
@ -10,6 +9,7 @@ import { CustomError } from 'utils/error';
import { Input } from '@mui/material';
import { KeyAttributes, User } from 'types/user';
import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker';
export interface VerifyMasterPasswordFormProps {
user: User;
@ -29,7 +29,7 @@ export default function VerifyMasterPasswordForm({
setFieldError
) => {
try {
const cryptoWorker = await new CryptoWorker();
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
let kek: string = null;
try {
kek = await cryptoWorker.deriveKey(
@ -43,7 +43,7 @@ export default function VerifyMasterPasswordForm({
throw Error(CustomError.WEAK_DEVICE);
}
try {
const key: string = await cryptoWorker.decryptB64(
const key = await cryptoWorker.decryptB64(
keyAttributes.encryptedKey,
keyAttributes.keyDecryptionNonce,
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 NavbarBase from 'components/Navbar/base';
import SidebarToggler from 'components/Navbar/SidebarToggler';
import { getNonTrashedUniqueUserFiles } from 'utils/file';
import SearchBar from 'components/Search/SearchBar';
import { Collection } from 'types/collection';
import { EnteFile } from 'types/file';
@ -36,7 +35,7 @@ export function GalleryNavbar({
isInSearchMode={isInSearchMode}
setIsInSearchMode={setIsInSearchMode}
collections={collections}
files={getNonTrashedUniqueUserFiles(files)}
files={files}
setActiveCollection={setActiveCollection}
updateSearch={updateSearch}
/>

View file

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

View file

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

View file

@ -1,7 +1,7 @@
import { ENTE_WEBSITE_LINK } from 'constants/urls';
import React, { useEffect, useState } from 'react';
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';
export const NoStyleAnchor = styled('a')`
@ -16,7 +16,7 @@ function GoToEnte() {
const [os, setOS] = useState<OS>(OS.UNKNOWN);
useEffect(() => {
const os = GetDeviceOS();
const os = getDeviceOS();
setOS(os);
}, []);

View file

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

View file

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

View file

@ -15,7 +15,3 @@ export enum PAGES {
SHARED_ALBUMS = '/shared-albums',
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.VIDEO, exactType: 'webm', mimeType: 'video/webm' },
{ 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.
@ -52,6 +53,7 @@ export enum UPLOAD_RESULT {
UPLOADED_WITH_STATIC_THUMBNAIL,
ADDED_SYMLINK,
CANCELLED,
SKIPPED_VIDEOS,
}
export enum PICKED_UPLOAD_TYPE {
@ -76,6 +78,7 @@ export const USE_CF_PROXY = false;
export const DEFAULT_IMPORT_SUGGESTION: ImportSuggestion = {
rootFolderName: '',
hasNestedFolders: false,
hasRootLevelFileWithFolder: false,
};
export const BLACK_THUMBNAIL_BASE64 =

View file

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

View file

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

View file

@ -34,7 +34,7 @@ import {
setIsFirstLogin,
setJustSignedUp,
} from 'utils/storage';
import { isTokenValid, logoutUser } from 'services/userService';
import { isTokenValid, logoutUser, validateKey } from 'services/userService';
import { useDropzone } from 'react-dropzone';
import EnteSpinner from 'components/EnteSpinner';
import { LoadingOverlay } from 'components/LoadingOverlay';
@ -42,7 +42,7 @@ import PhotoFrame from 'components/PhotoFrame';
import {
changeFilesVisibility,
downloadFiles,
getNonTrashedUniqueUserFiles,
getNonTrashedFiles,
getSelectedFiles,
mergeMetadata,
sortFiles,
@ -87,7 +87,7 @@ import FixCreationTime, {
} from 'components/FixCreationTime';
import { Collection, CollectionSummaries } from 'types/collection';
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 Collections from 'components/Collections';
import { GalleryNavbar } from 'components/pages/gallery/Navbar';
@ -124,12 +124,45 @@ export const GalleryContext = createContext<GalleryContextType>(
defaultGalleryContext
);
type FilesFn = EnteFile[] | ((files: EnteFile[]) => EnteFile[]);
export default function Gallery() {
const router = useRouter();
const [user, setUser] = useState(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 [isFirstLoad, setIsFirstLoad] = useState(false);
@ -225,7 +258,12 @@ export default function Gallery() {
router.push(PAGES.ROOT);
return;
}
preloadImage('/images/subscription-card-background');
const main = async () => {
const valid = await validateKey();
if (!valid) {
return;
}
setActiveCollection(ALL_SECTION);
setIsFirstLoad(isFirstLogin());
setIsFirstFetch(true);
@ -245,7 +283,6 @@ export default function Gallery() {
setIsFirstLoad(false);
setJustSignedUp(false);
setIsFirstFetch(false);
preloadImage('/images/subscription-card-background');
};
main();
}, []);
@ -307,7 +344,7 @@ export default function Gallery() {
searchResultSummary={searchResultSummary}
/>
),
itemType: ITEM_TYPE.OTHER,
itemType: ITEM_TYPE.HEADER,
});
}
}, [isInSearchMode, searchResultSummary]);
@ -329,6 +366,7 @@ export default function Gallery() {
let files = await syncFiles(collections, setFiles);
const trash = await syncTrash(collections, setFiles, files);
files = [...files, ...getTrashedFiles(trash)];
setFiles(sortFiles(files));
} catch (e) {
logError(e, 'syncWithRemote failed');
switch (e.message) {
@ -511,7 +549,6 @@ export default function Gallery() {
if (newSearch?.collection) {
setActiveCollection(newSearch?.collection);
} else {
setActiveCollection(ALL_SECTION);
setSearch(newSearch);
}
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 selectedFiles = getSelectedFiles(selected, files);
setFixCreationTimeAttributes({ files: selectedFiles });
@ -552,6 +582,10 @@ export default function Gallery() {
setUploadTypeSelectorView(true);
};
const closeCollectionSelector = () => {
setCollectionSelectorView(false);
};
return (
<GalleryContext.Provider
value={{
@ -611,7 +645,7 @@ export default function Gallery() {
openUploader={openUploader}
isInSearchMode={isInSearchMode}
collections={collections}
files={getNonTrashedUniqueUserFiles(files)}
files={getNonTrashedFiles(files)}
setActiveCollection={setActiveCollection}
updateSearch={updateSearch}
/>

View file

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

View file

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

View file

@ -18,15 +18,11 @@ import { EnteFile } from 'types/file';
import { mergeMetadata, sortFiles } from 'utils/file';
import { AppContext } from 'pages/_app';
import { AbuseReportForm } from 'components/pages/sharedAlbum/AbuseReportForm';
import {
defaultPublicCollectionGalleryContext,
PublicCollectionGalleryContext,
} from 'utils/publicCollectionGallery';
import { PublicCollectionGalleryContext } from 'utils/publicCollectionGallery';
import { CustomError, parseSharingErrorCodes } from 'utils/error';
import VerticallyCentered from 'components/Container';
import VerticallyCentered, { CenteredFlex } from 'components/Container';
import constants from 'utils/strings/constants';
import EnteSpinner from 'components/EnteSpinner';
import CryptoWorker from 'utils/crypto';
import { PAGES } from 'constants/pages';
import { useRouter } from 'next/router';
import SingleInputForm, {
@ -41,6 +37,17 @@ import FormContainer from 'components/Form/FormContainer';
import FormPaper from 'components/Form/FormPaper';
import FormPaperTitle from 'components/Form/FormPaper/Title';
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 = () => (
<VerticallyCentered>
@ -49,7 +56,6 @@ const Loader = () => (
</EnteSpinner>
</VerticallyCentered>
);
const bs58 = require('bs58');
export default function PublicCollectionGallery() {
const token = useRef<string>(null);
// 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] =
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(() => {
const currentURL = new URL(window.location.href);
if (currentURL.pathname !== PAGES.ROOT) {
@ -91,7 +149,8 @@ export default function PublicCollectionGallery() {
}
const main = async () => {
try {
const worker = await new CryptoWorker();
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
url.current = window.location.href;
const currentURL = new URL(url.current);
const t = currentURL.searchParams.get('t');
@ -101,8 +160,8 @@ export default function PublicCollectionGallery() {
}
const dck =
ck.length < 50
? await worker.toB64(bs58.decode(ck))
: await worker.fromHex(ck);
? await cryptoWorker.toB64(bs58.decode(ck))
: await cryptoWorker.fromHex(ck);
token.current = t;
collectionKey.current = dck;
url.current = window.location.href;
@ -111,6 +170,9 @@ export default function PublicCollectionGallery() {
);
if (localCollection) {
setPublicCollection(localCollection);
const isPasswordProtected =
localCollection?.publicURLs?.[0]?.passwordEnabled;
setIsPasswordProtected(isPasswordProtected);
const collectionUID = getPublicCollectionUID(token.current);
const localFiles = await getLocalPublicFiles(collectionUID);
const localPublicFiles = sortFiles(
@ -140,15 +202,38 @@ export default function PublicCollectionGallery() {
/>
</CollectionInfoBarWrapper>
),
itemType: ITEM_TYPE.OTHER,
itemType: ITEM_TYPE.HEADER,
height: 68,
});
}, [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 collectionUID = getPublicCollectionUID(token.current);
try {
appContext.startLoading();
setLoading(true);
const collection = await getPublicCollection(
token.current,
collectionKey.current
@ -196,7 +281,7 @@ export default function PublicCollectionGallery() {
setErrorMessage(
parsedError.message === CustomError.TOO_MANY_REQUESTS
? constants.LINK_TOO_MANY_REQUESTS
: constants.LINK_EXPIRED
: constants.LINK_EXPIRED_MESSAGE
);
// share has been disabled
// local cache should be cleared
@ -211,6 +296,7 @@ export default function PublicCollectionGallery() {
}
} finally {
appContext.finishLoading();
setLoading(false);
}
};
@ -219,7 +305,7 @@ export default function PublicCollectionGallery() {
setFieldError
) => {
try {
const cryptoWorker = await new CryptoWorker();
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
let hashedPassword: string = null;
try {
const publicUrl = publicCollection.publicURLs[0];
@ -297,31 +383,66 @@ export default function PublicCollectionGallery() {
return (
<PublicCollectionGalleryContext.Provider
value={{
...defaultPublicCollectionGalleryContext,
token: token.current,
passwordToken: passwordJWTToken.current,
accessedThroughSharedURL: true,
openReportForm,
photoListHeader,
photoListFooter,
}}>
<SharedAlbumNavbar />
<PhotoFrame
files={publicFiles}
syncWithRemote={syncWithRemote}
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}
/>
<FullScreenDropZone
getDragAndDropRootProps={getDragAndDropRootProps}>
<UploadSelectorInputs
getDragAndDropInputProps={getDragAndDropInputProps}
getFileSelectorInputProps={getFileSelectorInputProps}
getFolderSelectorInputProps={getFolderSelectorInputProps}
/>
<SharedAlbumNavbar
showUploadButton={
publicCollection?.publicURLs?.[0]?.enableCollect
}
openUploader={openUploader}
/>
<PhotoFrame
files={publicFiles}
syncWithRemote={syncWithRemote}
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>
);
}

View file

@ -2,7 +2,6 @@ import React, { useContext, useEffect, useState } from 'react';
import constants from 'utils/strings/constants';
import { getData, LS_KEYS, setData } from 'utils/storage/localStorage';
import { useRouter } from 'next/router';
import CryptoWorker, { B64EncryptionResult } from 'utils/crypto';
import SingleInputForm, {
SingleInputFormProps,
} from 'components/SingleInputForm';
@ -15,6 +14,8 @@ import FormPaper from 'components/Form/FormPaper';
import FormPaperTitle from 'components/Form/FormPaper/Title';
import FormPaperFooter from 'components/Form/FormPaper/Footer';
import LinkButton from 'components/pages/gallery/LinkButton';
import { B64EncryptionResult } from 'types/crypto';
import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker';
const bip39 = require('bip39');
// mobile client library only supports english.
bip39.setDefaultWordlist('english');
@ -65,8 +66,8 @@ export default function Recover() {
}
recoveryKey = bip39.mnemonicToEntropy(recoveryKey);
}
const cryptoWorker = await new CryptoWorker();
const twoFactorSecret: string = await cryptoWorker.decryptB64(
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
const twoFactorSecret = await cryptoWorker.decryptB64(
encryptedTwoFactorSecret.encryptedData,
encryptedTwoFactorSecret.nonce,
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 { getActualKey, getToken } from 'utils/common/key';
import CryptoWorker from 'utils/crypto';
import { getPublicKey } from './userService';
import { B64EncryptionResult } from 'utils/crypto';
import HTTPService from './HTTPService';
import { EnteFile } from 'types/file';
import { logError } from 'utils/sentry';
@ -28,6 +26,9 @@ import {
CollectionSummaries,
CollectionSummary,
CollectionFilesCount,
EncryptedCollection,
CollectionMagicMetadata,
CollectionMagicMetadataProps,
} from 'types/collection';
import {
COLLECTION_SORT_BY,
@ -38,61 +39,77 @@ import {
ALL_SECTION,
CollectionSummaryType,
} from 'constants/collection';
import { UpdateMagicMetadataRequest } from 'types/magicMetadata';
import { EncryptionResult } from 'types/upload';
import {
NEW_COLLECTION_MAGIC_METADATA,
SUB_TYPE,
UpdateMagicMetadataRequest,
} from 'types/magicMetadata';
import constants from 'utils/strings/constants';
import { IsArchived } from 'utils/magicMetadata';
import { IsArchived, updateMagicMetadataProps } from 'utils/magicMetadata';
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 COLLECTION_TABLE = 'collections';
const COLLECTION_UPDATION_TIME = 'collection-updation-time';
const getCollectionWithSecrets = async (
collection: Collection,
collection: EncryptedCollection,
masterKey: string
) => {
const worker = await new CryptoWorker();
): Promise<Collection> => {
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
const userID = getData(LS_KEYS.USER).id;
let decryptedKey: string;
let collectionKey: string;
if (collection.owner.id === userID) {
decryptedKey = await worker.decryptB64(
collectionKey = await cryptoWorker.decryptB64(
collection.encryptedKey,
collection.keyDecryptionNonce,
masterKey
);
} else {
const keyAttributes = getData(LS_KEYS.KEY_ATTRIBUTES);
const secretKey = await worker.decryptB64(
const secretKey = await cryptoWorker.decryptB64(
keyAttributes.encryptedSecretKey,
keyAttributes.secretKeyDecryptionNonce,
masterKey
);
decryptedKey = await worker.boxSealOpen(
collectionKey = await cryptoWorker.boxSealOpen(
collection.encryptedKey,
keyAttributes.publicKey,
secretKey
);
}
collection.name =
const collectionName =
collection.name ||
(await worker.decryptToUTF8(
(await cryptoWorker.decryptToUTF8(
collection.encryptedName,
collection.nameDecryptionNonce,
decryptedKey
collectionKey
));
let collectionMagicMetadata: CollectionMagicMetadata;
if (collection.magicMetadata?.data) {
collection.magicMetadata.data = await worker.decryptMetadata(
collection.magicMetadata.data,
collection.magicMetadata.header,
decryptedKey
);
collectionMagicMetadata = {
...collection.magicMetadata,
data: await cryptoWorker.decryptMetadata(
collection.magicMetadata.data,
collection.magicMetadata.header,
collectionKey
),
};
}
return {
...collection,
key: decryptedKey,
name: collectionName,
key: collectionKey,
magicMetadata: collectionMagicMetadata,
};
};
@ -109,27 +126,25 @@ const getCollections = async (
},
{ 'X-Auth-Token': token }
);
const promises: Promise<Collection>[] = resp.data.collections.map(
async (collection: Collection) => {
if (collection.isDeleted) {
return collection;
const decryptedCollections: Collection[] = await Promise.all(
resp.data.collections.map(
async (collection: EncryptedCollection) => {
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
const collections = (await Promise.all(promises)).filter(
const collections = decryptedCollections.filter(
(collection) => collection.isDeleted || collection.key
);
return collections;
@ -274,25 +289,15 @@ export const createCollection = async (
return collection;
}
}
const worker = await new CryptoWorker();
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
const encryptionKey = await getActualKey();
const token = getToken();
const collectionKey: string = await worker.generateEncryptionKey();
const {
encryptedData: encryptedKey,
nonce: keyDecryptionNonce,
}: B64EncryptionResult = await worker.encryptToB64(
collectionKey,
encryptionKey
);
const {
encryptedData: encryptedName,
nonce: nameDecryptionNonce,
}: B64EncryptionResult = await worker.encryptUTF8(
collectionName,
collectionKey
);
const newCollection: Collection = {
const collectionKey = await cryptoWorker.generateEncryptionKey();
const { encryptedData: encryptedKey, nonce: keyDecryptionNonce } =
await cryptoWorker.encryptToB64(collectionKey, encryptionKey);
const { encryptedData: encryptedName, nonce: nameDecryptionNonce } =
await cryptoWorker.encryptUTF8(collectionName, collectionKey);
const newCollection: EncryptedCollection = {
id: null,
owner: null,
encryptedKey,
@ -306,15 +311,12 @@ export const createCollection = async (
isDeleted: false,
magicMetadata: null,
};
let createdCollection: Collection = await postCollection(
newCollection,
token
);
createdCollection = await getCollectionWithSecrets(
const createdCollection = await postCollection(newCollection, token);
const decryptedCreatedCollection = await getCollectionWithSecrets(
createdCollection,
encryptionKey
);
return createdCollection;
return decryptedCreatedCollection;
} catch (e) {
logError(e, 'create collection failed');
throw e;
@ -322,9 +324,9 @@ export const createCollection = async (
};
const postCollection = async (
collectionData: Collection,
collectionData: EncryptedCollection,
token: string
): Promise<Collection> => {
): Promise<EncryptedCollection> => {
try {
const response = await HTTPService.post(
`${ENDPOINT}/collections`,
@ -457,19 +459,19 @@ const encryptWithNewCollectionKey = async (
files: EnteFile[]
): Promise<EncryptedFileKey[]> => {
const fileKeysEncryptedWithNewCollection: EncryptedFileKey[] = [];
const worker = await new CryptoWorker();
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
for (const file of files) {
const newEncryptedKey: B64EncryptionResult = await worker.encryptToB64(
const newEncryptedKey = await cryptoWorker.encryptToB64(
file.key,
newCollection.key
);
file.encryptedKey = newEncryptedKey.encryptedData;
file.keyDecryptionNonce = newEncryptedKey.nonce;
const encryptedKey = newEncryptedKey.encryptedData;
const keyDecryptionNonce = newEncryptedKey.nonce;
fileKeysEncryptedWithNewCollection.push({
id: file.id,
encryptedKey: file.encryptedKey,
keyDecryptionNonce: file.keyDecryptionNonce,
encryptedKey,
keyDecryptionNonce,
});
}
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) => {
const token = getToken();
if (!token) {
return;
}
const worker = await new CryptoWorker();
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
const { file: encryptedMagicMetadata }: EncryptionResult =
await worker.encryptMetadata(
collection.magicMetadata.data,
collection.key
);
const { file: encryptedMagicMetadata } = await cryptoWorker.encryptMetadata(
collection.magicMetadata.data,
collection.key
);
const reqBody: UpdateMagicMetadataRequest = {
id: collection.id,
magicMetadata: {
version: collection.magicMetadata.version,
count: collection.magicMetadata.count,
data: encryptedMagicMetadata.encryptedData as unknown as string,
data: encryptedMagicMetadata.encryptedData,
header: encryptedMagicMetadata.decryptionHeader,
},
};
@ -559,15 +576,14 @@ export const renameCollection = async (
collection: Collection,
newCollectionName: string
) => {
if (isQuickLinkCollection(collection)) {
// Convert quick link collction to normal collection on rename
await updateCollectionSubType(collection, SUB_TYPE.DEFAULT);
}
const token = getToken();
const worker = await new CryptoWorker();
const {
encryptedData: encryptedName,
nonce: nameDecryptionNonce,
}: B64EncryptionResult = await worker.encryptUTF8(
newCollectionName,
collection.key
);
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
const { encryptedData: encryptedName, nonce: nameDecryptionNonce } =
await cryptoWorker.encryptUTF8(newCollectionName, collection.key);
const collectionRenameRequest = {
collectionID: collection.id,
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 (
collection: Collection,
withUserEmail: string
) => {
try {
const worker = await new CryptoWorker();
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
const token = getToken();
const publicKey: string = await getPublicKey(withUserEmail);
const encryptedKey: string = await worker.boxSeal(
const encryptedKey = await cryptoWorker.boxSeal(
collection.key,
publicKey
);
@ -736,11 +770,6 @@ export function sortCollectionSummaries(
return collectionSummaries
.sort((a, b) => {
switch (sortBy) {
case COLLECTION_SORT_BY.CREATION_TIME_DESCENDING:
return compareCollectionsLatestFile(
b.latestFile,
a.latestFile
);
case COLLECTION_SORT_BY.CREATION_TIME_ASCENDING:
return (
-1 *
@ -798,12 +827,15 @@ export function getCollectionSummaries(
latestFile: collectionLatestFiles.get(collection.id),
fileCount: collectionFilesCount.get(collection.id),
updationTime: collection.updationTime,
type:
collection.owner.id !== user.id
? CollectionSummaryType.shared
: IsArchived(collection)
? CollectionSummaryType.archived
: CollectionSummaryType[collection.type],
type: isSharedWithMe(collection, user)
? CollectionSummaryType.incomingShare
: isSharedByMe(collection)
? CollectionSummaryType.outgoingShare
: isSharedOnlyViaLink(collection)
? CollectionSummaryType.sharedOnlyViaLink
: IsArchived(collection)
? CollectionSummaryType.archived
: CollectionSummaryType[collection.type],
});
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -14,7 +14,7 @@ import { getCollection } from './collectionService';
import { EnteFile } from 'types/file';
import HTTPService from './HTTPService';
import { Trash, TrashItem } from 'types/trash';
import { EncryptedTrashItem, Trash } from 'types/trash';
const TRASH = 'file-trash';
const TRASH_TIME = 'trash-time';
@ -99,7 +99,8 @@ export const updateTrash = async (
'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;
let collection = collections.get(collectionID);
if (!collection) {
@ -110,19 +111,21 @@ export const updateTrash = async (
]);
}
if (!trashItem.isDeleted && !trashItem.isRestored) {
trashItem.file = await decryptFile(
const decryptedFile = await decryptFile(
trashItem.file,
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) {
time = resp.data.diff.slice(-1)[0].updatedAt;
}
updatedTrash = removeDuplicates(updatedTrash);
updatedTrash = removeRestoredOrDeletedTrashItems(updatedTrash);
setFiles(
preservePhotoswipeProps(
@ -145,28 +148,6 @@ export const updateTrash = async (
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) {
return mergeMetadata(
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) {
const { stream, chunkCount } = fileData;
@ -33,7 +34,7 @@ async function encryptFileStream(worker, fileData: DataStream) {
export async function encryptFiledata(
worker,
filedata: Uint8Array | DataStream
): Promise<EncryptionResult> {
): Promise<EncryptionResult<Uint8Array | DataStream>> {
return isDataStream(filedata)
? await encryptFileStream(worker, filedata)
: await worker.encryptFile(filedata);

View file

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

View file

@ -1,11 +1,13 @@
import { FILE_READER_CHUNK_SIZE } from 'constants/upload';
import { getFileStream, getElectronFileStream } from 'services/readerService';
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';
export async function getFileHash(file: File | ElectronFile) {
export async function getFileHash(worker, file: File | ElectronFile) {
try {
addLogLine(`getFileHash called for ${getFileNameSize(file)}`);
let filedata: DataStream;
if (file instanceof File) {
filedata = getFileStream(file, FILE_READER_CHUNK_SIZE);
@ -15,22 +17,29 @@ export async function getFileHash(file: File | ElectronFile) {
FILE_READER_CHUNK_SIZE
);
}
const cryptoWorker = await new CryptoWorker();
const hashState = await cryptoWorker.initChunkHashing();
const hashState = await worker.initChunkHashing();
const reader = filedata.stream.getReader();
// eslint-disable-next-line no-constant-condition
while (true) {
const { done, value: chunk } = await reader.read();
const streamReader = filedata.stream.getReader();
for (let i = 0; i < filedata.chunkCount; i++) {
const { done, value: chunk } = await streamReader.read();
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;
} catch (e) {
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 { LIVE_PHOTO_ASSET_SIZE_LIMIT } from 'constants/upload';
import { encodeMotionPhoto } from 'services/motionPhotoService';
import { getFileType } from 'services/typeDetectionService';
import {
ElectronFile,
FileTypeInfo,
FileWithCollection,
LivePhotoAssets,
Metadata,
ParsedMetadataJSONMap,
} from 'types/upload';
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 { getUint8ArrayView } from '../readerService';
import { extractFileMetadata } from './fileService';
import { getFileHash } from './hashService';
import { generateThumbnail } from './thumbnailService';
import uploadService from './uploadService';
import UploadService from './uploadService';
import uploadCancelService from './uploadCancelService';
interface LivePhotoIdentifier {
collectionID: number;
@ -23,56 +26,58 @@ interface LivePhotoIdentifier {
size: number;
}
interface Asset {
file: File | ElectronFile;
metadata: Metadata;
fileTypeInfo: FileTypeInfo;
}
const ENTE_LIVE_PHOTO_FORMAT = 'elp';
const UNDERSCORE_THREE = '_3';
const UNDERSCORE = '_';
export function getLivePhotoFileType(
imageFileTypeInfo: FileTypeInfo,
videoTypeInfo: FileTypeInfo
): FileTypeInfo {
export async function getLivePhotoFileType(
livePhotoAssets: LivePhotoAssets
): Promise<FileTypeInfo> {
const imageFileTypeInfo = await getFileType(livePhotoAssets.image);
const videoFileTypeInfo = await getFileType(livePhotoAssets.video);
return {
fileType: FILE_TYPE.LIVE_PHOTO,
exactType: `${imageFileTypeInfo.exactType}+${videoTypeInfo.exactType}`,
exactType: `${imageFileTypeInfo.exactType}+${videoFileTypeInfo.exactType}`,
imageType: imageFileTypeInfo.exactType,
videoType: videoTypeInfo.exactType,
videoType: videoFileTypeInfo.exactType,
};
}
export function getLivePhotoMetadata(
imageMetadata: Metadata,
videoMetadata: Metadata
export async function extractLivePhotoMetadata(
worker,
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 {
...imageMetadata,
title: getLivePhotoName(imageMetadata.title),
title: getLivePhotoName(livePhotoAssets),
fileType: FILE_TYPE.LIVE_PHOTO,
imageHash: imageMetadata.hash,
videoHash: videoMetadata.hash,
videoHash: videoHash,
hash: undefined,
};
}
export function getLivePhotoFilePath(imageAsset: Asset): string {
return getLivePhotoName((imageAsset.file as any).path);
}
export function getLivePhotoSize(livePhotoAssets: LivePhotoAssets) {
return livePhotoAssets.image.size + livePhotoAssets.video.size;
}
export function getLivePhotoName(imageTitle: string) {
return `${
splitFilenameAndExtension(imageTitle)[0]
}.${ENTE_LIVE_PHOTO_FORMAT}`;
export function getLivePhotoName(livePhotoAssets: LivePhotoAssets) {
return livePhotoAssets.image.name;
}
export async function readLivePhoto(
@ -103,7 +108,7 @@ export async function readLivePhoto(
};
}
export function clusterLivePhotoFiles(mediaFiles: FileWithCollection[]) {
export async function clusterLivePhotoFiles(mediaFiles: FileWithCollection[]) {
try {
const analysedMediaFiles: FileWithCollection[] = [];
mediaFiles
@ -120,59 +125,48 @@ export function clusterLivePhotoFiles(mediaFiles: FileWithCollection[]) {
);
let index = 0;
while (index < mediaFiles.length - 1) {
if (uploadCancelService.isUploadCancelationRequested()) {
throw Error(CustomError.UPLOAD_CANCELLED);
}
const firstMediaFile = mediaFiles[index];
const secondMediaFile = mediaFiles[index + 1];
const {
fileTypeInfo: firstFileTypeInfo,
metadata: firstFileMetadata,
} = UploadService.getFileMetadataAndFileTypeInfo(
firstMediaFile.localID
);
const {
fileTypeInfo: secondFileFileInfo,
metadata: secondFileMetadata,
} = UploadService.getFileMetadataAndFileTypeInfo(
secondMediaFile.localID
);
const firstFileType =
getFileTypeFromExtensionForLivePhotoClustering(
firstMediaFile.file.name
);
const secondFileType =
getFileTypeFromExtensionForLivePhotoClustering(
secondMediaFile.file.name
);
const firstFileIdentifier: LivePhotoIdentifier = {
collectionID: firstMediaFile.collectionID,
fileType: firstFileTypeInfo.fileType,
fileType: firstFileType,
name: firstMediaFile.file.name,
size: firstMediaFile.file.size,
};
const secondFileIdentifier: LivePhotoIdentifier = {
collectionID: secondMediaFile.collectionID,
fileType: secondFileFileInfo.fileType,
fileType: secondFileType,
name: secondMediaFile.file.name,
size: secondMediaFile.file.size,
};
const firstAsset = {
file: firstMediaFile.file,
metadata: firstFileMetadata,
fileTypeInfo: firstFileTypeInfo,
};
const secondAsset = {
file: secondMediaFile.file,
metadata: secondFileMetadata,
fileTypeInfo: secondFileFileInfo,
};
if (
areFilesLivePhotoAssets(
firstFileIdentifier,
secondFileIdentifier
)
) {
let imageAsset: Asset;
let videoAsset: Asset;
let imageFile: File | ElectronFile;
let videoFile: File | ElectronFile;
if (
firstFileTypeInfo.fileType === FILE_TYPE.IMAGE &&
secondFileFileInfo.fileType === FILE_TYPE.VIDEO
firstFileType === FILE_TYPE.IMAGE &&
secondFileType === FILE_TYPE.VIDEO
) {
imageAsset = firstAsset;
videoAsset = secondAsset;
imageFile = firstMediaFile.file;
videoFile = secondMediaFile.file;
} else {
videoAsset = firstAsset;
imageAsset = secondAsset;
videoFile = firstMediaFile.file;
imageFile = secondMediaFile.file;
}
const livePhotoLocalID = firstMediaFile.localID;
analysedMediaFiles.push({
@ -180,25 +174,10 @@ export function clusterLivePhotoFiles(mediaFiles: FileWithCollection[]) {
collectionID: firstMediaFile.collectionID,
isLivePhoto: true,
livePhotoAssets: {
image: imageAsset.file,
video: videoAsset.file,
image: imageFile,
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;
} else {
analysedMediaFiles.push({
@ -216,8 +195,12 @@ export function clusterLivePhotoFiles(mediaFiles: FileWithCollection[]) {
}
return analysedMediaFiles;
} catch (e) {
logError(e, 'failed to cluster live photo');
throw e;
if (e.message === CustomError.UPLOAD_CANCELLED) {
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(
worker,
receivedFile: File | ElectronFile,
fileTypeInfo: FileTypeInfo
) {
@ -39,7 +40,7 @@ export async function extractMetadata(
} else if (fileTypeInfo.fileType === FILE_TYPE.VIDEO) {
extractedMetadata = await getVideoMetadata(receivedFile);
}
const fileHash = await getFileHash(receivedFile);
const fileHash = await getFileHash(worker, receivedFile);
const metadata: Metadata = {
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 { BLACK_THUMBNAIL_BASE64 } from 'constants/upload';
import * as FFmpegService from 'services/ffmpeg/ffmpegService';
import ElectronImageProcessorService from 'services/electron/imageProcessor';
import { convertBytesToHumanReadable } from 'utils/file/size';
import { isExactTypeHEIC } from 'utils/file';
import { ElectronFile, FileTypeInfo } from 'types/upload';
@ -30,42 +31,24 @@ export async function generateThumbnail(
try {
addLogLine(`generating thumbnail for ${getFileNameSize(file)}`);
let hasStaticThumbnail = false;
let canvas = document.createElement('canvas');
let thumbnail: Uint8Array;
try {
if (fileTypeInfo.fileType === FILE_TYPE.IMAGE) {
const isHEIC = isExactTypeHEIC(fileTypeInfo.exactType);
canvas = await generateImageThumbnail(file, isHEIC);
thumbnail = await generateImageThumbnail(file, fileTypeInfo);
} else {
try {
addLogLine(
`ffmpeg generateThumbnail called for ${getFileNameSize(
file
)}`
);
const thumbFile =
await FFmpegService.generateVideoThumbnail(file);
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);
}
thumbnail = await generateVideoThumbnail(file, fileTypeInfo);
}
if (thumbnail.length > 1.5 * MAX_THUMBNAIL_SIZE) {
logError(
Error('thumbnail_too_large'),
'thumbnail greater than max limit',
{
thumbnailSize: convertBytesToHumanReadable(
thumbnail.length
),
}
);
}
const thumbnailBlob = await thumbnailCanvasToBlob(canvas);
thumbnail = await getUint8ArrayView(thumbnailBlob);
if (thumbnail.length === 0) {
throw Error('EMPTY THUMBNAIL');
}
@ -93,16 +76,42 @@ export async function generateThumbnail(
}
}
export async function generateImageThumbnail(
async function generateImageThumbnail(
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 canvasCTX = canvas.getContext('2d');
let imageURL = null;
let timeout = null;
const isHEIC = isExactTypeHEIC(fileTypeInfo.exactType);
if (isHEIC) {
addLogLine(`HEICConverter called for ${getFileNameSize(file)}`);
const convertedBlob = await HeicConversionService.convert(
@ -151,10 +160,42 @@ export async function generateImageThumbnail(
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 canvasCTX = canvas.getContext('2d');
@ -205,10 +246,11 @@ export async function generateVideoThumbnail(file: File | ElectronFile) {
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 prevSize = Number.MAX_SAFE_INTEGER;
let quality = MAX_QUALITY;
@ -234,13 +276,6 @@ async function thumbnailCanvasToBlob(canvas: HTMLCanvasElement) {
percentageSizeDiff(thumbnailBlob.size, prevSize) >=
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;
}

View file

@ -1,6 +1,5 @@
import { getLocalFiles } from '../fileService';
import { SetFiles } from 'types/gallery';
import { getDedicatedCryptoWorker } from 'utils/crypto';
import {
sortFiles,
preservePhotoswipeProps,
@ -18,23 +17,15 @@ import UIService from './uiService';
import UploadService from './uploadService';
import { CustomError } from 'utils/error';
import { Collection } from 'types/collection';
import { EnteFile } from 'types/file';
import { EncryptedEnteFile, EnteFile } from 'types/file';
import {
ElectronFile,
FileWithCollection,
Metadata,
MetadataAndFileTypeInfo,
MetadataAndFileTypeInfoMap,
ParsedMetadataJSON,
ParsedMetadataJSONMap,
PublicUploadProps,
} from 'types/upload';
import {
UPLOAD_RESULT,
MAX_FILE_SIZE_SUPPORTED,
UPLOAD_STAGES,
} from 'constants/upload';
import { ComlinkWorker } from 'utils/comlink';
import { FILE_TYPE } from 'constants/file';
import { UPLOAD_RESULT, UPLOAD_STAGES } from 'constants/upload';
import uiService from './uiService';
import { addLogLine, getFileNameSize } from 'utils/logging';
import isElectron from 'is-electron';
@ -42,14 +33,23 @@ import ImportService from 'services/importService';
import watchFolderService from 'services/watchFolder/watchFolderService';
import { ProgressUpdater } from 'types/upload/ui';
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 FILE_UPLOAD_COMPLETED = 100;
class UploadManager {
private cryptoWorkers = new Array<ComlinkWorker>(MAX_CONCURRENT_UPLOADS);
private cryptoWorkers = new Array<
ComlinkWorker<typeof DedicatedCryptoWorker>
>(MAX_CONCURRENT_UPLOADS);
private parsedMetadataJSONMap: ParsedMetadataJSONMap;
private metadataAndFileTypeInfoMap: MetadataAndFileTypeInfoMap;
private filesToBeUploaded: FileWithCollection[];
private remainingFiles: FileWithCollection[] = [];
private failedFiles: FileWithCollection[];
@ -58,12 +58,18 @@ class UploadManager {
private setFiles: SetFiles;
private collections: Map<number, Collection>;
private uploadInProgress: boolean;
private publicUploadProps: PublicUploadProps;
private uploaderName: string;
public async init(progressUpdater: ProgressUpdater, setFiles: SetFiles) {
UIService.init(progressUpdater);
this.setFiles = setFiles;
public async init(
progressUpdater: ProgressUpdater,
setFiles: SetFiles,
publicCollectProps: PublicUploadProps
) {
UIService.init(progressUpdater);
UploadService.init(publicCollectProps);
this.setFiles = setFiles;
this.publicUploadProps = publicCollectProps;
}
public isUploadRunning() {
@ -75,10 +81,8 @@ class UploadManager {
this.remainingFiles = [];
this.failedFiles = [];
this.parsedMetadataJSONMap = new Map<string, ParsedMetadataJSON>();
this.metadataAndFileTypeInfoMap = new Map<
number,
MetadataAndFileTypeInfo
>();
this.uploaderName = null;
}
prepareForNewUpload() {
@ -89,10 +93,17 @@ class UploadManager {
}
async updateExistingFilesAndCollections(collections: Collection[]) {
this.existingFiles = await getLocalFiles();
this.userOwnedNonTrashedExistingFiles = getUserOwnedNonTrashedFiles(
this.existingFiles
);
if (this.publicUploadProps.accessedThroughSharedURL) {
this.existingFiles = await getLocalPublicFiles(
getPublicCollectionUID(this.publicUploadProps.token)
);
this.userOwnedNonTrashedExistingFiles = this.existingFiles;
} else {
this.existingFiles = await getLocalFiles();
this.userOwnedNonTrashedExistingFiles = getUserOwnedNonTrashedFiles(
this.existingFiles
);
}
this.collections = new Map(
collections.map((collection) => [collection.id, collection])
);
@ -100,7 +111,8 @@ class UploadManager {
public async queueFilesForUpload(
filesWithCollectionToUploadIn: FileWithCollection[],
collections: Collection[]
collections: Collection[],
uploaderName?: string
) {
try {
if (this.uploadInProgress) {
@ -108,6 +120,7 @@ class UploadManager {
}
this.uploadInProgress = true;
await this.updateExistingFilesAndCollections(collections);
this.uploaderName = uploaderName;
addLogLine(
`received ${filesWithCollectionToUploadIn.length} files to upload`
);
@ -134,50 +147,18 @@ class UploadManager {
);
}
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`);
const analysedMediaFiles =
UploadService.clusterLivePhotoFiles(filesWithMetadata);
await UploadService.clusterLivePhotoFiles(mediaFiles);
addLogLine(`clusterLivePhotoFiles ended`);
const allFiles = [...rejectedFiles, ...analysedMediaFiles];
addLogLine(
`got live photos: ${mediaFiles.length !== allFiles.length}`
`got live photos: ${
mediaFiles.length !== analysedMediaFiles.length
}`
);
uiService.setFilenames(
new Map<number, string>(
allFiles.map((mediaFile) => [
analysedMediaFiles.map((mediaFile) => [
mediaFile.localID,
UploadService.getAssetName(mediaFile),
])
@ -185,10 +166,10 @@ class UploadManager {
);
UIService.setHasLivePhoto(
mediaFiles.length !== allFiles.length
mediaFiles.length !== analysedMediaFiles.length
);
await this.uploadMediaFiles(allFiles);
await this.uploadMediaFiles(analysedMediaFiles);
}
} catch (e) {
if (e.message === CustomError.UPLOAD_CANCELLED) {
@ -203,7 +184,7 @@ class UploadManager {
UIService.setUploadStage(UPLOAD_STAGES.FINISH);
UIService.setPercentComplete(FILE_UPLOAD_COMPLETED);
for (let i = 0; i < MAX_CONCURRENT_UPLOADS; i++) {
this.cryptoWorkers[i]?.worker.terminate();
this.cryptoWorkers[i]?.terminate();
}
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[]) {
addLogLine(`uploadMediaFiles called`);
this.filesToBeUploaded = [...this.filesToBeUploaded, ...mediaFiles];
@ -386,21 +274,14 @@ class UploadManager {
i < MAX_CONCURRENT_UPLOADS && this.filesToBeUploaded.length > 0;
i++
) {
const cryptoWorker = getDedicatedCryptoWorker();
if (!cryptoWorker) {
throw Error(CustomError.FAILED_TO_LOAD_WEB_WORKER);
}
this.cryptoWorkers[i] = cryptoWorker;
uploadProcesses.push(
this.uploadNextFileInQueue(
await new this.cryptoWorkers[i].comlink()
)
);
this.cryptoWorkers[i] = getDedicatedCryptoWorker();
const worker = await new this.cryptoWorkers[i].remote();
uploadProcesses.push(this.uploadNextFileInQueue(worker));
}
await Promise.all(uploadProcesses);
}
private async uploadNextFileInQueue(worker: any) {
private async uploadNextFileInQueue(worker: Remote<DedicatedCryptoWorker>) {
while (this.filesToBeUploaded.length > 0) {
if (uploadCancelService.isUploadCancelationRequested()) {
throw Error(CustomError.UPLOAD_CANCELLED);
@ -412,7 +293,9 @@ class UploadManager {
const { fileUploadResult, uploadedFile } = await uploader(
worker,
this.userOwnedNonTrashedExistingFiles,
fileWithCollection
fileWithCollection,
this.uploaderName,
this.publicUploadProps?.accessedThroughSharedURL
);
const finalUploadResult = await this.postUploadTask(
@ -431,7 +314,7 @@ class UploadManager {
async postUploadTask(
fileUploadResult: UPLOAD_RESULT,
uploadedFile: EnteFile | null,
uploadedFile: EncryptedEnteFile | EnteFile | null,
fileWithCollection: FileWithCollection
) {
try {
@ -446,22 +329,23 @@ class UploadManager {
this.failedFiles.push(fileWithCollection);
break;
case UPLOAD_RESULT.ALREADY_UPLOADED:
decryptedFile = uploadedFile;
decryptedFile = uploadedFile as EnteFile;
break;
case UPLOAD_RESULT.ADDED_SYMLINK:
decryptedFile = uploadedFile;
decryptedFile = uploadedFile as EnteFile;
fileUploadResult = UPLOAD_RESULT.UPLOADED;
break;
case UPLOAD_RESULT.UPLOADED:
case UPLOAD_RESULT.UPLOADED_WITH_STATIC_THUMBNAIL:
decryptedFile = await decryptFile(
uploadedFile,
uploadedFile as EncryptedEnteFile,
fileWithCollection.collection.key
);
break;
case UPLOAD_RESULT.UNSUPPORTED:
case UPLOAD_RESULT.TOO_LARGE:
case UPLOAD_RESULT.CANCELLED:
case UPLOAD_RESULT.SKIPPED_VIDEOS:
// no-op
break;
default:
@ -479,7 +363,7 @@ class UploadManager {
await this.watchFolderCallback(
fileUploadResult,
fileWithCollection,
uploadedFile
uploadedFile as EncryptedEnteFile
);
return fileUploadResult;
} catch (e) {
@ -491,7 +375,7 @@ class UploadManager {
private async watchFolderCallback(
fileUploadResult: UPLOAD_RESULT,
fileWithCollection: FileWithCollection,
uploadedFile: EnteFile
uploadedFile: EncryptedEnteFile
) {
if (isElectron()) {
await watchFolderService.onFileUpload(
@ -508,13 +392,17 @@ class UploadManager {
uploadCancelService.requestUploadCancelation();
}
async getFailedFilesWithCollections() {
getFailedFilesWithCollections() {
return {
files: this.failedFiles,
collections: [...this.collections.values()],
};
}
getUploaderName() {
return this.uploaderName;
}
private updateExistingFiles(decryptedFile: EnteFile) {
if (!decryptedFile) {
throw Error("decrypted file can't be undefined");

View file

@ -5,26 +5,25 @@ import { extractFileMetadata, getFilename } from './fileService';
import { getFileType } from '../typeDetectionService';
import { CustomError, handleUploadError } from 'utils/error';
import {
B64EncryptionResult,
BackupedFile,
ElectronFile,
EncryptedFile,
FileTypeInfo,
FileWithCollection,
FileWithMetadata,
isDataStream,
Metadata,
MetadataAndFileTypeInfo,
MetadataAndFileTypeInfoMap,
ParsedMetadataJSON,
ParsedMetadataJSONMap,
ProcessedFile,
PublicUploadProps,
UploadAsset,
UploadFile,
UploadURL,
} from 'types/upload';
import {
clusterLivePhotoFiles,
extractLivePhotoMetadata,
getLivePhotoFileType,
getLivePhotoName,
getLivePhotoSize,
readLivePhoto,
@ -33,6 +32,12 @@ import { encryptFile, getFileSize, readFile } from './fileService';
import { uploadStreamUsingMultipart } from './multiPartUploadService';
import UIService from './uiService';
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 {
private uploadURLs: UploadURL[] = [];
@ -40,26 +45,32 @@ class UploadService {
string,
ParsedMetadataJSON
>();
private metadataAndFileTypeInfoMap: MetadataAndFileTypeInfoMap = new Map<
number,
MetadataAndFileTypeInfo
>();
private uploaderName: string;
private pendingUploadCount: number = 0;
private publicUploadProps: PublicUploadProps = undefined;
init(publicUploadProps: PublicUploadProps) {
this.publicUploadProps = publicUploadProps;
}
async setFileCount(fileCount: number) {
this.pendingUploadCount = fileCount;
await this.preFetchUploadURLs();
this.preFetchUploadURLs();
}
setParsedMetadataJSONMap(parsedMetadataJSONMap: ParsedMetadataJSONMap) {
this.parsedMetadataJSONMap = parsedMetadataJSONMap;
}
setMetadataAndFileTypeInfoMap(
metadataAndFileTypeInfoMap: MetadataAndFileTypeInfoMap
) {
this.metadataAndFileTypeInfoMap = metadataAndFileTypeInfoMap;
setUploaderName(uploaderName: string) {
this.uploaderName = uploaderName;
}
getUploaderName() {
return this.uploaderName;
}
reducePendingUploadCount() {
@ -72,14 +83,16 @@ class UploadService {
: getFileSize(file);
}
getAssetName({ isLivePhoto, file, livePhotoAssets }: FileWithCollection) {
getAssetName({ isLivePhoto, file, livePhotoAssets }: UploadAsset) {
return isLivePhoto
? getLivePhotoName(livePhotoAssets.image.name)
? getLivePhotoName(livePhotoAssets)
: getFilename(file);
}
async getFileType(file: File | ElectronFile) {
return getFileType(file);
getAssetFileType({ isLivePhoto, file, livePhotoAssets }: UploadAsset) {
return isLivePhoto
? getLivePhotoFileType(livePhotoAssets)
: getFileType(file);
}
async readAsset(
@ -91,39 +104,41 @@ class UploadService {
: await readFile(fileTypeInfo, file);
}
async extractFileMetadata(
file: File | ElectronFile,
async extractAssetMetadata(
worker,
{ isLivePhoto, file, livePhotoAssets }: UploadAsset,
collectionID: number,
fileTypeInfo: FileTypeInfo
): Promise<Metadata> {
return extractFileMetadata(
this.parsedMetadataJSONMap,
file,
collectionID,
fileTypeInfo
);
}
getFileMetadataAndFileTypeInfo(localID: number) {
return this.metadataAndFileTypeInfoMap.get(localID);
}
setFileMetadataAndFileTypeInfo(
localID: number,
metadataAndFileTypeInfo: MetadataAndFileTypeInfo
) {
return this.metadataAndFileTypeInfoMap.set(
localID,
metadataAndFileTypeInfo
);
return isLivePhoto
? extractLivePhotoMetadata(
worker,
this.parsedMetadataJSONMap,
collectionID,
fileTypeInfo,
livePhotoAssets
)
: await extractFileMetadata(
worker,
this.parsedMetadataJSONMap,
collectionID,
fileTypeInfo,
file
);
}
clusterLivePhotoFiles(mediaFiles: FileWithCollection[]) {
return clusterLivePhotoFiles(mediaFiles);
}
constructPublicMagicMetadata(
publicMagicMetadataProps: FilePublicMagicMetadataProps
) {
return constructPublicMagicMetadata(publicMagicMetadataProps);
}
async encryptAsset(
worker: any,
worker: Remote<DedicatedCryptoWorker>,
file: FileWithMetadata,
encryptionKey: string
): Promise<EncryptedFile> {
@ -146,13 +161,13 @@ class UploadService {
if (USE_CF_PROXY) {
fileObjectKey = await UploadHttpClient.putFileV2(
fileUploadURL,
file.file.encryptedData,
file.file.encryptedData as Uint8Array,
progressTracker
);
} else {
fileObjectKey = await UploadHttpClient.putFile(
fileUploadURL,
file.file.encryptedData,
file.file.encryptedData as Uint8Array,
progressTracker
);
}
@ -162,13 +177,13 @@ class UploadService {
if (USE_CF_PROXY) {
thumbnailObjectKey = await UploadHttpClient.putFileV2(
thumbnailUploadURL,
file.thumbnail.encryptedData as Uint8Array,
file.thumbnail.encryptedData,
null
);
} else {
thumbnailObjectKey = await UploadHttpClient.putFile(
thumbnailUploadURL,
file.thumbnail.encryptedData as Uint8Array,
file.thumbnail.encryptedData,
null
);
}
@ -183,6 +198,7 @@ class UploadService {
objectKey: thumbnailObjectKey,
},
metadata: file.metadata,
pubMagicMetadata: file.pubMagicMetadata,
};
return backupedFile;
} 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() {
await UploadHttpClient.fetchUploadURLs(
this.pendingUploadCount,
this.uploadURLs
);
if (this.publicUploadProps.accessedThroughSharedURL) {
await publicUploadHttpClient.fetchUploadURLs(
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 { logError } from 'utils/sentry';
import { findMatchingExistingFiles } from 'utils/upload';
import UploadHttpClient from './uploadHttpClient';
import UIService from './uiService';
import UploadService from './uploadService';
import { FILE_TYPE } from 'constants/file';
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 { convertBytesToHumanReadable } from 'utils/file/size';
import { sleep } from 'utils/common';
import { addToCollection } from 'services/collectionService';
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 {
fileUploadResult: UPLOAD_RESULT;
@ -20,9 +29,11 @@ interface UploadResponse {
}
export default async function uploader(
worker: any,
worker: Remote<DedicatedCryptoWorker>,
existingFiles: EnteFile[],
fileWithCollection: FileWithCollection
fileWithCollection: FileWithCollection,
uploaderName: string,
skipVideos: boolean
): Promise<UploadResponse> {
const { collection, localID, ...uploadAsset } = fileWithCollection;
const fileNameSize = `${UploadService.getAssetName(
@ -32,20 +43,33 @@ export default async function uploader(
addLogLine(`uploader called for ${fileNameSize}`);
UIService.setFileProgress(localID, 0);
await sleep(0);
const { fileTypeInfo, metadata } =
UploadService.getFileMetadataAndFileTypeInfo(localID);
let fileTypeInfo: FileTypeInfo;
try {
const fileSize = UploadService.getAssetSize(uploadAsset);
if (fileSize >= MAX_FILE_SIZE_SUPPORTED) {
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) {
throw Error(CustomError.UNSUPPORTED_FILE_FORMAT);
}
if (!metadata) {
throw Error(CustomError.NO_METADATA);
if (skipVideos && fileTypeInfo.fileType === FILE_TYPE.VIDEO) {
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(
existingFiles,
metadata
@ -100,11 +124,18 @@ export default async function uploader(
if (file.hasStaticThumbnail) {
metadata.hasStaticThumbnail = true;
}
const fileWithMetadata = {
let pubMagicMetadata: FilePublicMagicMetadata;
if (uploaderName) {
pubMagicMetadata = await uploadService.constructPublicMagicMetadata(
{ uploaderName }
);
}
const fileWithMetadata: FileWithMetadata = {
localID,
filedata: file.filedata,
thumbnail: file.thumbnail,
metadata,
pubMagicMetadata,
};
if (uploadCancelService.isUploadCancelationRequested()) {
@ -132,7 +163,7 @@ export default async function uploader(
encryptedFile.fileKey
);
const uploadedFile = await UploadHttpClient.uploadFile(uploadFile);
const uploadedFile = await UploadService.uploadFile(uploadFile);
UIService.increaseFileUploaded();
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 { getToken } from 'utils/common/key';
import HTTPService from './HTTPService';
import { B64EncryptionResult } from 'utils/crypto';
import { getRecoveryKey } from 'utils/crypto';
import { logError } from 'utils/sentry';
import {
KeyAttributes,
@ -23,6 +23,7 @@ import { ServerErrorCodes } from 'utils/error';
import isElectron from 'is-electron';
import safeStorageService from './electron/safeStorage';
import { deleteThumbnailCache } from './cacheService';
import { B64EncryptionResult } from 'types/crypto';
const ENDPOINT = getEndpoint();
@ -364,3 +365,13 @@ export const deleteAccount = async (challenge: string) => {
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;
try {
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(
'writeFile',
tempInputFilePath,
await getUint8ArrayView(inputFile)
);
tempOutputFilePath = `${generateTempName(10)}-${outputFileName}`;
tempOutputFilePath = `${generateTempName(10, outputFileName)}`;
cmd = cmd.map((cmdPart) => {
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 { CustomError } from 'utils/error';
import { createNewConvertWorker } from 'utils/heicConverter';
import { retryAsyncFunction } from 'utils/network';
import { logError } from 'utils/sentry';
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 MAX_CONVERSION_IN_PARALLEL = 1;
@ -17,7 +19,7 @@ class HEICConverter {
private convertProcessor = new QueueProcessor<Blob>(
MAX_CONVERSION_IN_PARALLEL
);
private workerPool: { comlink: any; worker: Worker }[];
private workerPool: ComlinkWorker<typeof DedicatedConvertWorker>[] = [];
private ready: Promise<void>;
constructor() {
@ -26,14 +28,15 @@ class HEICConverter {
private async init() {
this.workerPool = [];
for (let i = 0; i < WORKER_POOL_SIZE; i++) {
this.workerPool.push(await createNewConvertWorker());
this.workerPool.push(getDedicatedConvertWorker());
}
}
async convert(fileBlob: Blob): Promise<Blob> {
await this.ready;
const response = this.convertProcessor.queueUpRequest(() =>
retryAsyncFunction<Blob>(async () => {
const { comlink, worker } = this.workerPool.shift();
const convertWorker = this.workerPool.shift();
const worker = await new convertWorker.remote();
try {
const convertedHEIC = await new Promise<Blob>(
(resolve, reject) => {
@ -43,15 +46,15 @@ class HEICConverter {
reject(Error('wait time exceeded'));
}, WAIT_TIME_IN_MICROSECONDS);
const startTime = Date.now();
const convertedHEIC: Blob =
await comlink.convertHEIC(
const convertedHEIC =
await worker.convertHEIC(
fileBlob,
CONVERT_FORMAT
);
addLogLine(
`originalFileSize:${makeHumanReadableStorage(
`originalFileSize:${convertBytesToHumanReadable(
fileBlob?.size
)},convertedFileSize:${makeHumanReadableStorage(
)},convertedFileSize:${convertBytesToHumanReadable(
convertedHEIC?.size
)}, heic conversion time: ${
Date.now() - startTime
@ -71,10 +74,10 @@ class HEICConverter {
Error(`converted heic fileSize is Zero`),
'converted heic fileSize is Zero',
{
originalFileSize: makeHumanReadableStorage(
originalFileSize: convertBytesToHumanReadable(
fileBlob?.size ?? 0
),
convertedFileSize: makeHumanReadableStorage(
convertedFileSize: convertBytesToHumanReadable(
convertedHEIC?.size ?? 0
),
}
@ -86,12 +89,12 @@ class HEICConverter {
BREATH_TIME_IN_MICROSECONDS
);
});
this.workerPool.push({ comlink, worker });
this.workerPool.push(convertWorker);
return convertedHEIC;
} catch (e) {
logError(e, 'heic conversion failed');
worker.terminate();
this.workerPool.push(await createNewConvertWorker());
convertWorker.terminate();
this.workerPool.push(getDedicatedConvertWorker());
throw e;
}
}, WAIT_TIME_BEFORE_NEXT_ATTEMPT_IN_MICROSECONDS)

View file

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

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