Merge pull request #282 from ente-io/master

release
This commit is contained in:
Abhinav Kumar 2022-01-13 16:23:27 +05:30 committed by GitHub
commit d3856b4063
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
100 changed files with 1461 additions and 1177 deletions

44
SECURITY.md Normal file
View file

@ -0,0 +1,44 @@
ente believes that working with security researchers across the globe is crucial to keeping our
users safe. If you believe you've found a security issue in our product or service, we encourage you to
notify us (security@ente.io). We welcome working with you to resolve the issue promptly. Thanks in advance!
# Disclosure Policy
- Let us know as soon as possible upon discovery of a potential security issue, and we'll make every
effort to quickly resolve the issue.
- Provide us a reasonable amount of time to resolve the issue before any disclosure to the public or a
third-party. We may publicly disclose the issue before resolving it, if appropriate.
- Make a good faith effort to avoid privacy violations, destruction of data, and interruption or
degradation of our service. Only interact with accounts you own or with explicit permission of the
account holder.
- If you would like to encrypt your report, please use the PGP key with long ID
`E273695C0403F34F74171932DF6DDDE98EBD2394` (available in the public keyserver pool).
# In-scope
- Security issues in any current release of ente. This includes the web app, desktop app,
and mobile apps (iOS and Android). Product downloads are available at https://ente.io. Source
code is available at https://github.com/ente-io.
# Exclusions
The following bug classes are out-of scope:
- Bugs that are already reported on any of ente's issue trackers (https://github.com/ente-io),
or that we already know of. Note that some of our issue tracking is private.
- Issues in an upstream software dependency (ex: Flutter, Next.js etc) which are already reported to the upstream maintainer.
- Attacks requiring physical access to a user's device.
- Self-XSS
- Issues related to software or protocols not under ente's control
- Vulnerabilities in outdated versions of ente
- Missing security best practices that do not directly lead to a vulnerability
- Issues that do not have any impact on the general public
While researching, we'd like to ask you to refrain from:
- Denial of service
- Spamming
- Social engineering (including phishing) of ente staff or contractors
- Any physical attempts against ente property or data centers
Thank you for helping keep ente and our users safe!

55
configUtil.js Normal file
View file

@ -0,0 +1,55 @@
const cp = require('child_process');
const { getIsSentryEnabled } = require('./sentryConfigUtil');
module.exports = {
COOP_COEP_HEADERS: {
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Embedder-Policy': 'require-corp',
},
WEB_SECURITY_HEADERS: {
'Strict-Transport-Security': ' max-age=63072000',
'X-Content-Type-Options': 'nosniff',
'X-Download-Options': 'noopen',
'X-Frame-Options': 'deny',
'X-XSS-Protection': '1; mode=block',
'Referrer-Policy': 'same-origin',
},
CSP_DIRECTIVES: {
'default-src': "'none'",
'img-src': "'self' blob:",
'style-src': "'self' 'unsafe-inline'",
'font-src ': "'self'; script-src 'self' 'unsafe-eval' blob:",
'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',
},
WORKBOX_CONFIG: {
swSrc: 'src/serviceWorker.js',
exclude: [/manifest\.json$/i],
},
ALL_ROUTES: '/(.*)',
buildCSPHeader: (directives) => ({
'Content-Security-Policy-Report-Only': Object.entries(
directives
).reduce((acc, [key, value]) => acc + `${key} ${value};`, ''),
}),
convertToNextHeaderFormat: (headers) =>
Object.entries(headers).map(([key, value]) => ({ key, value })),
getGitSha: () =>
cp.execSync('git rev-parse --short HEAD', {
cwd: __dirname,
encoding: 'utf8',
}),
getIsSentryEnabled: getIsSentryEnabled,
};

View file

@ -4,31 +4,57 @@ const withBundleAnalyzer = require('@next/bundle-analyzer')({
const withWorkbox = require('@ente-io/next-with-workbox'); const withWorkbox = require('@ente-io/next-with-workbox');
const { withSentryConfig } = require('@sentry/nextjs'); const { withSentryConfig } = require('@sentry/nextjs');
const { PHASE_DEVELOPMENT_SERVER } = require('next/constants');
const cp = require('child_process'); const {
const gitSha = cp.execSync('git rev-parse --short HEAD', { getGitSha,
cwd: __dirname, convertToNextHeaderFormat,
encoding: 'utf8', buildCSPHeader,
}); COOP_COEP_HEADERS,
WEB_SECURITY_HEADERS,
CSP_DIRECTIVES,
WORKBOX_CONFIG,
ALL_ROUTES,
getIsSentryEnabled,
} = require('./configUtil');
module.exports = withSentryConfig( const GIT_SHA = getGitSha();
withWorkbox(
withBundleAnalyzer({ const IS_SENTRY_ENABLED = getIsSentryEnabled();
env: {
SENTRY_RELEASE: gitSha, module.exports = (phase) =>
}, withSentryConfig(
workbox: { withWorkbox(
swSrc: 'src/serviceWorker.js', withBundleAnalyzer({
exclude: [/manifest\.json$/i], env: {
}, SENTRY_RELEASE: GIT_SHA,
// https://dev.to/marcinwosinek/how-to-add-resolve-fallback-to-webpack-5-in-nextjs-10-i6j },
webpack: (config, { isServer }) => { workbox: WORKBOX_CONFIG,
if (!isServer) {
config.resolve.fallback.fs = false; headers() {
} return [
return config; {
}, // Apply these headers to all routes in your application....
}) source: ALL_ROUTES,
), headers: convertToNextHeaderFormat({
{ release: gitSha } ...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

@ -1,6 +1,6 @@
{ {
"name": "bada-frame", "name": "bada-frame",
"version": "0.4.0", "version": "0.4.2",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",

View file

@ -1,18 +1,24 @@
import * as Sentry from '@sentry/nextjs'; import * as Sentry from '@sentry/nextjs';
import { getSentryTunnelUrl } from 'utils/common/apiUtil'; import { getSentryTunnelUrl } from 'utils/common/apiUtil';
import { getUserAnonymizedID } from 'utils/user'; import { getUserAnonymizedID } from 'utils/user';
import {
getSentryDSN,
getSentryENV,
getSentryRelease,
getIsSentryEnabled,
} from 'constants/sentry';
const SENTRY_DSN = const SENTRY_DSN = getSentryDSN();
process.env.NEXT_PUBLIC_SENTRY_DSN ?? const SENTRY_ENV = getSentryENV();
'https://860186db60c54c7fbacfe255124958e8@errors.ente.io/4'; const SENTRY_RELEASE = getSentryRelease();
const SENTRY_ENV = process.env.NEXT_PUBLIC_SENTRY_ENV ?? 'development'; const IS_ENABLED = getIsSentryEnabled();
Sentry.setUser({ id: getUserAnonymizedID() }); Sentry.setUser({ id: getUserAnonymizedID() });
Sentry.init({ Sentry.init({
dsn: SENTRY_DSN, dsn: SENTRY_DSN,
enabled: SENTRY_ENV !== 'development', enabled: IS_ENABLED,
environment: SENTRY_ENV, environment: SENTRY_ENV,
release: process.env.SENTRY_RELEASE, release: SENTRY_RELEASE,
attachStacktrace: true, attachStacktrace: true,
autoSessionTracking: false, autoSessionTracking: false,
tunnel: getSentryTunnelUrl(), tunnel: getSentryTunnelUrl(),

View file

@ -1,12 +1,20 @@
import * as Sentry from '@sentry/nextjs'; import * as Sentry from '@sentry/nextjs';
import {
getSentryDSN,
getSentryENV,
getSentryRelease,
getIsSentryEnabled,
} from 'constants/sentry';
const SENTRY_DSN = process.env.NEXT_PUBLIC_SENTRY_DSN ?? 'https://860186db60c54c7fbacfe255124958e8@errors.ente.io/4'; const SENTRY_DSN = getSentryDSN();
const SENTRY_ENV = process.env.NEXT_PUBLIC_SENTRY_ENV ?? 'development'; const SENTRY_ENV = getSentryENV();
const SENTRY_RELEASE = getSentryRelease();
const IS_ENABLED = getIsSentryEnabled();
Sentry.init({ Sentry.init({
dsn: SENTRY_DSN, dsn: SENTRY_DSN,
enabled: SENTRY_ENV !== 'development', enabled: IS_ENABLED,
environment: SENTRY_ENV, environment: SENTRY_ENV,
release: process.env.SENTRY_RELEASE, release: SENTRY_RELEASE,
autoSessionTracking: false, autoSessionTracking: false,
}); });

10
sentryConfigUtil.js Normal file
View file

@ -0,0 +1,10 @@
module.exports.getIsSentryEnabled = () => {
if (process.env.NEXT_PUBLIC_IS_SENTRY_ENABLED) {
return process.env.NEXT_PUBLIC_IS_SENTRY_ENABLED === 'yes';
} else {
if (process.env.NEXT_PUBLIC_SENTRY_ENV) {
return process.env.NEXT_PUBLIC_SENTRY_ENV !== 'development';
}
}
return false;
};

View file

@ -9,7 +9,7 @@ import { changeEmail, getOTTForEmailChange } from 'services/userService';
import styled from 'styled-components'; import styled from 'styled-components';
import { AppContext, FLASH_MESSAGE_TYPE } from 'pages/_app'; import { AppContext, FLASH_MESSAGE_TYPE } from 'pages/_app';
import { getData, LS_KEYS, setData } from 'utils/storage/localStorage'; import { getData, LS_KEYS, setData } from 'utils/storage/localStorage';
import { PAGES } from 'types'; import { PAGES } from 'constants/pages';
interface formValues { interface formValues {
email: string; email: string;

View file

@ -6,15 +6,12 @@ import Form from 'react-bootstrap/Form';
import FormControl from 'react-bootstrap/FormControl'; import FormControl from 'react-bootstrap/FormControl';
import { Button, Col, Table } from 'react-bootstrap'; import { Button, Col, Table } from 'react-bootstrap';
import { DeadCenter } from 'pages/gallery'; import { DeadCenter } from 'pages/gallery';
import { User } from 'services/userService'; import { User } from 'types/user';
import { import { shareCollection, unshareCollection } from 'services/collectionService';
Collection,
shareCollection,
unshareCollection,
} from 'services/collectionService';
import { getData, LS_KEYS } from 'utils/storage/localStorage'; import { getData, LS_KEYS } from 'utils/storage/localStorage';
import SubmitButton from './SubmitButton'; import SubmitButton from './SubmitButton';
import MessageDialog from './MessageDialog'; import MessageDialog from './MessageDialog';
import { Collection } from 'types/collection';
interface Props { interface Props {
show: boolean; show: boolean;

View file

@ -1,20 +1,33 @@
import React from 'react'; import React from 'react';
import DatePicker from 'react-datepicker';
import 'react-datepicker/dist/react-datepicker.css';
import { import {
MIN_EDITED_CREATION_TIME, MIN_EDITED_CREATION_TIME,
MAX_EDITED_CREATION_TIME, MAX_EDITED_CREATION_TIME,
ALL_TIME, ALL_TIME,
} from 'services/fileService'; } from 'constants/file';
import DatePicker from 'react-datepicker';
import 'react-datepicker/dist/react-datepicker.css';
const isSameDay = (first, second) => const isSameDay = (first, second) =>
first.getFullYear() === second.getFullYear() && first.getFullYear() === second.getFullYear() &&
first.getMonth() === second.getMonth() && first.getMonth() === second.getMonth() &&
first.getDate() === second.getDate(); first.getDate() === second.getDate();
const EnteDateTimePicker = ({ isInEditMode, pickedTime, handleChange }) => ( interface Props {
loading?: boolean;
isInEditMode: boolean;
pickedTime: Date;
handleChange: (date: Date) => void;
}
const EnteDateTimePicker = ({
loading,
isInEditMode,
pickedTime,
handleChange,
}: Props) => (
<DatePicker <DatePicker
disabled={loading}
open={isInEditMode} open={isInEditMode}
selected={pickedTime} selected={pickedTime}
onChange={handleChange} onChange={handleChange}

View file

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { Button } from 'react-bootstrap'; import { Button } from 'react-bootstrap';
import { ExportStats } from 'services/exportService'; import { ExportStats } from 'types/export';
import { formatDateTime } from 'utils/file'; import { formatDateTime } from 'utils/file';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
import { Label, Row, Value } from './Container'; import { Label, Row, Value } from './Container';

View file

@ -1,8 +1,9 @@
import React from 'react'; import React from 'react';
import { Button, ProgressBar } from 'react-bootstrap'; import { Button, ProgressBar } from 'react-bootstrap';
import { ExportProgress, ExportStage } from 'services/exportService'; import { ExportProgress } from 'types/export';
import styled from 'styled-components'; import styled from 'styled-components';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
import { ExportStage } from 'constants/export';
export const ComfySpan = styled.span` export const ComfySpan = styled.span`
word-spacing: 1rem; word-spacing: 1rem;

View file

@ -1,14 +1,10 @@
import isElectron from 'is-electron'; import isElectron from 'is-electron';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Button } from 'react-bootstrap'; import { Button } from 'react-bootstrap';
import exportService, { import exportService from 'services/exportService';
ExportProgress, import { ExportProgress, ExportStats } from 'types/export';
ExportStage,
ExportStats,
ExportType,
} from 'services/exportService';
import { getLocalFiles } from 'services/fileService'; import { getLocalFiles } from 'services/fileService';
import { User } from 'services/userService'; import { User } from 'types/user';
import styled from 'styled-components'; import styled from 'styled-components';
import { sleep } from 'utils/common'; import { sleep } from 'utils/common';
import { getExportRecordFileUID } from 'utils/export'; import { getExportRecordFileUID } from 'utils/export';
@ -22,6 +18,7 @@ import ExportInProgress from './ExportInProgress';
import FolderIcon from './icons/FolderIcon'; import FolderIcon from './icons/FolderIcon';
import InProgressIcon from './icons/InProgressIcon'; import InProgressIcon from './icons/InProgressIcon';
import MessageDialog from './MessageDialog'; import MessageDialog from './MessageDialog';
import { ExportStage, ExportType } from 'constants/export';
const FolderIconWrapper = styled.div` const FolderIconWrapper = styled.div`
width: 15%; width: 15%;

View file

@ -3,14 +3,14 @@ import MessageDialog from '../MessageDialog';
import React, { useContext, useEffect, useState } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import { updateCreationTimeWithExif } from 'services/updateCreationTimeWithExif'; import { updateCreationTimeWithExif } from 'services/updateCreationTimeWithExif';
import { GalleryContext } from 'pages/gallery'; import { GalleryContext } from 'pages/gallery';
import { File } from 'services/fileService'; import { EnteFile } from 'types/file';
import FixCreationTimeRunning from './running'; import FixCreationTimeRunning from './running';
import FixCreationTimeFooter from './footer'; import FixCreationTimeFooter from './footer';
import { Formik } from 'formik'; import { Formik } from 'formik';
import FixCreationTimeOptions from './options'; import FixCreationTimeOptions from './options';
export interface FixCreationTimeAttributes { export interface FixCreationTimeAttributes {
files: File[]; files: EnteFile[];
} }
interface Props { interface Props {
@ -89,7 +89,6 @@ export default function FixCreationTime(props: Props) {
} }
const onSubmit = (values: formValues) => { const onSubmit = (values: formValues) => {
console.log(values);
startFix(Number(values.option), new Date(values.customTime)); startFix(Number(values.option), new Date(values.customTime));
}; };

View file

@ -11,7 +11,7 @@ import { setData, LS_KEYS, getData } from 'utils/storage/localStorage';
import SubmitButton from 'components/SubmitButton'; import SubmitButton from 'components/SubmitButton';
import Button from 'react-bootstrap/Button'; import Button from 'react-bootstrap/Button';
import LogoImg from './LogoImg'; import LogoImg from './LogoImg';
import { PAGES } from 'types'; import { PAGES } from 'constants/pages';
interface formValues { interface formValues {
email: string; email: string;

View file

@ -1,14 +1,8 @@
import { import { GalleryContext } from 'pages/gallery';
GalleryContext,
Search,
SelectedState,
SetFiles,
setSearchStats,
} from 'pages/gallery';
import PreviewCard from './pages/gallery/PreviewCard'; import PreviewCard from './pages/gallery/PreviewCard';
import React, { useContext, useEffect, useRef, useState } from 'react'; import React, { useContext, useEffect, useRef, useState } from 'react';
import { Button } from 'react-bootstrap'; import { Button } from 'react-bootstrap';
import { File, FILE_TYPE } from 'services/fileService'; import { EnteFile } from 'types/file';
import styled from 'styled-components'; import styled from 'styled-components';
import DownloadManager from 'services/downloadManager'; import DownloadManager from 'services/downloadManager';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
@ -21,10 +15,12 @@ import {
ALL_SECTION, ALL_SECTION,
ARCHIVE_SECTION, ARCHIVE_SECTION,
TRASH_SECTION, TRASH_SECTION,
} from './pages/gallery/Collections'; } from 'constants/collection';
import { isSharedFile } from 'utils/file'; import { isSharedFile } from 'utils/file';
import { isPlaybackPossible } from 'utils/photoFrame'; import { isPlaybackPossible } from 'utils/photoFrame';
import { PhotoList } from './PhotoList'; import { PhotoList } from './PhotoList';
import { SetFiles, SelectedState, Search, setSearchStats } from 'types/gallery';
import { FILE_TYPE } from 'constants/file';
const Container = styled.div` const Container = styled.div`
display: block; display: block;
@ -53,7 +49,7 @@ const EmptyScreen = styled.div`
`; `;
interface Props { interface Props {
files: File[]; files: EnteFile[];
setFiles: SetFiles; setFiles: SetFiles;
syncWithRemote: () => Promise<void>; syncWithRemote: () => Promise<void>;
favItemIds: Set<number>; favItemIds: Set<number>;
@ -134,7 +130,7 @@ const PhotoFrame = ({
onThumbnailClick(filteredDataIdx)(); onThumbnailClick(filteredDataIdx)();
} }
} }
}, [search]); }, [search, filteredData]);
const resetFetching = () => { const resetFetching = () => {
setFetching({}); setFetching({});
@ -289,14 +285,23 @@ const PhotoFrame = ({
if (selected.collectionID !== activeCollection) { if (selected.collectionID !== activeCollection) {
setSelected({ count: 0, collectionID: 0 }); setSelected({ count: 0, collectionID: 0 });
} }
if (checked) { if (typeof index !== 'undefined') {
setRangeStart(index); if (checked) {
setRangeStart(index);
} else {
setRangeStart(undefined);
}
} }
setSelected((selected) => ({ setSelected((selected) => ({
...selected, ...selected,
[id]: checked, [id]: checked,
count: checked ? selected.count + 1 : selected.count - 1, count:
selected[id] === checked
? selected.count
: checked
? selected.count + 1
: selected.count - 1,
collectionID: activeCollection, collectionID: activeCollection,
})); }));
}; };
@ -305,22 +310,28 @@ const PhotoFrame = ({
}; };
const handleRangeSelect = (index: number) => () => { const handleRangeSelect = (index: number) => () => {
if (rangeStart !== index) { if (typeof rangeStart !== 'undefined' && rangeStart !== index) {
let leftEnd = -1; const direction =
let rightEnd = -1; (index - rangeStart) / Math.abs(index - rangeStart);
if (index < rangeStart) { let checked = true;
leftEnd = index + 1; for (
rightEnd = rangeStart - 1; let i = rangeStart;
} else { (index - i) * direction >= 0;
leftEnd = rangeStart + 1; i += direction
rightEnd = index - 1; ) {
checked = checked && !!selected[filteredData[i].id];
} }
for (let i = leftEnd; i <= rightEnd; i++) { for (
handleSelect(filteredData[i].id)(true); let i = rangeStart;
(index - i) * direction > 0;
i += direction
) {
handleSelect(filteredData[i].id)(!checked);
} }
handleSelect(filteredData[index].id, index)(!checked);
} }
}; };
const getThumbnail = (file: File[], index: number) => ( const getThumbnail = (file: EnteFile[], index: number) => (
<PreviewCard <PreviewCard
key={`tile-${file[index].id}-selected-${ key={`tile-${file[index].id}-selected-${
selected[file[index].id] ?? false selected[file[index].id] ?? false
@ -337,9 +348,7 @@ const PhotoFrame = ({
selectOnClick={selected.count > 0} selectOnClick={selected.count > 0}
onHover={onHoverOver(index)} onHover={onHoverOver(index)}
onRangeSelect={handleRangeSelect(index)} onRangeSelect={handleRangeSelect(index)}
isRangeSelectActive={ isRangeSelectActive={isShiftKeyPressed && selected.count > 0}
isShiftKeyPressed && (rangeStart || rangeStart === 0)
}
isInsSelectRange={ isInsSelectRange={
(index >= rangeStart && index <= currentHover) || (index >= rangeStart && index <= currentHover) ||
(index >= currentHover && index <= rangeStart) (index >= currentHover && index <= rangeStart)
@ -347,7 +356,11 @@ const PhotoFrame = ({
/> />
); );
const getSlideData = async (instance: any, index: number, item: File) => { const getSlideData = async (
instance: any,
index: number,
item: EnteFile
) => {
if (!item.msrc) { if (!item.msrc) {
try { try {
let url: string; let url: string;

View file

@ -1,7 +1,7 @@
import React, { useRef, useEffect } from 'react'; import React, { useRef, useEffect } from 'react';
import { VariableSizeList as List } from 'react-window'; import { VariableSizeList as List } from 'react-window';
import styled from 'styled-components'; import styled from 'styled-components';
import { File } from 'services/fileService'; import { EnteFile } from 'types/file';
import { import {
IMAGE_CONTAINER_MAX_WIDTH, IMAGE_CONTAINER_MAX_WIDTH,
IMAGE_CONTAINER_MAX_HEIGHT, IMAGE_CONTAINER_MAX_HEIGHT,
@ -9,7 +9,7 @@ import {
DATE_CONTAINER_HEIGHT, DATE_CONTAINER_HEIGHT,
GAP_BTW_TILES, GAP_BTW_TILES,
SPACE_BTW_DATES, SPACE_BTW_DATES,
} from 'types'; } from 'constants/gallery';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
const A_DAY = 24 * 60 * 60 * 1000; const A_DAY = 24 * 60 * 60 * 1000;
@ -23,7 +23,7 @@ enum ITEM_TYPE {
interface TimeStampListItem { interface TimeStampListItem {
itemType: ITEM_TYPE; itemType: ITEM_TYPE;
items?: File[]; items?: EnteFile[];
itemStartIndex?: number; itemStartIndex?: number;
date?: string; date?: string;
dates?: { dates?: {
@ -102,9 +102,9 @@ const NothingContainer = styled.div<{ span: number }>`
interface Props { interface Props {
height: number; height: number;
width: number; width: number;
filteredData: File[]; filteredData: EnteFile[];
showBanner: boolean; showBanner: boolean;
getThumbnail: (file: File[], index: number) => JSX.Element; getThumbnail: (file: EnteFile[], index: number) => JSX.Element;
activeCollection: number; activeCollection: number;
resetFetching: () => void; resetFetching: () => void;
} }

View file

@ -7,11 +7,8 @@ import {
addToFavorites, addToFavorites,
removeFromFavorites, removeFromFavorites,
} from 'services/collectionService'; } from 'services/collectionService';
import { import { updatePublicMagicMetadata } from 'services/fileService';
File, import { EnteFile } from 'types/file';
MAX_EDITED_FILE_NAME_LENGTH,
updatePublicMagicMetadata,
} from 'services/fileService';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
import exifr from 'exifr'; import exifr from 'exifr';
import Modal from 'react-bootstrap/Modal'; import Modal from 'react-bootstrap/Modal';
@ -45,13 +42,23 @@ import { Formik } from 'formik';
import * as Yup from 'yup'; import * as Yup from 'yup';
import EnteSpinner from 'components/EnteSpinner'; import EnteSpinner from 'components/EnteSpinner';
import EnteDateTimePicker from 'components/EnteDateTimePicker'; import EnteDateTimePicker from 'components/EnteDateTimePicker';
import { MAX_EDITED_FILE_NAME_LENGTH } from 'constants/file';
import { sleep } from 'utils/common';
const SmallLoadingSpinner = () => (
<EnteSpinner
style={{
width: '20px',
height: '20px',
}}
/>
);
interface Iprops { interface Iprops {
isOpen: boolean; isOpen: boolean;
items: any[]; items: any[];
currentIndex?: number; currentIndex?: number;
onClose?: (needUpdate: boolean) => void; onClose?: (needUpdate: boolean) => void;
gettingData: (instance: any, index: number, item: File) => void; gettingData: (instance: any, index: number, item: EnteFile) => void;
id?: string; id?: string;
className?: string; className?: string;
favItemIds: Set<number>; favItemIds: Set<number>;
@ -87,9 +94,10 @@ function RenderCreationTime({
file, file,
scheduleUpdate, scheduleUpdate,
}: { }: {
file: File; file: EnteFile;
scheduleUpdate: () => void; scheduleUpdate: () => void;
}) { }) {
const [loading, setLoading] = useState(false);
const originalCreationTime = new Date(file?.metadata.creationTime / 1000); const originalCreationTime = new Date(file?.metadata.creationTime / 1000);
const [isInEditMode, setIsInEditMode] = useState(false); const [isInEditMode, setIsInEditMode] = useState(false);
@ -100,6 +108,7 @@ function RenderCreationTime({
const saveEdits = async () => { const saveEdits = async () => {
try { try {
setLoading(true);
if (isInEditMode && file) { if (isInEditMode && file) {
const unixTimeInMicroSec = pickedTime.getTime() * 1000; const unixTimeInMicroSec = pickedTime.getTime() * 1000;
if (unixTimeInMicroSec === file?.metadata.creationTime) { if (unixTimeInMicroSec === file?.metadata.creationTime) {
@ -118,14 +127,16 @@ function RenderCreationTime({
} }
} catch (e) { } catch (e) {
logError(e, 'failed to update creationTime'); logError(e, 'failed to update creationTime');
} finally {
closeEditMode();
setLoading(false);
} }
closeEditMode();
}; };
const discardEdits = () => { const discardEdits = () => {
setPickedTime(originalCreationTime); setPickedTime(originalCreationTime);
closeEditMode(); closeEditMode();
}; };
const handleChange = (newDate) => { const handleChange = (newDate: Date) => {
if (newDate instanceof Date) { if (newDate instanceof Date) {
setPickedTime(newDate); setPickedTime(newDate);
} }
@ -137,6 +148,7 @@ function RenderCreationTime({
<Value width={isInEditMode ? '50%' : '60%'}> <Value width={isInEditMode ? '50%' : '60%'}>
{isInEditMode ? ( {isInEditMode ? (
<EnteDateTimePicker <EnteDateTimePicker
loading={loading}
isInEditMode={isInEditMode} isInEditMode={isInEditMode}
pickedTime={pickedTime} pickedTime={pickedTime}
handleChange={handleChange} handleChange={handleChange}
@ -155,7 +167,11 @@ function RenderCreationTime({
) : ( ) : (
<> <>
<IconButton onClick={saveEdits}> <IconButton onClick={saveEdits}>
<TickIcon /> {loading ? (
<SmallLoadingSpinner />
) : (
<TickIcon />
)}
</IconButton> </IconButton>
<IconButton onClick={discardEdits}> <IconButton onClick={discardEdits}>
<CloseIcon /> <CloseIcon />
@ -239,12 +255,7 @@ const FileNameEditForm = ({ filename, saveEdits, discardEdits, extension }) => {
<Value width={'16.67%'}> <Value width={'16.67%'}>
<IconButton type="submit" disabled={loading}> <IconButton type="submit" disabled={loading}>
{loading ? ( {loading ? (
<EnteSpinner <SmallLoadingSpinner />
style={{
width: '20px',
height: '20px',
}}
/>
) : ( ) : (
<TickIcon /> <TickIcon />
)} )}
@ -267,7 +278,7 @@ function RenderFileName({
file, file,
scheduleUpdate, scheduleUpdate,
}: { }: {
file: File; file: EnteFile;
scheduleUpdate: () => void; scheduleUpdate: () => void;
}) { }) {
const originalTitle = file?.metadata.title; const originalTitle = file?.metadata.title;
@ -456,7 +467,7 @@ function PhotoSwipe(props: Iprops) {
const { isOpen, items } = props; const { isOpen, items } = props;
const [isFav, setIsFav] = useState(false); const [isFav, setIsFav] = useState(false);
const [showInfo, setShowInfo] = useState(false); const [showInfo, setShowInfo] = useState(false);
const [metadata, setMetaData] = useState<File['metadata']>(null); const [metadata, setMetaData] = useState<EnteFile['metadata']>(null);
const [exif, setExif] = useState<any>(null); const [exif, setExif] = useState<any>(null);
const needUpdate = useRef(false); const needUpdate = useRef(false);
@ -605,26 +616,28 @@ function PhotoSwipe(props: Iprops) {
} }
}; };
const checkExifAvailable = () => { const checkExifAvailable = async () => {
setExif(null); setExif(null);
setTimeout(() => { await sleep(100);
try {
const img: HTMLImageElement = document.querySelector( const img: HTMLImageElement = document.querySelector(
'.pswp__img:not(.pswp__img--placeholder)' '.pswp__img:not(.pswp__img--placeholder)'
); );
if (img) { if (img) {
exifr.parse(img).then(function (exifData) { const exifData = await exifr.parse(img);
if (!exifData) { if (!exifData) {
return; return;
} }
exifData.raw = prettyPrintExif(exifData); exifData.raw = prettyPrintExif(exifData);
setExif(exifData); setExif(exifData);
});
} }
}, 100); } catch (e) {
logError(e, 'exifr parsing failed');
}
}; };
function updateInfo() { function updateInfo() {
const file: File = this?.currItem; const file: EnteFile = this?.currItem;
if (file?.metadata) { if (file?.metadata) {
setMetaData(file.metadata); setMetaData(file.metadata);
setExif(null); setExif(null);

View file

@ -1,11 +1,9 @@
import { Search, SearchStats } from 'pages/gallery';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import AsyncSelect from 'react-select/async'; import AsyncSelect from 'react-select/async';
import { components } from 'react-select'; import { components } from 'react-select';
import debounce from 'debounce-promise'; import debounce from 'debounce-promise';
import { import {
Bbox,
getHolidaySuggestion, getHolidaySuggestion,
getYearSuggestion, getYearSuggestion,
parseHumanDate, parseHumanDate,
@ -19,12 +17,16 @@ import LocationIcon from './icons/LocationIcon';
import DateIcon from './icons/DateIcon'; import DateIcon from './icons/DateIcon';
import SearchIcon from './icons/SearchIcon'; import SearchIcon from './icons/SearchIcon';
import CloseIcon from './icons/CloseIcon'; import CloseIcon from './icons/CloseIcon';
import { Collection } from 'services/collectionService'; import { Collection } from 'types/collection';
import CollectionIcon from './icons/CollectionIcon'; import CollectionIcon from './icons/CollectionIcon';
import { File, FILE_TYPE } from 'services/fileService';
import ImageIcon from './icons/ImageIcon'; import ImageIcon from './icons/ImageIcon';
import VideoIcon from './icons/VideoIcon'; import VideoIcon from './icons/VideoIcon';
import { IconButton } from './Container'; import { IconButton } from './Container';
import { EnteFile } from 'types/file';
import { Suggestion, SuggestionType, DateValue, Bbox } from 'types/search';
import { Search, SearchStats } from 'types/gallery';
import { FILE_TYPE } from 'constants/file';
const Wrapper = styled.div<{ isDisabled: boolean; isOpen: boolean }>` const Wrapper = styled.div<{ isDisabled: boolean; isOpen: boolean }>`
position: fixed; position: fixed;
@ -75,23 +77,6 @@ const SearchInput = styled.div`
margin: auto; margin: auto;
`; `;
export enum SuggestionType {
DATE,
LOCATION,
COLLECTION,
IMAGE,
VIDEO,
}
export interface DateValue {
date?: number;
month?: number;
year?: number;
}
export interface Suggestion {
type: SuggestionType;
label: string;
value: Bbox | DateValue | number;
}
interface Props { interface Props {
isOpen: boolean; isOpen: boolean;
isFirstFetch: boolean; isFirstFetch: boolean;
@ -101,7 +86,7 @@ interface Props {
searchStats: SearchStats; searchStats: SearchStats;
collections: Collection[]; collections: Collection[];
setActiveCollection: (id: number) => void; setActiveCollection: (id: number) => void;
files: File[]; files: EnteFile[];
} }
export default function SearchBar(props: Props) { export default function SearchBar(props: Props) {
const [value, setValue] = useState<Suggestion>(null); const [value, setValue] = useState<Suggestion>(null);

View file

@ -13,10 +13,10 @@ import {
isSubscriptionCancelled, isSubscriptionCancelled,
isSubscribed, isSubscribed,
convertToHumanReadable, convertToHumanReadable,
} from 'utils/billingUtil'; } from 'utils/billing';
import isElectron from 'is-electron'; import isElectron from 'is-electron';
import { Collection } from 'services/collectionService'; import { Collection } from 'types/collection';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import LinkButton from './pages/gallery/LinkButton'; import LinkButton from './pages/gallery/LinkButton';
import { downloadApp } from 'utils/common'; import { downloadApp } from 'utils/common';
@ -27,16 +27,14 @@ import EnteSpinner from './EnteSpinner';
import RecoveryKeyModal from './RecoveryKeyModal'; import RecoveryKeyModal from './RecoveryKeyModal';
import TwoFactorModal from './TwoFactorModal'; import TwoFactorModal from './TwoFactorModal';
import ExportModal from './ExportModal'; import ExportModal from './ExportModal';
import { GalleryContext, SetLoading } from 'pages/gallery'; import { GalleryContext } from 'pages/gallery';
import InProgressIcon from './icons/InProgressIcon'; import InProgressIcon from './icons/InProgressIcon';
import exportService from 'services/exportService'; import exportService from 'services/exportService';
import { Subscription } from 'services/billingService'; import { Subscription } from 'types/billing';
import { PAGES } from 'types'; import { PAGES } from 'constants/pages';
import { import { ARCHIVE_SECTION, TRASH_SECTION } from 'constants/collection';
ARCHIVE_SECTION,
TRASH_SECTION,
} from 'components/pages/gallery/Collections';
import FixLargeThumbnails from './FixLargeThumbnail'; import FixLargeThumbnails from './FixLargeThumbnail';
import { SetLoading } from 'types/gallery';
interface Props { interface Props {
collections: Collection[]; collections: Collection[];
setDialogMessage: SetDialogMessage; setDialogMessage: SetDialogMessage;

View file

@ -19,7 +19,7 @@ import { setJustSignedUp } from 'utils/storage';
import LogoImg from './LogoImg'; import LogoImg from './LogoImg';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
import { SESSION_KEYS } from 'utils/storage/sessionStorage'; import { SESSION_KEYS } from 'utils/storage/sessionStorage';
import { PAGES } from 'types'; import { PAGES } from 'constants/pages';
interface FormValues { interface FormValues {
email: string; email: string;

View file

@ -1,10 +1,11 @@
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { DeadCenter, SetLoading } from 'pages/gallery'; import { DeadCenter } from 'pages/gallery';
import { AppContext, FLASH_MESSAGE_TYPE } from 'pages/_app'; import { AppContext, FLASH_MESSAGE_TYPE } from 'pages/_app';
import React, { useContext, useEffect, useState } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import { Button } from 'react-bootstrap'; import { Button } from 'react-bootstrap';
import { disableTwoFactor, getTwoFactorStatus } from 'services/userService'; import { disableTwoFactor, getTwoFactorStatus } from 'services/userService';
import { PAGES } from 'types'; import { SetLoading } from 'types/gallery';
import { PAGES } from 'constants/pages';
import { getData, LS_KEYS, setData } from 'utils/storage/localStorage'; import { getData, LS_KEYS, setData } from 'utils/storage/localStorage';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
import { Label, Value, Row } from './Container'; import { Label, Value, Row } from './Container';

View file

@ -1,16 +1,13 @@
import React from 'react'; import React from 'react';
import { SetDialogMessage } from 'components/MessageDialog'; import { SetDialogMessage } from 'components/MessageDialog';
import { ListGroup, Popover } from 'react-bootstrap'; import { ListGroup, Popover } from 'react-bootstrap';
import { import { deleteCollection, renameCollection } from 'services/collectionService';
Collection,
deleteCollection,
renameCollection,
} from 'services/collectionService';
import { downloadCollection, getSelectedCollection } from 'utils/collection'; import { downloadCollection, getSelectedCollection } from 'utils/collection';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
import { SetCollectionNamerAttributes } from './CollectionNamer'; import { SetCollectionNamerAttributes } from './CollectionNamer';
import LinkButton, { ButtonVariant, LinkButtonProps } from './LinkButton'; import LinkButton, { ButtonVariant, LinkButtonProps } from './LinkButton';
import { sleep } from 'utils/common'; import { sleep } from 'utils/common';
import { Collection } from 'types/collection';
interface CollectionOptionsProps { interface CollectionOptionsProps {
syncWithRemote: () => Promise<void>; syncWithRemote: () => Promise<void>;

View file

@ -1,15 +1,12 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Card, Modal } from 'react-bootstrap'; import { Card, Modal } from 'react-bootstrap';
import styled from 'styled-components'; import styled from 'styled-components';
import {
Collection,
CollectionAndItsLatestFile,
CollectionType,
} from 'services/collectionService';
import AddCollectionButton from './AddCollectionButton'; import AddCollectionButton from './AddCollectionButton';
import PreviewCard from './PreviewCard'; import PreviewCard from './PreviewCard';
import { getData, LS_KEYS } from 'utils/storage/localStorage'; import { getData, LS_KEYS } from 'utils/storage/localStorage';
import { User } from 'services/userService'; import { User } from 'types/user';
import { Collection, CollectionAndItsLatestFile } from 'types/collection';
import { CollectionType } from 'constants/collection';
export const CollectionIcon = styled.div` export const CollectionIcon = styled.div`
width: 200px; width: 200px;
@ -53,7 +50,7 @@ function CollectionSelector({
CollectionAndItsLatestFile[] CollectionAndItsLatestFile[]
>([]); >([]);
useEffect(() => { useEffect(() => {
if (!attributes) { if (!attributes || !props.show) {
return; return;
} }
const user: User = getData(LS_KEYS.USER); const user: User = getData(LS_KEYS.USER);
@ -86,6 +83,7 @@ function CollectionSelector({
<PreviewCard <PreviewCard
file={item.file} file={item.file}
updateUrl={() => {}} updateUrl={() => {}}
onSelect={() => {}}
forcedEnable forcedEnable
/> />
<Card.Text className="text-center"> <Card.Text className="text-center">

View file

@ -2,7 +2,7 @@ import { IconButton } from 'components/Container';
import SortIcon from 'components/icons/SortIcon'; import SortIcon from 'components/icons/SortIcon';
import React from 'react'; import React from 'react';
import { OverlayTrigger } from 'react-bootstrap'; import { OverlayTrigger } from 'react-bootstrap';
import { COLLECTION_SORT_BY } from 'services/collectionService'; import { COLLECTION_SORT_BY } from 'constants/collection';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
import CollectionSortOptions from './CollectionSortOptions'; import CollectionSortOptions from './CollectionSortOptions';
import { IconWithMessage } from './SelectedFileOptions'; import { IconWithMessage } from './SelectedFileOptions';

View file

@ -2,7 +2,7 @@ import { Value } from 'components/Container';
import TickIcon from 'components/icons/TickIcon'; import TickIcon from 'components/icons/TickIcon';
import React from 'react'; import React from 'react';
import { ListGroup, Popover, Row } from 'react-bootstrap'; import { ListGroup, Popover, Row } from 'react-bootstrap';
import { COLLECTION_SORT_BY } from 'services/collectionService'; import { COLLECTION_SORT_BY } from 'constants/collection';
import styled from 'styled-components'; import styled from 'styled-components';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
import { MenuItem, MenuLink } from './CollectionOptions'; import { MenuItem, MenuLink } from './CollectionOptions';

View file

@ -5,16 +5,11 @@ import NavigationButton, {
} from 'components/NavigationButton'; } from 'components/NavigationButton';
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { OverlayTrigger, Tooltip } from 'react-bootstrap'; import { OverlayTrigger, Tooltip } from 'react-bootstrap';
import { import { sortCollections } from 'services/collectionService';
Collection, import { User } from 'types/user';
CollectionAndItsLatestFile,
CollectionType,
COLLECTION_SORT_BY,
sortCollections,
} from 'services/collectionService';
import { User } from 'services/userService';
import styled from 'styled-components'; import styled from 'styled-components';
import { IMAGE_CONTAINER_MAX_WIDTH } from 'types'; import { IMAGE_CONTAINER_MAX_WIDTH } from 'constants/gallery';
import { Collection, CollectionAndItsLatestFile } from 'types/collection';
import { getSelectedCollection } from 'utils/collection'; import { getSelectedCollection } from 'utils/collection';
import { getData, LS_KEYS } from 'utils/storage/localStorage'; import { getData, LS_KEYS } from 'utils/storage/localStorage';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
@ -22,10 +17,13 @@ import { SetCollectionNamerAttributes } from './CollectionNamer';
import CollectionOptions from './CollectionOptions'; import CollectionOptions from './CollectionOptions';
import CollectionSort from './CollectionSort'; import CollectionSort from './CollectionSort';
import OptionIcon, { OptionIconWrapper } from './OptionIcon'; import OptionIcon, { OptionIconWrapper } from './OptionIcon';
import {
export const ARCHIVE_SECTION = -1; ALL_SECTION,
export const TRASH_SECTION = -2; ARCHIVE_SECTION,
export const ALL_SECTION = 0; CollectionType,
COLLECTION_SORT_BY,
TRASH_SECTION,
} from 'constants/collection';
interface CollectionProps { interface CollectionProps {
collections: Collection[]; collections: Collection[];

View file

@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react';
import { Form, Modal, Button } from 'react-bootstrap'; import { Form, Modal, Button } from 'react-bootstrap';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
import styled from 'styled-components'; import styled from 'styled-components';
import billingService, { Plan, Subscription } from 'services/billingService'; import { Plan, Subscription } from 'types/billing';
import { import {
convertBytesToGBs, convertBytesToGBs,
getUserSubscription, getUserSubscription,
@ -16,12 +16,14 @@ import {
hasPaidSubscription, hasPaidSubscription,
isOnFreePlan, isOnFreePlan,
planForSubscription, planForSubscription,
} from 'utils/billingUtil'; } from 'utils/billing';
import { reverseString } from 'utils/common'; import { reverseString } from 'utils/common';
import { SetDialogMessage } from 'components/MessageDialog'; import { SetDialogMessage } from 'components/MessageDialog';
import ArrowEast from 'components/icons/ArrowEast'; import ArrowEast from 'components/icons/ArrowEast';
import LinkButton from './LinkButton'; import LinkButton from './LinkButton';
import { DeadCenter, SetLoading } from 'pages/gallery'; import { DeadCenter } from 'pages/gallery';
import billingService from 'services/billingService';
import { SetLoading } from 'types/gallery';
export const PlanIcon = styled.div<{ selected: boolean }>` export const PlanIcon = styled.div<{ selected: boolean }>`
border-radius: 20px; border-radius: 20px;

View file

@ -1,20 +1,20 @@
import React, { useContext, useLayoutEffect, useRef, useState } from 'react'; import React, { useContext, useLayoutEffect, useRef, useState } from 'react';
import { File } from 'services/fileService'; import { EnteFile } from 'types/file';
import styled from 'styled-components'; import styled from 'styled-components';
import PlayCircleOutline from 'components/icons/PlayCircleOutline'; import PlayCircleOutline from 'components/icons/PlayCircleOutline';
import DownloadManager from 'services/downloadManager'; import DownloadManager from 'services/downloadManager';
import useLongPress from 'utils/common/useLongPress'; import useLongPress from 'utils/common/useLongPress';
import { GalleryContext } from 'pages/gallery'; import { GalleryContext } from 'pages/gallery';
import { GAP_BTW_TILES } from 'types'; import { GAP_BTW_TILES } from 'constants/gallery';
interface IProps { interface IProps {
file: File; file: EnteFile;
updateUrl: (url: string) => void; updateUrl: (url: string) => void;
onClick?: () => void; onClick?: () => void;
forcedEnable?: boolean; forcedEnable?: boolean;
selectable?: boolean; selectable?: boolean;
selected?: boolean; selected?: boolean;
onSelect?: (checked: boolean) => void; onSelect: (checked: boolean) => void;
onHover?: () => void; onHover?: () => void;
onRangeSelect?: () => void; onRangeSelect?: () => void;
isRangeSelectActive?: boolean; isRangeSelectActive?: boolean;
@ -217,8 +217,9 @@ export default function PreviewCard(props: IProps) {
if (selectOnClick) { if (selectOnClick) {
if (isRangeSelectActive) { if (isRangeSelectActive) {
onRangeSelect(); onRangeSelect();
} else {
onSelect(!selected);
} }
onSelect?.(!selected);
} else if (file?.msrc || imgSrc) { } else if (file?.msrc || imgSrc) {
onClick?.(); onClick?.();
} }
@ -227,15 +228,16 @@ export default function PreviewCard(props: IProps) {
const handleSelect: React.ChangeEventHandler<HTMLInputElement> = (e) => { const handleSelect: React.ChangeEventHandler<HTMLInputElement> = (e) => {
if (isRangeSelectActive) { if (isRangeSelectActive) {
onRangeSelect?.(); onRangeSelect?.();
} else {
onSelect(e.target.checked);
} }
onSelect?.(e.target.checked);
}; };
const longPressCallback = () => { const longPressCallback = () => {
onSelect(!selected); onSelect(!selected);
}; };
const handleHover = () => { const handleHover = () => {
if (selectOnClick) { if (isRangeSelectActive) {
onHover(); onHover();
} }
}; };

View file

@ -11,19 +11,21 @@ import constants from 'utils/strings/constants';
import Archive from 'components/icons/Archive'; import Archive from 'components/icons/Archive';
import MoveIcon from 'components/icons/MoveIcon'; import MoveIcon from 'components/icons/MoveIcon';
import { COLLECTION_OPS_TYPE } from 'utils/collection'; import { COLLECTION_OPS_TYPE } from 'utils/collection';
import { ALL_SECTION, ARCHIVE_SECTION, TRASH_SECTION } from './Collections'; import {
ALL_SECTION,
ARCHIVE_SECTION,
TRASH_SECTION,
} from 'constants/collection';
import UnArchive from 'components/icons/UnArchive'; import UnArchive from 'components/icons/UnArchive';
import { OverlayTrigger } from 'react-bootstrap'; import { OverlayTrigger } from 'react-bootstrap';
import { Collection } from 'services/collectionService'; import { Collection } from 'types/collection';
import RemoveIcon from 'components/icons/RemoveIcon'; import RemoveIcon from 'components/icons/RemoveIcon';
import RestoreIcon from 'components/icons/RestoreIcon'; import RestoreIcon from 'components/icons/RestoreIcon';
import ClockIcon from 'components/icons/ClockIcon'; import ClockIcon from 'components/icons/ClockIcon';
import { getData, LS_KEYS } from 'utils/storage/localStorage'; import { getData, LS_KEYS } from 'utils/storage/localStorage';
import { import { FIX_CREATION_TIME_VISIBLE_TO_USER_IDS } from 'constants/user';
FIX_CREATION_TIME_VISIBLE_TO_USER_IDS,
User,
} from 'services/userService';
import DownloadIcon from 'components/icons/DownloadIcon'; import DownloadIcon from 'components/icons/DownloadIcon';
import { User } from 'types/user';
interface Props { interface Props {
addToCollectionHelper: (collection: Collection) => void; addToCollectionHelper: (collection: Collection) => void;

View file

@ -1,10 +1,6 @@
import React, { useContext, useEffect, useState } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import { import { syncCollections, createAlbum } from 'services/collectionService';
Collection,
syncCollections,
createAlbum,
} from 'services/collectionService';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
import { SetDialogMessage } from 'components/MessageDialog'; import { SetDialogMessage } from 'components/MessageDialog';
import UploadProgress from './UploadProgress'; import UploadProgress from './UploadProgress';
@ -12,24 +8,25 @@ import UploadProgress from './UploadProgress';
import ChoiceModal from './ChoiceModal'; import ChoiceModal from './ChoiceModal';
import { SetCollectionNamerAttributes } from './CollectionNamer'; import { SetCollectionNamerAttributes } from './CollectionNamer';
import { SetCollectionSelectorAttributes } from './CollectionSelector'; import { SetCollectionSelectorAttributes } from './CollectionSelector';
import { GalleryContext, SetFiles, SetLoading } from 'pages/gallery'; import { GalleryContext } from 'pages/gallery';
import { AppContext } from 'pages/_app'; import { AppContext } from 'pages/_app';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
import { FileRejection } from 'react-dropzone'; import { FileRejection } from 'react-dropzone';
import UploadManager, { import UploadManager from 'services/upload/uploadManager';
FileWithCollection,
UPLOAD_STAGES,
} from 'services/upload/uploadManager';
import uploadManager from 'services/upload/uploadManager'; import uploadManager from 'services/upload/uploadManager';
import { METADATA_FOLDER_NAME } from 'services/exportService'; import { METADATA_FOLDER_NAME } from 'constants/export';
import { getUserFacingErrorMessage } from 'utils/common/errorUtil'; import { getUserFacingErrorMessage } from 'utils/error';
import { Collection } from 'types/collection';
import { SetLoading, SetFiles } from 'types/gallery';
import { UPLOAD_STAGES } from 'constants/upload';
import { FileWithCollection } from 'types/upload';
const FIRST_ALBUM_NAME = 'My First Album'; const FIRST_ALBUM_NAME = 'My First Album';
interface Props { interface Props {
syncWithRemote: (force?: boolean, silent?: boolean) => Promise<void>; syncWithRemote: (force?: boolean, silent?: boolean) => Promise<void>;
setBannerMessage: (message: string | JSX.Element) => void; setBannerMessage: (message: string | JSX.Element) => void;
acceptedFiles: globalThis.File[]; acceptedFiles: File[];
closeCollectionSelector: () => void; closeCollectionSelector: () => void;
setCollectionSelectorAttributes: SetCollectionSelectorAttributes; setCollectionSelectorAttributes: SetCollectionSelectorAttributes;
setCollectionNamerAttributes: SetCollectionNamerAttributes; setCollectionNamerAttributes: SetCollectionNamerAttributes;
@ -42,7 +39,7 @@ interface Props {
isFirstUpload: boolean; isFirstUpload: boolean;
} }
export enum UPLOAD_STRATEGY { enum UPLOAD_STRATEGY {
SINGLE_COLLECTION, SINGLE_COLLECTION,
COLLECTION_PER_FOLDER, COLLECTION_PER_FOLDER,
} }
@ -51,18 +48,6 @@ interface AnalysisResult {
suggestedCollectionName: string; suggestedCollectionName: string;
multipleFolders: boolean; multipleFolders: boolean;
} }
export interface ProgressUpdater {
setPercentComplete: React.Dispatch<React.SetStateAction<number>>;
setFileCounter: React.Dispatch<
React.SetStateAction<{
finished: number;
total: number;
}>
>;
setUploadStage: React.Dispatch<React.SetStateAction<UPLOAD_STAGES>>;
setFileProgress: React.Dispatch<React.SetStateAction<Map<string, number>>>;
setUploadResult: React.Dispatch<React.SetStateAction<Map<string, number>>>;
}
export default function Upload(props: Props) { export default function Upload(props: Props) {
const [progressView, setProgressView] = useState(false); const [progressView, setProgressView] = useState(false);
@ -156,12 +141,12 @@ export default function Upload(props: Props) {
} }
} }
return { return {
suggestedCollectionName: commonPathPrefix, suggestedCollectionName: commonPathPrefix || null,
multipleFolders: firstFileFolder !== lastFileFolder, multipleFolders: firstFileFolder !== lastFileFolder,
}; };
} }
function getCollectionWiseFiles() { function getCollectionWiseFiles() {
const collectionWiseFiles = new Map<string, globalThis.File[]>(); const collectionWiseFiles = new Map<string, File[]>();
for (const file of props.acceptedFiles) { for (const file of props.acceptedFiles) {
const filePath = file['path'] as string; const filePath = file['path'] as string;
@ -203,7 +188,7 @@ export default function Upload(props: Props) {
const filesWithCollectionToUpload: FileWithCollection[] = []; const filesWithCollectionToUpload: FileWithCollection[] = [];
const collections: Collection[] = []; const collections: Collection[] = [];
let collectionWiseFiles = new Map<string, globalThis.File[]>(); let collectionWiseFiles = new Map<string, File[]>();
if (strategy === UPLOAD_STRATEGY.SINGLE_COLLECTION) { if (strategy === UPLOAD_STRATEGY.SINGLE_COLLECTION) {
collectionWiseFiles.set(collectionName, props.acceptedFiles); collectionWiseFiles.set(collectionName, props.acceptedFiles);
} else { } else {
@ -288,16 +273,20 @@ export default function Upload(props: Props) {
}; };
const uploadToSingleNewCollection = (collectionName: string) => { const uploadToSingleNewCollection = (collectionName: string) => {
uploadFilesToNewCollections( if (collectionName) {
UPLOAD_STRATEGY.SINGLE_COLLECTION, uploadFilesToNewCollections(
collectionName UPLOAD_STRATEGY.SINGLE_COLLECTION,
); collectionName
);
} else {
showCollectionCreateModal();
}
}; };
const showCollectionCreateModal = (analysisResult: AnalysisResult) => { const showCollectionCreateModal = () => {
props.setCollectionNamerAttributes({ props.setCollectionNamerAttributes({
title: constants.CREATE_COLLECTION, title: constants.CREATE_COLLECTION,
buttonText: constants.CREATE, buttonText: constants.CREATE,
autoFilledName: analysisResult?.suggestedCollectionName, autoFilledName: null,
callback: uploadToSingleNewCollection, callback: uploadToSingleNewCollection,
}); });
}; };
@ -306,25 +295,26 @@ export default function Upload(props: Props) {
analysisResult: AnalysisResult, analysisResult: AnalysisResult,
isFirstUpload: boolean isFirstUpload: boolean
) => { ) => {
if (!analysisResult.suggestedCollectionName) { if (isFirstUpload) {
if (isFirstUpload) { const collectionName =
uploadToSingleNewCollection(FIRST_ALBUM_NAME); analysisResult.suggestedCollectionName ?? FIRST_ALBUM_NAME;
} else {
props.setCollectionSelectorAttributes({ uploadToSingleNewCollection(collectionName);
callback: uploadFilesToExistingCollection,
showNextModal: () =>
showCollectionCreateModal(analysisResult),
title: constants.UPLOAD_TO_COLLECTION,
});
}
} else { } else {
let showNextModal = () => {};
if (analysisResult.multipleFolders) { if (analysisResult.multipleFolders) {
setChoiceModalView(true); showNextModal = () => setChoiceModalView(true);
} else if (analysisResult.suggestedCollectionName) { } else {
uploadToSingleNewCollection( showNextModal = () =>
analysisResult.suggestedCollectionName uploadToSingleNewCollection(
); analysisResult.suggestedCollectionName
);
} }
props.setCollectionSelectorAttributes({
callback: uploadFilesToExistingCollection,
showNextModal,
title: constants.UPLOAD_TO_COLLECTION,
});
} }
}; };

View file

@ -3,15 +3,13 @@ import ExpandMore from 'components/icons/ExpandMore';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Button, Modal, ProgressBar } from 'react-bootstrap'; import { Button, Modal, ProgressBar } from 'react-bootstrap';
import { FileRejection } from 'react-dropzone'; import { FileRejection } from 'react-dropzone';
import {
FileUploadResults,
UPLOAD_STAGES,
} from 'services/upload/uploadManager';
import styled from 'styled-components'; import styled from 'styled-components';
import { DESKTOP_APP_DOWNLOAD_URL } from 'utils/common'; import { DESKTOP_APP_DOWNLOAD_URL } from 'utils/common';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
import { Collapse } from 'react-collapse'; import { Collapse } from 'react-collapse';
import { ButtonVariant, getVariantColor } from './LinkButton'; import { ButtonVariant, getVariantColor } from './LinkButton';
import { FileUploadResults, UPLOAD_STAGES } from 'constants/upload';
interface Props { interface Props {
fileCounter; fileCounter;

View file

@ -0,0 +1,15 @@
export const ARCHIVE_SECTION = -1;
export const TRASH_SECTION = -2;
export const ALL_SECTION = 0;
export enum CollectionType {
folder = 'folder',
favorites = 'favorites',
album = 'album',
}
export enum COLLECTION_SORT_BY {
LATEST_FILE,
MODIFICATION_TIME,
NAME,
}

View file

@ -0,0 +1 @@
export const ENCRYPTION_CHUNK_SIZE = 4 * 1024 * 1024;

View file

@ -0,0 +1,28 @@
export const METADATA_FOLDER_NAME = 'metadata';
export enum ExportNotification {
START = 'export started',
IN_PROGRESS = 'export already in progress',
FINISH = 'export finished',
FAILED = 'export failed',
ABORT = 'export aborted',
PAUSE = 'export paused',
UP_TO_DATE = `no new files to export`,
}
export enum RecordType {
SUCCESS = 'success',
FAILED = 'failed',
}
export enum ExportStage {
INIT,
INPROGRESS,
PAUSED,
FINISHED,
}
export enum ExportType {
NEW,
PENDING,
RETRY_FAILED,
}

View file

@ -0,0 +1,22 @@
export const MIN_EDITED_CREATION_TIME = new Date(1800, 0, 1);
export const MAX_EDITED_CREATION_TIME = new Date();
export const ALL_TIME = new Date(1800, 0, 1, 23, 59, 59);
export const MAX_EDITED_FILE_NAME_LENGTH = 100;
export const TYPE_HEIC = 'heic';
export const TYPE_HEIF = 'heif';
export const TYPE_JPEG = 'jpeg';
export const TYPE_JPG = 'jpg';
export enum FILE_TYPE {
IMAGE,
VIDEO,
LIVE_PHOTO,
OTHERS,
}
export enum VISIBILITY_STATE {
VISIBLE,
ARCHIVED,
}

View file

@ -0,0 +1,7 @@
export const GAP_BTW_TILES = 4;
export const DATE_CONTAINER_HEIGHT = 48;
export const IMAGE_CONTAINER_MAX_HEIGHT = 200;
export const IMAGE_CONTAINER_MAX_WIDTH =
IMAGE_CONTAINER_MAX_HEIGHT - GAP_BTW_TILES;
export const MIN_COLUMNS = 4;
export const SPACE_BTW_DATES = 44;

View file

@ -0,0 +1,15 @@
export enum PAGES {
CHANGE_EMAIL = '/change-email',
CHANGE_PASSWORD = '/change-password',
CREDENTIALS = '/credentials',
GALLERY = '/gallery',
GENERATE = '/generate',
LOGIN = '/login',
RECOVER = '/recover',
SIGNUP = '/signup',
TWO_FACTOR_SETUP = '/two-factor/setup',
TWO_FACTOR_VERIFY = '/two-factor/verify',
TWO_FACTOR_RECOVER = '/two-factor/recover',
VERIFY = '/verify',
ROOT = '/',
}

View file

@ -0,0 +1,10 @@
export const getSentryDSN = () =>
process.env.NEXT_PUBLIC_SENTRY_DSN ??
'https://860186db60c54c7fbacfe255124958e8@errors.ente.io/4';
export const getSentryENV = () =>
process.env.NEXT_PUBLIC_SENTRY_ENV ?? 'development';
export const getSentryRelease = () => process.env.SENTRY_RELEASE;
export { getIsSentryEnabled } from '../../../sentryConfigUtil';

View file

@ -0,0 +1,39 @@
import { ENCRYPTION_CHUNK_SIZE } from 'constants/crypto';
import { FILE_TYPE } from 'constants/file';
import { Location } from 'types/upload';
// list of format that were missed by type-detection for some files.
export const FORMAT_MISSED_BY_FILE_TYPE_LIB = [
{ fileType: FILE_TYPE.IMAGE, exactType: 'jpeg' },
{ fileType: FILE_TYPE.IMAGE, exactType: 'jpg' },
{ fileType: FILE_TYPE.VIDEO, exactType: 'webm' },
];
// this is the chunk size of the un-encrypted file which is read and encrypted before uploading it as a single part.
export const MULTIPART_PART_SIZE = 20 * 1024 * 1024;
export const FILE_READER_CHUNK_SIZE = ENCRYPTION_CHUNK_SIZE;
export const FILE_CHUNKS_COMBINED_FOR_A_UPLOAD_PART = Math.floor(
MULTIPART_PART_SIZE / FILE_READER_CHUNK_SIZE
);
export const RANDOM_PERCENTAGE_PROGRESS_FOR_PUT = () => 90 + 10 * Math.random();
export const NULL_LOCATION: Location = { latitude: null, longitude: null };
export enum UPLOAD_STAGES {
START,
READING_GOOGLE_METADATA_FILES,
UPLOADING,
FINISH,
}
export enum FileUploadResults {
FAILED = -1,
SKIPPED = -2,
UNSUPPORTED = -3,
BLOCKED = -4,
TOO_LARGE = -5,
UPLOADED = 100,
}

View file

@ -0,0 +1 @@
export const FIX_CREATION_TIME_VISIBLE_TO_USER_IDS = [1, 125, 243, 341];

View file

@ -8,7 +8,7 @@ import { getToken } from 'utils/common/key';
import EnteSpinner from 'components/EnteSpinner'; import EnteSpinner from 'components/EnteSpinner';
import ChangeEmailForm from 'components/ChangeEmail'; import ChangeEmailForm from 'components/ChangeEmail';
import EnteCard from 'components/EnteCard'; import EnteCard from 'components/EnteCard';
import { PAGES } from 'types'; import { PAGES } from 'constants/pages';
function ChangeEmailPage() { function ChangeEmailPage() {
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');

View file

@ -8,17 +8,12 @@ import CryptoWorker, {
B64EncryptionResult, B64EncryptionResult,
} from 'utils/crypto'; } from 'utils/crypto';
import { getActualKey } from 'utils/common/key'; import { getActualKey } from 'utils/common/key';
import { setKeys, UpdatedKey } from 'services/userService'; import { setKeys } from 'services/userService';
import SetPasswordForm from 'components/SetPasswordForm'; import SetPasswordForm from 'components/SetPasswordForm';
import { AppContext } from 'pages/_app'; import { AppContext } from 'pages/_app';
import { SESSION_KEYS } from 'utils/storage/sessionStorage'; import { SESSION_KEYS } from 'utils/storage/sessionStorage';
import { PAGES } from 'types'; import { PAGES } from 'constants/pages';
import { KEK, UpdatedKey } from 'types/user';
export interface KEK {
key: string;
opsLimit: number;
memLimit: number;
}
export default function Generate() { export default function Generate() {
const [token, setToken] = useState<string>(); const [token, setToken] = useState<string>();

View file

@ -3,7 +3,7 @@ import React, { useContext, useEffect, useState } from 'react';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
import { clearData, getData, LS_KEYS } from 'utils/storage/localStorage'; import { clearData, getData, LS_KEYS } from 'utils/storage/localStorage';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { KeyAttributes, PAGES } from 'types'; import { PAGES } from 'constants/pages';
import { SESSION_KEYS, getKey } from 'utils/storage/sessionStorage'; import { SESSION_KEYS, getKey } from 'utils/storage/sessionStorage';
import CryptoWorker, { import CryptoWorker, {
decryptAndStoreToken, decryptAndStoreToken,
@ -18,6 +18,7 @@ import { Button, Card } from 'react-bootstrap';
import { AppContext } from 'pages/_app'; import { AppContext } from 'pages/_app';
import LogoImg from 'components/LogoImg'; import LogoImg from 'components/LogoImg';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
import { KeyAttributes } from 'types/user';
export default function Credentials() { export default function Credentials() {
const router = useRouter(); const router = useRouter();

View file

@ -8,30 +8,25 @@ import React, {
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { clearKeys, getKey, SESSION_KEYS } from 'utils/storage/sessionStorage'; import { clearKeys, getKey, SESSION_KEYS } from 'utils/storage/sessionStorage';
import { import {
File,
getLocalFiles, getLocalFiles,
syncFiles, syncFiles,
updateMagicMetadata, updateMagicMetadata,
VISIBILITY_STATE,
trashFiles, trashFiles,
deleteFromTrash, deleteFromTrash,
} from 'services/fileService'; } from 'services/fileService';
import styled from 'styled-components'; import styled from 'styled-components';
import LoadingBar from 'react-top-loading-bar'; import LoadingBar from 'react-top-loading-bar';
import { import {
Collection,
syncCollections, syncCollections,
CollectionAndItsLatestFile,
getCollectionsAndTheirLatestFile, getCollectionsAndTheirLatestFile,
getFavItemIds, getFavItemIds,
getLocalCollections, getLocalCollections,
getNonEmptyCollections, getNonEmptyCollections,
createCollection, createCollection,
CollectionType,
} from 'services/collectionService'; } from 'services/collectionService';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
import billingService from 'services/billingService'; import billingService from 'services/billingService';
import { checkSubscriptionPurchase } from 'utils/billingUtil'; import { checkSubscriptionPurchase } from 'utils/billing';
import FullScreenDropZone from 'components/FullScreenDropZone'; import FullScreenDropZone from 'components/FullScreenDropZone';
import Sidebar from 'components/Sidebar'; import Sidebar from 'components/Sidebar';
@ -57,8 +52,7 @@ import {
sortFiles, sortFiles,
sortFilesIntoCollections, sortFilesIntoCollections,
} from 'utils/file'; } from 'utils/file';
import SearchBar, { DateValue } from 'components/SearchBar'; import SearchBar from 'components/SearchBar';
import { Bbox } from 'services/searchService';
import SelectedFileOptions from 'components/pages/gallery/SelectedFileOptions'; import SelectedFileOptions from 'components/pages/gallery/SelectedFileOptions';
import CollectionSelector, { import CollectionSelector, {
CollectionSelectorAttributes, CollectionSelectorAttributes,
@ -70,14 +64,15 @@ import AlertBanner from 'components/pages/gallery/AlertBanner';
import UploadButton from 'components/pages/gallery/UploadButton'; import UploadButton from 'components/pages/gallery/UploadButton';
import PlanSelector from 'components/pages/gallery/PlanSelector'; import PlanSelector from 'components/pages/gallery/PlanSelector';
import Upload from 'components/pages/gallery/Upload'; import Upload from 'components/pages/gallery/Upload';
import Collections, { import {
ALL_SECTION, ALL_SECTION,
ARCHIVE_SECTION, ARCHIVE_SECTION,
CollectionType,
TRASH_SECTION, TRASH_SECTION,
} from 'components/pages/gallery/Collections'; } from 'constants/collection';
import { AppContext } from 'pages/_app'; import { AppContext } from 'pages/_app';
import { CustomError, ServerErrorCodes } from 'utils/common/errorUtil'; import { CustomError, ServerErrorCodes } from 'utils/error';
import { PAGES } from 'types'; import { PAGES } from 'constants/pages';
import { import {
COLLECTION_OPS_TYPE, COLLECTION_OPS_TYPE,
isSharedCollection, isSharedCollection,
@ -92,12 +87,18 @@ import {
getLocalTrash, getLocalTrash,
getTrashedFiles, getTrashedFiles,
syncTrash, syncTrash,
Trash,
} from 'services/trashService'; } from 'services/trashService';
import { Trash } from 'types/trash';
import DeleteBtn from 'components/DeleteBtn'; import DeleteBtn from 'components/DeleteBtn';
import FixCreationTime, { import FixCreationTime, {
FixCreationTimeAttributes, FixCreationTimeAttributes,
} from 'components/FixCreationTime'; } from 'components/FixCreationTime';
import { Collection, CollectionAndItsLatestFile } from 'types/collection';
import { EnteFile } from 'types/file';
import { GalleryContextType, SelectedState, Search } from 'types/gallery';
import Collections from 'components/pages/gallery/Collections';
import { VISIBILITY_STATE } from 'constants/file';
export const DeadCenter = styled.div` export const DeadCenter = styled.div`
flex: 1; flex: 1;
@ -114,34 +115,6 @@ const AlertContainer = styled.div`
text-align: center; text-align: center;
`; `;
export type SelectedState = {
[k: number]: boolean;
count: number;
collectionID: number;
};
export type SetFiles = React.Dispatch<React.SetStateAction<File[]>>;
export type SetCollections = React.Dispatch<React.SetStateAction<Collection[]>>;
export type SetLoading = React.Dispatch<React.SetStateAction<Boolean>>;
export type setSearchStats = React.Dispatch<React.SetStateAction<SearchStats>>;
export type Search = {
date?: DateValue;
location?: Bbox;
fileIndex?: number;
};
export interface SearchStats {
resultCount: number;
timeTaken: number;
}
type GalleryContextType = {
thumbs: Map<number, string>;
files: Map<number, string>;
showPlanSelectorModal: () => void;
setActiveCollection: (collection: number) => void;
syncWithRemote: (force?: boolean, silent?: boolean) => Promise<void>;
};
const defaultGalleryContext: GalleryContextType = { const defaultGalleryContext: GalleryContextType = {
thumbs: new Map(), thumbs: new Map(),
files: new Map(), files: new Map(),
@ -159,7 +132,7 @@ export default function Gallery() {
const [collections, setCollections] = useState<Collection[]>([]); const [collections, setCollections] = useState<Collection[]>([]);
const [collectionsAndTheirLatestFile, setCollectionsAndTheirLatestFile] = const [collectionsAndTheirLatestFile, setCollectionsAndTheirLatestFile] =
useState<CollectionAndItsLatestFile[]>([]); useState<CollectionAndItsLatestFile[]>([]);
const [files, setFiles] = useState<File[]>(null); const [files, setFiles] = useState<EnteFile[]>(null);
const [favItemIds, setFavItemIds] = useState<Set<number>>(); const [favItemIds, setFavItemIds] = useState<Set<number>>();
const [bannerMessage, setBannerMessage] = useState<JSX.Element | string>( const [bannerMessage, setBannerMessage] = useState<JSX.Element | string>(
null null
@ -339,7 +312,7 @@ export default function Gallery() {
const setDerivativeState = async ( const setDerivativeState = async (
collections: Collection[], collections: Collection[],
files: File[] files: EnteFile[]
) => { ) => {
const favItemIds = await getFavItemIds(files); const favItemIds = await getFavItemIds(files);
setFavItemIds(favItemIds); setFavItemIds(favItemIds);

View file

@ -1,6 +1,6 @@
import React, { useState, useEffect, useContext } from 'react'; import React, { useState, useEffect, useContext } from 'react';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
import { logoutUser, putAttributes, User } from 'services/userService'; import { logoutUser, putAttributes } from 'services/userService';
import { getData, LS_KEYS } from 'utils/storage/localStorage'; import { getData, LS_KEYS } from 'utils/storage/localStorage';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { getKey, SESSION_KEYS } from 'utils/storage/sessionStorage'; import { getKey, SESSION_KEYS } from 'utils/storage/sessionStorage';
@ -12,17 +12,12 @@ import {
import SetPasswordForm from 'components/SetPasswordForm'; import SetPasswordForm from 'components/SetPasswordForm';
import { justSignedUp, setJustSignedUp } from 'utils/storage'; import { justSignedUp, setJustSignedUp } from 'utils/storage';
import RecoveryKeyModal from 'components/RecoveryKeyModal'; import RecoveryKeyModal from 'components/RecoveryKeyModal';
import { KeyAttributes, PAGES } from 'types'; import { PAGES } from 'constants/pages';
import Container from 'components/Container'; import Container from 'components/Container';
import EnteSpinner from 'components/EnteSpinner'; import EnteSpinner from 'components/EnteSpinner';
import { AppContext } from 'pages/_app'; import { AppContext } from 'pages/_app';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
import { KeyAttributes, User } from 'types/user';
export interface KEK {
key: string;
opsLimit: number;
memLimit: number;
}
export default function Generate() { export default function Generate() {
const [token, setToken] = useState<string>(); const [token, setToken] = useState<string>();

View file

@ -12,7 +12,7 @@ import constants from 'utils/strings/constants';
import localForage from 'utils/storage/localForage'; import localForage from 'utils/storage/localForage';
import IncognitoWarning from 'components/IncognitoWarning'; import IncognitoWarning from 'components/IncognitoWarning';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
import { PAGES } from 'types'; import { PAGES } from 'constants/pages';
const Container = styled.div` const Container = styled.div`
display: flex; display: flex;

View file

@ -6,7 +6,7 @@ import Login from 'components/Login';
import Container from 'components/Container'; import Container from 'components/Container';
import { getData, LS_KEYS } from 'utils/storage/localStorage'; import { getData, LS_KEYS } from 'utils/storage/localStorage';
import Card from 'react-bootstrap/Card'; import Card from 'react-bootstrap/Card';
import { PAGES } from 'types'; import { PAGES } from 'constants/pages';
export default function Home() { export default function Home() {
const router = useRouter(); const router = useRouter();

View file

@ -7,7 +7,7 @@ import {
setData, setData,
} from 'utils/storage/localStorage'; } from 'utils/storage/localStorage';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { KeyAttributes, PAGES } from 'types'; import { PAGES } from 'constants/pages';
import CryptoWorker, { import CryptoWorker, {
decryptAndStoreToken, decryptAndStoreToken,
SaveKeyInSessionStore, SaveKeyInSessionStore,
@ -20,7 +20,7 @@ import { AppContext } from 'pages/_app';
import LogoImg from 'components/LogoImg'; import LogoImg from 'components/LogoImg';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
import { getKey, SESSION_KEYS } from 'utils/storage/sessionStorage'; import { getKey, SESSION_KEYS } from 'utils/storage/sessionStorage';
import { User } from 'services/userService'; import { KeyAttributes, User } from 'types/user';
const bip39 = require('bip39'); const bip39 = require('bip39');
// mobile client library only supports english. // mobile client library only supports english.
bip39.setDefaultWordlist('english'); bip39.setDefaultWordlist('english');

View file

@ -6,7 +6,7 @@ import Container from 'components/Container';
import EnteSpinner from 'components/EnteSpinner'; import EnteSpinner from 'components/EnteSpinner';
import { getData, LS_KEYS } from 'utils/storage/localStorage'; import { getData, LS_KEYS } from 'utils/storage/localStorage';
import SignUp from 'components/SignUp'; import SignUp from 'components/SignUp';
import { PAGES } from 'types'; import { PAGES } from 'constants/pages';
export default function SignUpPage() { export default function SignUpPage() {
const router = useRouter(); const router = useRouter();

View file

@ -11,7 +11,7 @@ import LogoImg from 'components/LogoImg';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
import { recoverTwoFactor, removeTwoFactor } from 'services/userService'; import { recoverTwoFactor, removeTwoFactor } from 'services/userService';
import { AppContext, FLASH_MESSAGE_TYPE } from 'pages/_app'; import { AppContext, FLASH_MESSAGE_TYPE } from 'pages/_app';
import { PAGES } from 'types'; import { PAGES } from 'constants/pages';
const bip39 = require('bip39'); const bip39 = require('bip39');
// mobile client library only supports english. // mobile client library only supports english.
bip39.setDefaultWordlist('english'); bip39.setDefaultWordlist('english');

View file

@ -4,11 +4,7 @@ import { CodeBlock, FreeFlowText } from 'components/RecoveryKeyModal';
import { DeadCenter } from 'pages/gallery'; import { DeadCenter } from 'pages/gallery';
import React, { useContext, useEffect, useState } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import { Button, Card } from 'react-bootstrap'; import { Button, Card } from 'react-bootstrap';
import { import { enableTwoFactor, setupTwoFactor } from 'services/userService';
enableTwoFactor,
setupTwoFactor,
TwoFactorSecret,
} from 'services/userService';
import styled from 'styled-components'; import styled from 'styled-components';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
import Container from 'components/Container'; import Container from 'components/Container';
@ -18,7 +14,8 @@ import { B64EncryptionResult } from 'utils/crypto';
import { encryptWithRecoveryKey } from 'utils/crypto'; import { encryptWithRecoveryKey } from 'utils/crypto';
import { setData, LS_KEYS, getData } from 'utils/storage/localStorage'; import { setData, LS_KEYS, getData } from 'utils/storage/localStorage';
import { AppContext, FLASH_MESSAGE_TYPE } from 'pages/_app'; import { AppContext, FLASH_MESSAGE_TYPE } from 'pages/_app';
import { PAGES } from 'types'; import { PAGES } from 'constants/pages';
import { TwoFactorSecret } from 'types/user';
enum SetupMode { enum SetupMode {
QR_CODE, QR_CODE,

View file

@ -4,8 +4,9 @@ import VerifyTwoFactor from 'components/VerifyTwoFactor';
import router from 'next/router'; import router from 'next/router';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Button, Card } from 'react-bootstrap'; import { Button, Card } from 'react-bootstrap';
import { logoutUser, User, verifyTwoFactor } from 'services/userService'; import { logoutUser, verifyTwoFactor } from 'services/userService';
import { PAGES } from 'types'; import { PAGES } from 'constants/pages';
import { User } from 'types/user';
import { setData, LS_KEYS, getData } from 'utils/storage/localStorage'; import { setData, LS_KEYS, getData } from 'utils/storage/localStorage';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';

View file

@ -12,8 +12,6 @@ import {
getOtt, getOtt,
logoutUser, logoutUser,
clearFiles, clearFiles,
EmailVerificationResponse,
User,
putAttributes, putAttributes,
} from 'services/userService'; } from 'services/userService';
import { setIsFirstLogin } from 'utils/storage'; import { setIsFirstLogin } from 'utils/storage';
@ -21,7 +19,8 @@ import SubmitButton from 'components/SubmitButton';
import { clearKeys } from 'utils/storage/sessionStorage'; import { clearKeys } from 'utils/storage/sessionStorage';
import { AppContext } from 'pages/_app'; import { AppContext } from 'pages/_app';
import LogoImg from 'components/LogoImg'; import LogoImg from 'components/LogoImg';
import { KeyAttributes, PAGES } from 'types'; import { PAGES } from 'constants/pages';
import { KeyAttributes, EmailVerificationResponse, User } from 'types/user';
interface formValues { interface formValues {
ott: string; ott: string;

View file

@ -1,10 +1,11 @@
import { getEndpoint, getPaymentsUrl } from 'utils/common/apiUtil'; import { getEndpoint, getPaymentsUrl } from 'utils/common/apiUtil';
import { getToken } from 'utils/common/key'; import { getToken } from 'utils/common/key';
import { setData, LS_KEYS } from 'utils/storage/localStorage'; import { setData, LS_KEYS } from 'utils/storage/localStorage';
import { convertToHumanReadable } from 'utils/billingUtil'; import { convertToHumanReadable } from 'utils/billing';
import HTTPService from './HTTPService'; import HTTPService from './HTTPService';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
import { getPaymentToken } from './userService'; import { getPaymentToken } from './userService';
import { Plan, Subscription } from 'types/billing';
const ENDPOINT = getEndpoint(); const ENDPOINT = getEndpoint();
@ -12,31 +13,7 @@ enum PaymentActionType {
Buy = 'buy', Buy = 'buy',
Update = 'update', Update = 'update',
} }
export interface Subscription {
id: number;
userID: number;
productID: string;
storage: number;
originalTransactionID: string;
expiryTime: number;
paymentProvider: string;
attributes: {
isCancelled: boolean;
};
price: string;
period: string;
}
export interface Plan {
id: string;
androidID: string;
iosID: string;
storage: number;
price: string;
period: string;
stripeID: string;
}
export const FREE_PLAN = 'free';
class billingService { class billingService {
public async getPlans(): Promise<Plan[]> { public async getPlans(): Promise<Plan[]> {
try { try {

View file

@ -6,79 +6,26 @@ import { getActualKey, getToken } from 'utils/common/key';
import CryptoWorker from 'utils/crypto'; import CryptoWorker from 'utils/crypto';
import { SetDialogMessage } from 'components/MessageDialog'; import { SetDialogMessage } from 'components/MessageDialog';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
import { getPublicKey, User } from './userService'; import { getPublicKey } from './userService';
import { B64EncryptionResult } from 'utils/crypto'; import { B64EncryptionResult } from 'utils/crypto';
import HTTPService from './HTTPService'; import HTTPService from './HTTPService';
import { File } from './fileService'; import { EnteFile } from 'types/file';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
import { CustomError } from 'utils/common/errorUtil'; import { CustomError } from 'utils/error';
import { sortFiles } from 'utils/file'; import { sortFiles } from 'utils/file';
import {
Collection,
CollectionAndItsLatestFile,
AddToCollectionRequest,
MoveToCollectionRequest,
EncryptedFileKey,
RemoveFromCollectionRequest,
} from 'types/collection';
import { COLLECTION_SORT_BY, CollectionType } from 'constants/collection';
const ENDPOINT = getEndpoint(); const ENDPOINT = getEndpoint();
const COLLECTION_TABLE = 'collections';
export enum CollectionType {
folder = 'folder',
favorites = 'favorites',
album = 'album',
}
const COLLECTION_UPDATION_TIME = 'collection-updation-time'; const COLLECTION_UPDATION_TIME = 'collection-updation-time';
const COLLECTIONS = 'collections';
export interface Collection {
id: number;
owner: User;
key?: string;
name?: string;
encryptedName?: string;
nameDecryptionNonce?: string;
type: CollectionType;
attributes: collectionAttributes;
sharees: User[];
updationTime: number;
encryptedKey: string;
keyDecryptionNonce: string;
isDeleted: boolean;
isSharedCollection?: boolean;
}
interface EncryptedFileKey {
id: number;
encryptedKey: string;
keyDecryptionNonce: string;
}
interface AddToCollectionRequest {
collectionID: number;
files: EncryptedFileKey[];
}
interface MoveToCollectionRequest {
fromCollectionID: number;
toCollectionID: number;
files: EncryptedFileKey[];
}
interface collectionAttributes {
encryptedPath?: string;
pathDecryptionNonce?: string;
}
export interface CollectionAndItsLatestFile {
collection: Collection;
file: File;
}
export enum COLLECTION_SORT_BY {
LATEST_FILE,
MODIFICATION_TIME,
NAME,
}
interface RemoveFromCollectionRequest {
collectionID: number;
fileIDs: number[];
}
const getCollectionWithSecrets = async ( const getCollectionWithSecrets = async (
collection: Collection, collection: Collection,
@ -164,7 +111,7 @@ const getCollections = async (
export const getLocalCollections = async (): Promise<Collection[]> => { export const getLocalCollections = async (): Promise<Collection[]> => {
const collections: Collection[] = const collections: Collection[] =
(await localForage.getItem(COLLECTIONS)) ?? []; (await localForage.getItem(COLLECTION_TABLE)) ?? [];
return collections; return collections;
}; };
@ -212,7 +159,7 @@ export const syncCollections = async () => {
[], [],
COLLECTION_SORT_BY.MODIFICATION_TIME COLLECTION_SORT_BY.MODIFICATION_TIME
); );
await localForage.setItem(COLLECTIONS, collections); await localForage.setItem(COLLECTION_TABLE, collections);
await localForage.setItem(COLLECTION_UPDATION_TIME, updationTime); await localForage.setItem(COLLECTION_UPDATION_TIME, updationTime);
return collections; return collections;
}; };
@ -243,9 +190,9 @@ export const getCollection = async (
export const getCollectionsAndTheirLatestFile = ( export const getCollectionsAndTheirLatestFile = (
collections: Collection[], collections: Collection[],
files: File[] files: EnteFile[]
): CollectionAndItsLatestFile[] => { ): CollectionAndItsLatestFile[] => {
const latestFile = new Map<number, File>(); const latestFile = new Map<number, EnteFile>();
files.forEach((file) => { files.forEach((file) => {
if (!latestFile.has(file.collectionID)) { if (!latestFile.has(file.collectionID)) {
@ -263,7 +210,9 @@ export const getCollectionsAndTheirLatestFile = (
return collectionsAndTheirLatestFile; return collectionsAndTheirLatestFile;
}; };
export const getFavItemIds = async (files: File[]): Promise<Set<number>> => { export const getFavItemIds = async (
files: EnteFile[]
): Promise<Set<number>> => {
const favCollection = await getFavCollection(); const favCollection = await getFavCollection();
if (!favCollection) return new Set(); if (!favCollection) return new Set();
@ -356,7 +305,7 @@ const postCollection = async (
} }
}; };
export const addToFavorites = async (file: File) => { export const addToFavorites = async (file: EnteFile) => {
try { try {
let favCollection = await getFavCollection(); let favCollection = await getFavCollection();
if (!favCollection) { if (!favCollection) {
@ -365,7 +314,7 @@ export const addToFavorites = async (file: File) => {
CollectionType.favorites CollectionType.favorites
); );
const localCollections = await getLocalCollections(); const localCollections = await getLocalCollections();
await localForage.setItem(COLLECTIONS, [ await localForage.setItem(COLLECTION_TABLE, [
...localCollections, ...localCollections,
favCollection, favCollection,
]); ]);
@ -376,7 +325,7 @@ export const addToFavorites = async (file: File) => {
} }
}; };
export const removeFromFavorites = async (file: File) => { export const removeFromFavorites = async (file: EnteFile) => {
try { try {
const favCollection = await getFavCollection(); const favCollection = await getFavCollection();
if (!favCollection) { if (!favCollection) {
@ -390,7 +339,7 @@ export const removeFromFavorites = async (file: File) => {
export const addToCollection = async ( export const addToCollection = async (
collection: Collection, collection: Collection,
files: File[] files: EnteFile[]
) => { ) => {
try { try {
const token = getToken(); const token = getToken();
@ -417,7 +366,7 @@ export const addToCollection = async (
export const restoreToCollection = async ( export const restoreToCollection = async (
collection: Collection, collection: Collection,
files: File[] files: EnteFile[]
) => { ) => {
try { try {
const token = getToken(); const token = getToken();
@ -444,7 +393,7 @@ export const restoreToCollection = async (
export const moveToCollection = async ( export const moveToCollection = async (
fromCollectionID: number, fromCollectionID: number,
toCollection: Collection, toCollection: Collection,
files: File[] files: EnteFile[]
) => { ) => {
try { try {
const token = getToken(); const token = getToken();
@ -472,7 +421,7 @@ export const moveToCollection = async (
const encryptWithNewCollectionKey = async ( const encryptWithNewCollectionKey = async (
newCollection: Collection, newCollection: Collection,
files: File[] files: EnteFile[]
): Promise<EncryptedFileKey[]> => { ): Promise<EncryptedFileKey[]> => {
const fileKeysEncryptedWithNewCollection: EncryptedFileKey[] = []; const fileKeysEncryptedWithNewCollection: EncryptedFileKey[] = [];
const worker = await new CryptoWorker(); const worker = await new CryptoWorker();
@ -494,7 +443,7 @@ const encryptWithNewCollectionKey = async (
}; };
export const removeFromCollection = async ( export const removeFromCollection = async (
collection: Collection, collection: Collection,
files: File[] files: EnteFile[]
) => { ) => {
try { try {
const token = getToken(); const token = getToken();
@ -637,7 +586,7 @@ export const getFavCollection = async () => {
export const getNonEmptyCollections = ( export const getNonEmptyCollections = (
collections: Collection[], collections: Collection[],
files: File[] files: EnteFile[]
) => { ) => {
const nonEmptyCollectionsIds = new Set<number>(); const nonEmptyCollectionsIds = new Set<number>();
for (const file of files) { for (const file of files) {

View file

@ -7,14 +7,16 @@ import {
needsConversionForPreview, needsConversionForPreview,
} from 'utils/file'; } from 'utils/file';
import HTTPService from './HTTPService'; import HTTPService from './HTTPService';
import { File, FILE_TYPE } from './fileService'; import { EnteFile } from 'types/file';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
import { FILE_TYPE } from 'constants/file';
class DownloadManager { class DownloadManager {
private fileObjectUrlPromise = new Map<string, Promise<string>>(); private fileObjectUrlPromise = new Map<string, Promise<string>>();
private thumbnailObjectUrlPromise = new Map<number, Promise<string>>(); private thumbnailObjectUrlPromise = new Map<number, Promise<string>>();
public async getThumbnail(file: File) { public async getThumbnail(file: EnteFile) {
try { try {
const token = getToken(); const token = getToken();
if (!token) { if (!token) {
@ -52,7 +54,7 @@ class DownloadManager {
} }
} }
downloadThumb = async (token: string, file: File) => { downloadThumb = async (token: string, file: EnteFile) => {
const resp = await HTTPService.get( const resp = await HTTPService.get(
getThumbnailUrl(file.id), getThumbnailUrl(file.id),
null, null,
@ -68,7 +70,7 @@ class DownloadManager {
return decrypted; return decrypted;
}; };
getFile = async (file: File, forPreview = false) => { getFile = async (file: EnteFile, forPreview = false) => {
const shouldBeConverted = forPreview && needsConversionForPreview(file); const shouldBeConverted = forPreview && needsConversionForPreview(file);
const fileKey = shouldBeConverted const fileKey = shouldBeConverted
? `${file.id}_converted` ? `${file.id}_converted`
@ -97,11 +99,11 @@ class DownloadManager {
} }
}; };
public async getCachedOriginalFile(file: File) { public async getCachedOriginalFile(file: EnteFile) {
return await this.fileObjectUrlPromise.get(file.id.toString()); return await this.fileObjectUrlPromise.get(file.id.toString());
} }
async downloadFile(file: File) { async downloadFile(file: EnteFile) {
const worker = await new CryptoWorker(); const worker = await new CryptoWorker();
const token = getToken(); const token = getToken();
if (!token) { if (!token) {

View file

@ -23,80 +23,36 @@ import { retryAsyncFunction } from 'utils/network';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
import { getData, LS_KEYS } from 'utils/storage/localStorage'; import { getData, LS_KEYS } from 'utils/storage/localStorage';
import { import {
Collection,
getLocalCollections, getLocalCollections,
getNonEmptyCollections, getNonEmptyCollections,
} from './collectionService'; } from './collectionService';
import downloadManager from './downloadManager'; import downloadManager from './downloadManager';
import { File, FILE_TYPE, getLocalFiles } from './fileService'; import { getLocalFiles } from './fileService';
import { EnteFile } from 'types/file';
import { decodeMotionPhoto } from './motionPhotoService'; import { decodeMotionPhoto } from './motionPhotoService';
import { import {
fileNameWithoutExtension, fileNameWithoutExtension,
generateStreamFromArrayBuffer, generateStreamFromArrayBuffer,
getFileExtension, getFileExtension,
mergeMetadata, mergeMetadata,
TYPE_JPEG,
TYPE_JPG,
} from 'utils/file'; } from 'utils/file';
import { User } from './userService';
import { updateFileCreationDateInEXIF } from './upload/exifService';
import { MetadataObject } from './upload/uploadService';
import QueueProcessor from './upload/queueProcessor';
export type CollectionIDPathMap = Map<number, string>; import { updateFileCreationDateInEXIF } from './upload/exifService';
export interface ExportProgress { import { Metadata } from 'types/upload';
current: number; import QueueProcessor from './queueProcessor';
total: number; import { Collection } from 'types/collection';
} import {
export interface ExportedCollectionPaths { ExportProgress,
[collectionID: number]: string; CollectionIDPathMap,
} ExportRecord,
export interface ExportStats { } from 'types/export';
failed: number; import { User } from 'types/user';
success: number; import { FILE_TYPE, TYPE_JPEG, TYPE_JPG } from 'constants/file';
} import { ExportType, ExportNotification, RecordType } from 'constants/export';
const LATEST_EXPORT_VERSION = 1; const LATEST_EXPORT_VERSION = 1;
export interface ExportRecord {
version?: number;
stage?: ExportStage;
lastAttemptTimestamp?: number;
progress?: ExportProgress;
queuedFiles?: string[];
exportedFiles?: string[];
failedFiles?: string[];
exportedCollectionPaths?: ExportedCollectionPaths;
}
export enum ExportStage {
INIT,
INPROGRESS,
PAUSED,
FINISHED,
}
enum ExportNotification {
START = 'export started',
IN_PROGRESS = 'export already in progress',
FINISH = 'export finished',
FAILED = 'export failed',
ABORT = 'export aborted',
PAUSE = 'export paused',
UP_TO_DATE = `no new files to export`,
}
enum RecordType {
SUCCESS = 'success',
FAILED = 'failed',
}
export enum ExportType {
NEW,
PENDING,
RETRY_FAILED,
}
const EXPORT_RECORD_FILE_NAME = 'export_status.json'; const EXPORT_RECORD_FILE_NAME = 'export_status.json';
export const METADATA_FOLDER_NAME = 'metadata';
class ExportService { class ExportService {
ElectronAPIs: any; ElectronAPIs: any;
@ -106,6 +62,7 @@ class ExportService {
private stopExport: boolean = false; private stopExport: boolean = false;
private pauseExport: boolean = false; private pauseExport: boolean = false;
private allElectronAPIsExist: boolean = false; private allElectronAPIsExist: boolean = false;
private fileReader: FileReader = null;
constructor() { constructor() {
this.ElectronAPIs = runningInBrowser() && window['ElectronAPIs']; this.ElectronAPIs = runningInBrowser() && window['ElectronAPIs'];
@ -139,7 +96,7 @@ class ExportService {
} }
const user: User = getData(LS_KEYS.USER); const user: User = getData(LS_KEYS.USER);
let filesToExport: File[]; let filesToExport: EnteFile[];
const localFiles = await getLocalFiles(); const localFiles = await getLocalFiles();
const userPersonalFiles = localFiles const userPersonalFiles = localFiles
.filter((file) => file.ownerID === user?.id) .filter((file) => file.ownerID === user?.id)
@ -210,7 +167,7 @@ class ExportService {
} }
async fileExporter( async fileExporter(
files: File[], files: EnteFile[],
newCollections: Collection[], newCollections: Collection[],
renamedCollections: Collection[], renamedCollections: Collection[],
collectionIDPathMap: CollectionIDPathMap, collectionIDPathMap: CollectionIDPathMap,
@ -318,13 +275,17 @@ class ExportService {
throw e; throw e;
} }
} }
async addFilesQueuedRecord(folder: string, files: File[]) { async addFilesQueuedRecord(folder: string, files: EnteFile[]) {
const exportRecord = await this.getExportRecord(folder); const exportRecord = await this.getExportRecord(folder);
exportRecord.queuedFiles = files.map(getExportRecordFileUID); exportRecord.queuedFiles = files.map(getExportRecordFileUID);
await this.updateExportRecord(exportRecord, folder); await this.updateExportRecord(exportRecord, folder);
} }
async addFileExportedRecord(folder: string, file: File, type: RecordType) { async addFileExportedRecord(
folder: string,
file: EnteFile,
type: RecordType
) {
const fileUID = getExportRecordFileUID(file); const fileUID = getExportRecordFileUID(file);
const exportRecord = await this.getExportRecord(folder); const exportRecord = await this.getExportRecord(folder);
exportRecord.queuedFiles = exportRecord.queuedFiles.filter( exportRecord.queuedFiles = exportRecord.queuedFiles.filter(
@ -462,7 +423,7 @@ class ExportService {
} }
} }
async downloadAndSave(file: File, collectionPath: string) { async downloadAndSave(file: EnteFile, collectionPath: string) {
file.metadata = mergeMetadata([file])[0].metadata; file.metadata = mergeMetadata([file])[0].metadata;
const fileSaveName = getUniqueFileSaveName( const fileSaveName = getUniqueFileSaveName(
collectionPath, collectionPath,
@ -478,7 +439,11 @@ class ExportService {
(fileType === TYPE_JPEG || fileType === TYPE_JPG) (fileType === TYPE_JPEG || fileType === TYPE_JPG)
) { ) {
const fileBlob = await new Response(fileStream).blob(); const fileBlob = await new Response(fileStream).blob();
if (!this.fileReader) {
this.fileReader = new FileReader();
}
const updatedFileBlob = await updateFileCreationDateInEXIF( const updatedFileBlob = await updateFileCreationDateInEXIF(
this.fileReader,
fileBlob, fileBlob,
new Date(file.pubMagicMetadata.data.editedTime / 1000) new Date(file.pubMagicMetadata.data.editedTime / 1000)
); );
@ -498,7 +463,7 @@ class ExportService {
private async exportMotionPhoto( private async exportMotionPhoto(
fileStream: ReadableStream<any>, fileStream: ReadableStream<any>,
file: File, file: EnteFile,
collectionPath: string collectionPath: string
) { ) {
const fileBlob = await new Response(fileStream).blob(); const fileBlob = await new Response(fileStream).blob();
@ -544,7 +509,7 @@ class ExportService {
private async saveMetadataFile( private async saveMetadataFile(
collectionFolderPath: string, collectionFolderPath: string,
fileSaveName: string, fileSaveName: string,
metadata: MetadataObject metadata: Metadata
) { ) {
await this.ElectronAPIs.saveFileToDisk( await this.ElectronAPIs.saveFileToDisk(
getFileMetadataSavePath(collectionFolderPath, fileSaveName), getFileMetadataSavePath(collectionFolderPath, fileSaveName),
@ -572,7 +537,7 @@ class ExportService {
private async migrateExport( private async migrateExport(
exportDir: string, exportDir: string,
collections: Collection[], collections: Collection[],
allFiles: File[] allFiles: EnteFile[]
) { ) {
const exportRecord = await this.getExportRecord(exportDir); const exportRecord = await this.getExportRecord(exportDir);
const currentVersion = exportRecord?.version ?? 0; const currentVersion = exportRecord?.version ?? 0;
@ -633,7 +598,7 @@ class ExportService {
`fileID_fileName` to newer `fileName(numbered)` format `fileID_fileName` to newer `fileName(numbered)` format
*/ */
private async migrateFiles( private async migrateFiles(
files: File[], files: EnteFile[],
collectionIDPathMap: Map<number, string> collectionIDPathMap: Map<number, string>
) { ) {
for (let file of files) { for (let file of files) {

View file

@ -1,12 +1,13 @@
import { createFFmpeg, FFmpeg } from '@ffmpeg/ffmpeg'; import { createFFmpeg, FFmpeg } from '@ffmpeg/ffmpeg';
import { CustomError } from 'utils/common/errorUtil'; import { CustomError } from 'utils/error';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
import QueueProcessor from './upload/queueProcessor'; import QueueProcessor from './queueProcessor';
import { getUint8ArrayView } from './upload/readFileService'; import { getUint8ArrayView } from './upload/readFileService';
class FFmpegService { class FFmpegService {
private ffmpeg: FFmpeg = null; private ffmpeg: FFmpeg = null;
private isLoading = null; private isLoading = null;
private fileReader: FileReader = null;
private generateThumbnailProcessor = new QueueProcessor<Uint8Array>(1); private generateThumbnailProcessor = new QueueProcessor<Uint8Array>(1);
async init() { async init() {
@ -29,11 +30,19 @@ class FFmpegService {
if (!this.ffmpeg) { if (!this.ffmpeg) {
await this.init(); await this.init();
} }
if (!this.fileReader) {
this.fileReader = new FileReader();
}
if (this.isLoading) { if (this.isLoading) {
await this.isLoading; await this.isLoading;
} }
const response = this.generateThumbnailProcessor.queueUpRequest( const response = this.generateThumbnailProcessor.queueUpRequest(
generateThumbnailHelper.bind(null, this.ffmpeg, file) generateThumbnailHelper.bind(
null,
this.ffmpeg,
this.fileReader,
file
)
); );
try { try {
return await response.promise; return await response.promise;
@ -49,14 +58,18 @@ class FFmpegService {
} }
} }
async function generateThumbnailHelper(ffmpeg: FFmpeg, file: File) { async function generateThumbnailHelper(
ffmpeg: FFmpeg,
reader: FileReader,
file: File
) {
try { try {
const inputFileName = `${Date.now().toString()}-${file.name}`; const inputFileName = `${Date.now().toString()}-${file.name}`;
const thumbFileName = `${Date.now().toString()}-thumb.jpeg`; const thumbFileName = `${Date.now().toString()}-thumb.jpeg`;
ffmpeg.FS( ffmpeg.FS(
'writeFile', 'writeFile',
inputFileName, inputFileName,
await getUint8ArrayView(new FileReader(), file) await getUint8ArrayView(reader, file)
); );
let seekTime = 1.0; let seekTime = 1.0;
let thumb = null; let thumb = null;

View file

@ -2,140 +2,24 @@ import { getEndpoint } from 'utils/common/apiUtil';
import localForage from 'utils/storage/localForage'; import localForage from 'utils/storage/localForage';
import { getToken } from 'utils/common/key'; import { getToken } from 'utils/common/key';
import { import { EncryptionResult } from 'types/upload';
DataStream, import { Collection } from 'types/collection';
EncryptionResult,
MetadataObject,
} from './upload/uploadService';
import { Collection } from './collectionService';
import HTTPService from './HTTPService'; import HTTPService from './HTTPService';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
import { decryptFile, mergeMetadata, sortFiles } from 'utils/file'; import { decryptFile, mergeMetadata, sortFiles } from 'utils/file';
import CryptoWorker from 'utils/crypto'; import CryptoWorker from 'utils/crypto';
import { EnteFile, TrashRequest, UpdateMagicMetadataRequest } from 'types/file';
const ENDPOINT = getEndpoint(); const ENDPOINT = getEndpoint();
const FILES_TABLE = 'files'; const FILES_TABLE = 'files';
export const MIN_EDITED_CREATION_TIME = new Date(1800, 0, 1);
export const MAX_EDITED_CREATION_TIME = new Date();
export const ALL_TIME = new Date(1800, 0, 1, 23, 59, 59);
export const MAX_EDITED_FILE_NAME_LENGTH = 100;
export interface fileAttribute {
encryptedData?: DataStream | Uint8Array;
objectKey?: string;
decryptionHeader: string;
}
export enum FILE_TYPE {
IMAGE,
VIDEO,
LIVE_PHOTO,
OTHERS,
}
/* Build error occurred
ReferenceError: Cannot access 'FILE_TYPE' before initialization
when it was placed in readFileService
*/
// list of format that were missed by type-detection for some files.
export const FORMAT_MISSED_BY_FILE_TYPE_LIB = [
{ fileType: FILE_TYPE.IMAGE, exactType: 'jpeg' },
{ fileType: FILE_TYPE.IMAGE, exactType: 'jpg' },
{ fileType: FILE_TYPE.VIDEO, exactType: 'webm' },
];
export enum VISIBILITY_STATE {
VISIBLE,
ARCHIVED,
}
export interface MagicMetadataCore {
version: number;
count: number;
header: string;
data: Record<string, any>;
}
export interface EncryptedMagicMetadataCore
extends Omit<MagicMetadataCore, 'data'> {
data: string;
}
export interface MagicMetadataProps {
visibility?: VISIBILITY_STATE;
}
export interface MagicMetadata extends Omit<MagicMetadataCore, 'data'> {
data: MagicMetadataProps;
}
export interface PublicMagicMetadataProps {
editedTime?: number;
editedName?: string;
}
export interface PublicMagicMetadata extends Omit<MagicMetadataCore, 'data'> {
data: PublicMagicMetadataProps;
}
export interface File {
id: number;
collectionID: number;
ownerID: number;
file: fileAttribute;
thumbnail: fileAttribute;
metadata: MetadataObject;
magicMetadata: MagicMetadata;
pubMagicMetadata: PublicMagicMetadata;
encryptedKey: string;
keyDecryptionNonce: string;
key: string;
src: string;
msrc: string;
html: string;
w: number;
h: number;
isDeleted: boolean;
isTrashed?: boolean;
deleteBy?: number;
dataIndex: number;
updationTime: number;
}
interface UpdateMagicMetadataRequest {
metadataList: UpdateMagicMetadata[];
}
interface UpdateMagicMetadata {
id: number;
magicMetadata: EncryptedMagicMetadataCore;
}
export const NEW_MAGIC_METADATA: MagicMetadataCore = {
version: 0,
data: {},
header: null,
count: 0,
};
interface TrashRequest {
items: TrashRequestItems[];
}
interface TrashRequestItems {
fileID: number;
collectionID: number;
}
export const getLocalFiles = async () => { export const getLocalFiles = async () => {
const files: Array<File> = const files: Array<EnteFile> =
(await localForage.getItem<File[]>(FILES_TABLE)) || []; (await localForage.getItem<EnteFile[]>(FILES_TABLE)) || [];
return files; return files;
}; };
export const setLocalFiles = async (files: File[]) => { export const setLocalFiles = async (files: EnteFile[]) => {
await localForage.setItem(FILES_TABLE, files); await localForage.setItem(FILES_TABLE, files);
}; };
@ -144,7 +28,7 @@ const getCollectionLastSyncTime = async (collection: Collection) =>
export const syncFiles = async ( export const syncFiles = async (
collections: Collection[], collections: Collection[],
setFiles: (files: File[]) => void setFiles: (files: EnteFile[]) => void
) => { ) => {
const localFiles = await getLocalFiles(); const localFiles = await getLocalFiles();
let files = await removeDeletedCollectionFiles(collections, localFiles); let files = await removeDeletedCollectionFiles(collections, localFiles);
@ -163,7 +47,7 @@ export const syncFiles = async (
const fetchedFiles = const fetchedFiles =
(await getFiles(collection, lastSyncTime, files, setFiles)) ?? []; (await getFiles(collection, lastSyncTime, files, setFiles)) ?? [];
files.push(...fetchedFiles); files.push(...fetchedFiles);
const latestVersionFiles = new Map<string, File>(); const latestVersionFiles = new Map<string, EnteFile>();
files.forEach((file) => { files.forEach((file) => {
const uid = `${file.collectionID}-${file.id}`; const uid = `${file.collectionID}-${file.id}`;
if ( if (
@ -194,11 +78,11 @@ export const syncFiles = async (
export const getFiles = async ( export const getFiles = async (
collection: Collection, collection: Collection,
sinceTime: number, sinceTime: number,
files: File[], files: EnteFile[],
setFiles: (files: File[]) => void setFiles: (files: EnteFile[]) => void
): Promise<File[]> => { ): Promise<EnteFile[]> => {
try { try {
const decryptedFiles: File[] = []; const decryptedFiles: EnteFile[] = [];
let time = sinceTime; let time = sinceTime;
let resp; let resp;
do { do {
@ -219,12 +103,12 @@ export const getFiles = async (
decryptedFiles.push( decryptedFiles.push(
...(await Promise.all( ...(await Promise.all(
resp.data.diff.map(async (file: File) => { resp.data.diff.map(async (file: EnteFile) => {
if (!file.isDeleted) { if (!file.isDeleted) {
file = await decryptFile(file, collection); file = await decryptFile(file, collection);
} }
return file; return file;
}) as Promise<File>[] }) as Promise<EnteFile>[]
)) ))
); );
@ -249,7 +133,7 @@ export const getFiles = async (
const removeDeletedCollectionFiles = async ( const removeDeletedCollectionFiles = async (
collections: Collection[], collections: Collection[],
files: File[] files: EnteFile[]
) => { ) => {
const syncedCollectionIds = new Set<number>(); const syncedCollectionIds = new Set<number>();
for (const collection of collections) { for (const collection of collections) {
@ -259,7 +143,7 @@ const removeDeletedCollectionFiles = async (
return files; return files;
}; };
export const trashFiles = async (filesToTrash: File[]) => { export const trashFiles = async (filesToTrash: EnteFile[]) => {
try { try {
const token = getToken(); const token = getToken();
if (!token) { if (!token) {
@ -300,7 +184,7 @@ export const deleteFromTrash = async (filesToDelete: number[]) => {
} }
}; };
export const updateMagicMetadata = async (files: File[]) => { export const updateMagicMetadata = async (files: EnteFile[]) => {
const token = getToken(); const token = getToken();
if (!token) { if (!token) {
return; return;
@ -324,7 +208,7 @@ export const updateMagicMetadata = async (files: File[]) => {
'X-Auth-Token': token, 'X-Auth-Token': token,
}); });
return files.map( return files.map(
(file): File => ({ (file): EnteFile => ({
...file, ...file,
magicMetadata: { magicMetadata: {
...file.magicMetadata, ...file.magicMetadata,
@ -334,7 +218,7 @@ export const updateMagicMetadata = async (files: File[]) => {
); );
}; };
export const updatePublicMagicMetadata = async (files: File[]) => { export const updatePublicMagicMetadata = async (files: EnteFile[]) => {
const token = getToken(); const token = getToken();
if (!token) { if (!token) {
return; return;
@ -363,7 +247,7 @@ export const updatePublicMagicMetadata = async (files: File[]) => {
} }
); );
return files.map( return files.map(
(file): File => ({ (file): EnteFile => ({
...file, ...file,
pubMagicMetadata: { pubMagicMetadata: {
...file.pubMagicMetadata, ...file.pubMagicMetadata,

View file

@ -1,5 +1,5 @@
import downloadManager from 'services/downloadManager'; import downloadManager from 'services/downloadManager';
import { fileAttribute, getLocalFiles } from 'services/fileService'; import { getLocalFiles } from 'services/fileService';
import { generateThumbnail } from 'services/upload/thumbnailService'; import { generateThumbnail } from 'services/upload/thumbnailService';
import { getToken } from 'utils/common/key'; import { getToken } from 'utils/common/key';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
@ -7,10 +7,11 @@ import { getEndpoint } from 'utils/common/apiUtil';
import HTTPService from 'services/HTTPService'; import HTTPService from 'services/HTTPService';
import CryptoWorker from 'utils/crypto'; import CryptoWorker from 'utils/crypto';
import uploadHttpClient from 'services/upload/uploadHttpClient'; import uploadHttpClient from 'services/upload/uploadHttpClient';
import { EncryptionResult, UploadURL } from 'services/upload/uploadService';
import { SetProgressTracker } from 'components/FixLargeThumbnail'; import { SetProgressTracker } from 'components/FixLargeThumbnail';
import { getFileType } from './upload/readFileService'; import { getFileType } from './upload/readFileService';
import { getLocalTrash, getTrashedFiles } from './trashService'; import { getLocalTrash, getTrashedFiles } from './trashService';
import { EncryptionResult, UploadURL } from 'types/upload';
import { fileAttribute } from 'types/file';
const ENDPOINT = getEndpoint(); const ENDPOINT = getEndpoint();
const REPLACE_THUMBNAIL_THRESHOLD = 500 * 1024; // 500KB const REPLACE_THUMBNAIL_THRESHOLD = 500 * 1024; // 500KB
@ -43,6 +44,7 @@ export async function replaceThumbnail(
try { try {
const token = getToken(); const token = getToken();
const worker = await new CryptoWorker(); const worker = await new CryptoWorker();
const reader = new FileReader();
const files = await getLocalFiles(); const files = await getLocalFiles();
const trash = await getLocalTrash(); const trash = await getLocalTrash();
const trashFiles = getTrashedFiles(trash); const trashFiles = getTrashedFiles(trash);
@ -71,13 +73,14 @@ export async function replaceThumbnail(
token, token,
file file
); );
const dummyImageFile = new globalThis.File( const dummyImageFile = new File(
[originalThumbnail], [originalThumbnail],
file.metadata.title file.metadata.title
); );
const fileTypeInfo = await getFileType(worker, dummyImageFile); const fileTypeInfo = await getFileType(reader, dummyImageFile);
const { thumbnail: newThumbnail } = await generateThumbnail( const { thumbnail: newThumbnail } = await generateThumbnail(
worker, worker,
reader,
dummyImageFile, dummyImageFile,
fileTypeInfo fileTypeInfo
); );

View file

@ -1,4 +1,4 @@
import { CustomError } from 'utils/common/errorUtil'; import { CustomError } from 'utils/error';
interface RequestQueueItem { interface RequestQueueItem {
request: (canceller?: RequestCanceller) => Promise<any>; request: (canceller?: RequestCanceller) => Promise<any>;
@ -43,7 +43,7 @@ export default class QueueProcessor<T> {
return { promise, canceller }; return { promise, canceller };
} }
async pollQueue() { private async pollQueue() {
if (this.requestInProcessing < this.maxParallelProcesses) { if (this.requestInProcessing < this.maxParallelProcesses) {
this.requestInProcessing++; this.requestInProcessing++;
await this.processQueue(); await this.processQueue();
@ -51,9 +51,9 @@ export default class QueueProcessor<T> {
} }
} }
public async processQueue() { private async processQueue() {
while (this.requestQueue.length > 0) { while (this.requestQueue.length > 0) {
const queueItem = this.requestQueue.pop(); const queueItem = this.requestQueue.shift();
let response = null; let response = null;
if (queueItem.isCanceled.status) { if (queueItem.isCanceled.status) {

View file

@ -1,20 +1,21 @@
import * as chrono from 'chrono-node'; import * as chrono from 'chrono-node';
import { getEndpoint } from 'utils/common/apiUtil'; import { getEndpoint } from 'utils/common/apiUtil';
import { getToken } from 'utils/common/key'; import { getToken } from 'utils/common/key';
import { DateValue, Suggestion, SuggestionType } from 'components/SearchBar';
import HTTPService from './HTTPService'; import HTTPService from './HTTPService';
import { Collection } from './collectionService'; import { Collection } from 'types/collection';
import { File } from './fileService'; import { EnteFile } from 'types/file';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
import {
DateValue,
LocationSearchResponse,
Suggestion,
SuggestionType,
} from 'types/search';
const ENDPOINT = getEndpoint(); const ENDPOINT = getEndpoint();
const DIGITS = new Set(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']); const DIGITS = new Set(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']);
export type Bbox = [number, number, number, number];
export interface LocationSearchResponse {
place: string;
bbox: Bbox;
}
export const getMapboxToken = () => process.env.NEXT_PUBLIC_MAPBOX_TOKEN;
export function parseHumanDate(humanDate: string): DateValue[] { export function parseHumanDate(humanDate: string): DateValue[] {
const date = chrono.parseDate(humanDate); const date = chrono.parseDate(humanDate);
@ -118,7 +119,7 @@ export function searchCollection(
); );
} }
export function searchFiles(searchPhrase: string, files: File[]) { export function searchFiles(searchPhrase: string, files: EnteFile[]) {
return files return files
.map((file, idx) => ({ .map((file, idx) => ({
title: file.metadata.title, title: file.metadata.title,

View file

@ -1,12 +1,15 @@
import { SetFiles } from 'pages/gallery'; import { SetFiles } from 'types/gallery';
import { Collection } from 'types/collection';
import { getEndpoint } from 'utils/common/apiUtil'; import { getEndpoint } from 'utils/common/apiUtil';
import { getToken } from 'utils/common/key'; import { getToken } from 'utils/common/key';
import { decryptFile, mergeMetadata, sortFiles } from 'utils/file'; import { decryptFile, mergeMetadata, sortFiles } from 'utils/file';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
import localForage from 'utils/storage/localForage'; import localForage from 'utils/storage/localForage';
import { Collection, getCollection } from './collectionService'; import { getCollection } from './collectionService';
import { File } from './fileService'; import { EnteFile } from 'types/file';
import HTTPService from './HTTPService'; import HTTPService from './HTTPService';
import { Trash, TrashItem } from 'types/trash';
const TRASH = 'file-trash'; const TRASH = 'file-trash';
const TRASH_TIME = 'trash-time'; const TRASH_TIME = 'trash-time';
@ -14,16 +17,6 @@ const DELETED_COLLECTION = 'deleted-collection';
const ENDPOINT = getEndpoint(); const ENDPOINT = getEndpoint();
export interface TrashItem {
file: File;
isDeleted: boolean;
isRestored: boolean;
deleteBy: number;
createdAt: number;
updatedAt: number;
}
export type Trash = TrashItem[];
export async function getLocalTrash() { export async function getLocalTrash() {
const trash = (await localForage.getItem<Trash>(TRASH)) || []; const trash = (await localForage.getItem<Trash>(TRASH)) || [];
return trash; return trash;
@ -52,7 +45,7 @@ async function getLastSyncTime() {
export async function syncTrash( export async function syncTrash(
collections: Collection[], collections: Collection[],
setFiles: SetFiles, setFiles: SetFiles,
files: File[] files: EnteFile[]
): Promise<Trash> { ): Promise<Trash> {
const trash = await getLocalTrash(); const trash = await getLocalTrash();
collections = [...collections, ...(await getLocalDeletedCollections())]; collections = [...collections, ...(await getLocalDeletedCollections())];
@ -79,7 +72,7 @@ export const updateTrash = async (
collections: Map<number, Collection>, collections: Map<number, Collection>,
sinceTime: number, sinceTime: number,
setFiles: SetFiles, setFiles: SetFiles,
files: File[], files: EnteFile[],
currentTrash: Trash currentTrash: Trash
): Promise<Trash> => { ): Promise<Trash> => {
try { try {

View file

@ -1,6 +1,5 @@
import { FIX_OPTIONS } from 'components/FixCreationTime'; import { FIX_OPTIONS } from 'components/FixCreationTime';
import { SetProgressTracker } from 'components/FixLargeThumbnail'; import { SetProgressTracker } from 'components/FixLargeThumbnail';
import CryptoWorker from 'utils/crypto';
import { import {
changeFileCreationTime, changeFileCreationTime,
getFileFromURL, getFileFromURL,
@ -8,12 +7,15 @@ import {
} from 'utils/file'; } from 'utils/file';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
import downloadManager from './downloadManager'; import downloadManager from './downloadManager';
import { File, FILE_TYPE, updatePublicMagicMetadata } from './fileService'; import { updatePublicMagicMetadata } from './fileService';
import { EnteFile } from 'types/file';
import { getRawExif, getUNIXTime } from './upload/exifService'; import { getRawExif, getUNIXTime } from './upload/exifService';
import { getFileType } from './upload/readFileService'; import { getFileType } from './upload/readFileService';
import { FILE_TYPE } from 'constants/file';
export async function updateCreationTimeWithExif( export async function updateCreationTimeWithExif(
filesToBeUpdated: File[], filesToBeUpdated: EnteFile[],
fixOption: FIX_OPTIONS, fixOption: FIX_OPTIONS,
customTime: Date, customTime: Date,
setProgressTracker: SetProgressTracker setProgressTracker: SetProgressTracker
@ -35,8 +37,8 @@ export async function updateCreationTimeWithExif(
} else { } else {
const fileURL = await downloadManager.getFile(file); const fileURL = await downloadManager.getFile(file);
const fileObject = await getFileFromURL(fileURL); const fileObject = await getFileFromURL(fileURL);
const worker = await new CryptoWorker(); const reader = new FileReader();
const fileTypeInfo = await getFileType(worker, fileObject); const fileTypeInfo = await getFileType(reader, fileObject);
const exifData = await getRawExif(fileObject, fileTypeInfo); const exifData = await getRawExif(fileObject, fileTypeInfo);
if (fixOption === FIX_OPTIONS.DATE_TIME_ORIGINAL) { if (fixOption === FIX_OPTIONS.DATE_TIME_ORIGINAL) {
correctCreationTime = getUNIXTime( correctCreationTime = getUNIXTime(

View file

@ -1,4 +1,4 @@
import { DataStream, EncryptionResult, isDataStream } from './uploadService'; import { DataStream, EncryptionResult, isDataStream } from 'types/upload';
async function encryptFileStream(worker, fileData: DataStream) { async function encryptFileStream(worker, fileData: DataStream) {
const { stream, chunkCount } = fileData; const { stream, chunkCount } = fileData;

View file

@ -1,8 +1,9 @@
import { NULL_LOCATION } from 'constants/upload';
import { Location } from 'types/upload';
import exifr from 'exifr'; import exifr from 'exifr';
import piexif from 'piexifjs'; import piexif from 'piexifjs';
import { FileTypeInfo } from 'types/upload';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
import { NULL_LOCATION, Location } from './metadataService';
import { FileTypeInfo } from './readFileService';
const EXIF_TAGS_NEEDED = [ const EXIF_TAGS_NEEDED = [
'DateTimeOriginal', 'DateTimeOriginal',
@ -28,7 +29,7 @@ interface ParsedEXIFData {
} }
export async function getExifData( export async function getExifData(
receivedFile: globalThis.File, receivedFile: File,
fileTypeInfo: FileTypeInfo fileTypeInfo: FileTypeInfo
): Promise<ParsedEXIFData> { ): Promise<ParsedEXIFData> {
const nullExifData: ParsedEXIFData = { const nullExifData: ParsedEXIFData = {
@ -56,12 +57,13 @@ export async function getExifData(
} }
export async function updateFileCreationDateInEXIF( export async function updateFileCreationDateInEXIF(
reader: FileReader,
fileBlob: Blob, fileBlob: Blob,
updatedDate: Date updatedDate: Date
) { ) {
try { try {
const fileURL = URL.createObjectURL(fileBlob); const fileURL = URL.createObjectURL(fileBlob);
let imageDataURL = await convertImageToDataURL(fileURL); let imageDataURL = await convertImageToDataURL(reader, fileURL);
imageDataURL = imageDataURL =
'data:image/jpeg;base64' + 'data:image/jpeg;base64' +
imageDataURL.slice(imageDataURL.indexOf(',')); imageDataURL.slice(imageDataURL.indexOf(','));
@ -81,10 +83,9 @@ export async function updateFileCreationDateInEXIF(
} }
} }
export async function convertImageToDataURL(url: string) { export async function convertImageToDataURL(reader: FileReader, url: string) {
const blob = await fetch(url).then((r) => r.blob()); const blob = await fetch(url).then((r) => r.blob());
const dataUrl = await new Promise<string>((resolve) => { const dataUrl = await new Promise<string>((resolve) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result as string); reader.onload = () => resolve(reader.result as string);
reader.readAsDataURL(blob); reader.readAsDataURL(blob);
}); });

View file

@ -1,35 +1,27 @@
import { FILE_TYPE } from 'services/fileService'; import { FILE_TYPE } from 'constants/file';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
import { getExifData } from './exifService'; import { getExifData } from './exifService';
import { FileTypeInfo } from './readFileService'; import {
import { MetadataObject } from './uploadService'; Metadata,
ParsedMetadataJSON,
Location,
FileTypeInfo,
} from 'types/upload';
import { NULL_LOCATION } from 'constants/upload';
export interface Location { interface ParsedMetadataJSONWithTitle {
latitude: number;
longitude: number;
}
export interface ParsedMetaDataJSON {
creationTime: number;
modificationTime: number;
latitude: number;
longitude: number;
}
interface ParsedMetaDataJSONWithTitle {
title: string; title: string;
parsedMetaDataJSON: ParsedMetaDataJSON; parsedMetadataJSON: ParsedMetadataJSON;
} }
export const NULL_LOCATION: Location = { latitude: null, longitude: null }; const NULL_PARSED_METADATA_JSON: ParsedMetadataJSON = {
const NULL_PARSED_METADATA_JSON: ParsedMetaDataJSON = {
creationTime: null, creationTime: null,
modificationTime: null, modificationTime: null,
...NULL_LOCATION, ...NULL_LOCATION,
}; };
export async function extractMetadata( export async function extractMetadata(
receivedFile: globalThis.File, receivedFile: File,
fileTypeInfo: FileTypeInfo fileTypeInfo: FileTypeInfo
) { ) {
let exifData = null; let exifData = null;
@ -37,7 +29,7 @@ export async function extractMetadata(
exifData = await getExifData(receivedFile, fileTypeInfo); exifData = await getExifData(receivedFile, fileTypeInfo);
} }
const extractedMetadata: MetadataObject = { const extractedMetadata: Metadata = {
title: receivedFile.name, title: receivedFile.name,
creationTime: creationTime:
exifData?.creationTime ?? receivedFile.lastModified * 1000, exifData?.creationTime ?? receivedFile.lastModified * 1000,
@ -52,10 +44,12 @@ export async function extractMetadata(
export const getMetadataMapKey = (collectionID: number, title: string) => export const getMetadataMapKey = (collectionID: number, title: string) =>
`${collectionID}_${title}`; `${collectionID}_${title}`;
export async function parseMetadataJSON(receivedFile: globalThis.File) { export async function parseMetadataJSON(
reader: FileReader,
receivedFile: File
) {
try { try {
const metadataJSON: object = await new Promise((resolve, reject) => { const metadataJSON: object = await new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onabort = () => reject(Error('file reading was aborted')); reader.onabort = () => reject(Error('file reading was aborted'));
reader.onerror = () => reject(Error('file reading has failed')); reader.onerror = () => reject(Error('file reading has failed'));
reader.onload = () => { reader.onload = () => {
@ -68,7 +62,7 @@ export async function parseMetadataJSON(receivedFile: globalThis.File) {
reader.readAsText(receivedFile); reader.readAsText(receivedFile);
}); });
const parsedMetaDataJSON: ParsedMetaDataJSON = const parsedMetadataJSON: ParsedMetadataJSON =
NULL_PARSED_METADATA_JSON; NULL_PARSED_METADATA_JSON;
if (!metadataJSON || !metadataJSON['title']) { if (!metadataJSON || !metadataJSON['title']) {
return; return;
@ -79,20 +73,20 @@ export async function parseMetadataJSON(receivedFile: globalThis.File) {
metadataJSON['photoTakenTime'] && metadataJSON['photoTakenTime'] &&
metadataJSON['photoTakenTime']['timestamp'] metadataJSON['photoTakenTime']['timestamp']
) { ) {
parsedMetaDataJSON.creationTime = parsedMetadataJSON.creationTime =
metadataJSON['photoTakenTime']['timestamp'] * 1000000; metadataJSON['photoTakenTime']['timestamp'] * 1000000;
} else if ( } else if (
metadataJSON['creationTime'] && metadataJSON['creationTime'] &&
metadataJSON['creationTime']['timestamp'] metadataJSON['creationTime']['timestamp']
) { ) {
parsedMetaDataJSON.creationTime = parsedMetadataJSON.creationTime =
metadataJSON['creationTime']['timestamp'] * 1000000; metadataJSON['creationTime']['timestamp'] * 1000000;
} }
if ( if (
metadataJSON['modificationTime'] && metadataJSON['modificationTime'] &&
metadataJSON['modificationTime']['timestamp'] metadataJSON['modificationTime']['timestamp']
) { ) {
parsedMetaDataJSON.modificationTime = parsedMetadataJSON.modificationTime =
metadataJSON['modificationTime']['timestamp'] * 1000000; metadataJSON['modificationTime']['timestamp'] * 1000000;
} }
let locationData: Location = NULL_LOCATION; let locationData: Location = NULL_LOCATION;
@ -110,10 +104,10 @@ export async function parseMetadataJSON(receivedFile: globalThis.File) {
locationData = metadataJSON['geoDataExif']; locationData = metadataJSON['geoDataExif'];
} }
if (locationData !== null) { if (locationData !== null) {
parsedMetaDataJSON.latitude = locationData.latitude; parsedMetadataJSON.latitude = locationData.latitude;
parsedMetaDataJSON.longitude = locationData.longitude; parsedMetadataJSON.longitude = locationData.longitude;
} }
return { title, parsedMetaDataJSON } as ParsedMetaDataJSONWithTitle; return { title, parsedMetadataJSON } as ParsedMetadataJSONWithTitle;
} catch (e) { } catch (e) {
logError(e, 'parseMetadataJSON failed'); logError(e, 'parseMetadataJSON failed');
// ignore // ignore

View file

@ -1,23 +1,18 @@
import { import {
FILE_CHUNKS_COMBINED_FOR_A_UPLOAD_PART, FILE_CHUNKS_COMBINED_FOR_A_UPLOAD_PART,
DataStream, RANDOM_PERCENTAGE_PROGRESS_FOR_PUT,
} from './uploadService'; } from 'constants/upload';
import UIService from './uiService';
import UploadHttpClient from './uploadHttpClient'; import UploadHttpClient from './uploadHttpClient';
import * as convert from 'xml-js'; import * as convert from 'xml-js';
import UIService, { RANDOM_PERCENTAGE_PROGRESS_FOR_PUT } from './uiService'; import { CustomError } from 'utils/error';
import { CustomError } from 'utils/common/errorUtil'; import { DataStream, MultipartUploadURLs } from 'types/upload';
interface PartEtag { interface PartEtag {
PartNumber: number; PartNumber: number;
ETag: string; ETag: string;
} }
export interface MultipartUploadURLs {
objectKey: string;
partURLs: string[];
completeURL: string;
}
function calculatePartCount(chunkCount: number) { function calculatePartCount(chunkCount: number) {
const partCount = Math.ceil( const partCount = Math.ceil(
chunkCount / FILE_CHUNKS_COMBINED_FOR_A_UPLOAD_PART chunkCount / FILE_CHUNKS_COMBINED_FOR_A_UPLOAD_PART

View file

@ -1,38 +1,35 @@
import { import { FILE_TYPE } from 'constants/file';
FILE_TYPE,
FORMAT_MISSED_BY_FILE_TYPE_LIB,
} from 'services/fileService';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
import { FILE_READER_CHUNK_SIZE, MULTIPART_PART_SIZE } from './uploadService'; import {
FILE_READER_CHUNK_SIZE,
FORMAT_MISSED_BY_FILE_TYPE_LIB,
MULTIPART_PART_SIZE,
} from 'constants/upload';
import FileType from 'file-type/browser'; import FileType from 'file-type/browser';
import { CustomError } from 'utils/common/errorUtil'; import { CustomError } from 'utils/error';
import { getFileExtension } from 'utils/file'; import { getFileExtension, splitFilenameAndExtension } from 'utils/file';
import { FileTypeInfo } from 'types/upload';
const TYPE_VIDEO = 'video'; const TYPE_VIDEO = 'video';
const TYPE_IMAGE = 'image'; const TYPE_IMAGE = 'image';
const EDITED_FILE_SUFFIX = '-edited'; const EDITED_FILE_SUFFIX = '-edited';
const CHUNK_SIZE_FOR_TYPE_DETECTION = 4100; const CHUNK_SIZE_FOR_TYPE_DETECTION = 4100;
export async function getFileData(worker, file: globalThis.File) { export async function getFileData(reader: FileReader, file: File) {
if (file.size > MULTIPART_PART_SIZE) { if (file.size > MULTIPART_PART_SIZE) {
return getFileStream(worker, file, FILE_READER_CHUNK_SIZE); return getFileStream(reader, file, FILE_READER_CHUNK_SIZE);
} else { } else {
return await worker.getUint8ArrayView(file); return await getUint8ArrayView(reader, file);
} }
} }
export interface FileTypeInfo {
fileType: FILE_TYPE;
exactType: string;
}
export async function getFileType( export async function getFileType(
worker, reader: FileReader,
receivedFile: globalThis.File receivedFile: File
): Promise<FileTypeInfo> { ): Promise<FileTypeInfo> {
try { try {
let fileType: FILE_TYPE; let fileType: FILE_TYPE;
const mimeType = await getMimeType(worker, receivedFile); const mimeType = await getMimeType(reader, receivedFile);
const typeParts = mimeType?.split('/'); const typeParts = mimeType?.split('/');
if (typeParts?.length !== 2) { if (typeParts?.length !== 2) {
throw Error(CustomError.TYPE_DETECTION_FAILED); throw Error(CustomError.TYPE_DETECTION_FAILED);
@ -67,26 +64,35 @@ export async function getFileType(
Get the original file name for edited file to associate it to original file's metadataJSON file Get the original file name for edited file to associate it to original file's metadataJSON file
as edited file doesn't have their own metadata file as edited file doesn't have their own metadata file
*/ */
export function getFileOriginalName(file: globalThis.File) { export function getFileOriginalName(file: File) {
let originalName: string = null; let originalName: string = null;
const [nameWithoutExtension, extension] = splitFilenameAndExtension(
file.name
);
const isEditedFile = file.name.endsWith(EDITED_FILE_SUFFIX); const isEditedFile = nameWithoutExtension.endsWith(EDITED_FILE_SUFFIX);
if (isEditedFile) { if (isEditedFile) {
originalName = file.name.slice(0, -1 * EDITED_FILE_SUFFIX.length); originalName = nameWithoutExtension.slice(
0,
-1 * EDITED_FILE_SUFFIX.length
);
} else { } else {
originalName = file.name; originalName = nameWithoutExtension;
}
if (extension) {
originalName += '.' + extension;
} }
return originalName; return originalName;
} }
async function getMimeType(worker, file: globalThis.File) { async function getMimeType(reader: FileReader, file: File) {
const fileChunkBlob = file.slice(0, CHUNK_SIZE_FOR_TYPE_DETECTION); const fileChunkBlob = file.slice(0, CHUNK_SIZE_FOR_TYPE_DETECTION);
return getMimeTypeFromBlob(worker, fileChunkBlob); return getMimeTypeFromBlob(reader, fileChunkBlob);
} }
export async function getMimeTypeFromBlob(worker, fileBlob: Blob) { export async function getMimeTypeFromBlob(reader: FileReader, fileBlob: Blob) {
try { try {
const initialFiledata = await worker.getUint8ArrayView(fileBlob); const initialFiledata = await getUint8ArrayView(reader, fileBlob);
const result = await FileType.fromBuffer(initialFiledata); const result = await FileType.fromBuffer(initialFiledata);
return result.mime; return result.mime;
} catch (e) { } catch (e) {
@ -94,8 +100,8 @@ export async function getMimeTypeFromBlob(worker, fileBlob: Blob) {
} }
} }
function getFileStream(worker, file: globalThis.File, chunkSize: number) { function getFileStream(reader: FileReader, file: File, chunkSize: number) {
const fileChunkReader = fileChunkReaderMaker(worker, file, chunkSize); const fileChunkReader = fileChunkReaderMaker(reader, file, chunkSize);
const stream = new ReadableStream<Uint8Array>({ const stream = new ReadableStream<Uint8Array>({
async pull(controller: ReadableStreamDefaultController) { async pull(controller: ReadableStreamDefaultController) {
@ -115,14 +121,14 @@ function getFileStream(worker, file: globalThis.File, chunkSize: number) {
} }
async function* fileChunkReaderMaker( async function* fileChunkReaderMaker(
worker, reader: FileReader,
file: globalThis.File, file: File,
chunkSize: number chunkSize: number
) { ) {
let offset = 0; let offset = 0;
while (offset < file.size) { while (offset < file.size) {
const blob = file.slice(offset, chunkSize + offset); const blob = file.slice(offset, chunkSize + offset);
const fileChunk = await worker.getUint8ArrayView(blob); const fileChunk = await getUint8ArrayView(reader, blob);
yield fileChunk; yield fileChunk;
offset += chunkSize; offset += chunkSize;
} }

View file

@ -1,15 +1,16 @@
import { FILE_TYPE } from 'services/fileService'; import { FILE_TYPE } from 'constants/file';
import { CustomError, errorWithContext } from 'utils/common/errorUtil'; import { CustomError, errorWithContext } from 'utils/error';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
import { BLACK_THUMBNAIL_BASE64 } from '../../../public/images/black-thumbnail-b64'; import { BLACK_THUMBNAIL_BASE64 } from '../../../public/images/black-thumbnail-b64';
import FFmpegService from 'services/ffmpegService'; import FFmpegService from 'services/ffmpegService';
import { convertToHumanReadable } from 'utils/billingUtil'; import { convertToHumanReadable } from 'utils/billing';
import { isFileHEIC } from 'utils/file'; import { isFileHEIC } from 'utils/file';
import { FileTypeInfo } from './readFileService'; import { FileTypeInfo } from 'types/upload';
import { getUint8ArrayView } from './readFileService';
const MAX_THUMBNAIL_DIMENSION = 720; const MAX_THUMBNAIL_DIMENSION = 720;
const MIN_COMPRESSION_PERCENTAGE_SIZE_DIFF = 10; const MIN_COMPRESSION_PERCENTAGE_SIZE_DIFF = 10;
export const MAX_THUMBNAIL_SIZE = 100 * 1024; const MAX_THUMBNAIL_SIZE = 100 * 1024;
const MIN_QUALITY = 0.5; const MIN_QUALITY = 0.5;
const MAX_QUALITY = 0.7; const MAX_QUALITY = 0.7;
@ -22,7 +23,8 @@ interface Dimension {
export async function generateThumbnail( export async function generateThumbnail(
worker, worker,
file: globalThis.File, reader: FileReader,
file: File,
fileTypeInfo: FileTypeInfo fileTypeInfo: FileTypeInfo
): Promise<{ thumbnail: Uint8Array; hasStaticThumbnail: boolean }> { ): Promise<{ thumbnail: Uint8Array; hasStaticThumbnail: boolean }> {
try { try {
@ -50,7 +52,7 @@ export async function generateThumbnail(
} }
} }
const thumbnailBlob = await thumbnailCanvasToBlob(canvas); const thumbnailBlob = await thumbnailCanvasToBlob(canvas);
thumbnail = await worker.getUint8ArrayView(thumbnailBlob); thumbnail = await getUint8ArrayView(reader, thumbnailBlob);
if (thumbnail.length === 0) { if (thumbnail.length === 0) {
throw Error('EMPTY THUMBNAIL'); throw Error('EMPTY THUMBNAIL');
} }
@ -72,7 +74,7 @@ export async function generateThumbnail(
export async function generateImageThumbnail( export async function generateImageThumbnail(
worker, worker,
file: globalThis.File, file: File,
isHEIC: boolean isHEIC: boolean
) { ) {
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
@ -82,11 +84,7 @@ export async function generateImageThumbnail(
let timeout = null; let timeout = null;
if (isHEIC) { if (isHEIC) {
file = new globalThis.File( file = new File([await worker.convertHEIC2JPEG(file)], null, null);
[await worker.convertHEIC2JPEG(file)],
null,
null
);
} }
let image = new Image(); let image = new Image();
imageURL = URL.createObjectURL(file); imageURL = URL.createObjectURL(file);
@ -130,7 +128,7 @@ export async function generateImageThumbnail(
return canvas; return canvas;
} }
export async function generateVideoThumbnail(file: globalThis.File) { export async function generateVideoThumbnail(file: File) {
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
const canvasCTX = canvas.getContext('2d'); const canvasCTX = canvas.getContext('2d');

View file

@ -1,7 +1,8 @@
import { ProgressUpdater } from 'components/pages/gallery/Upload'; import {
import { UPLOAD_STAGES } from './uploadManager'; RANDOM_PERCENTAGE_PROGRESS_FOR_PUT,
UPLOAD_STAGES,
export const RANDOM_PERCENTAGE_PROGRESS_FOR_PUT = () => 90 + 10 * Math.random(); } from 'constants/upload';
import { ProgressUpdater } from 'types/upload';
class UIService { class UIService {
private perFileProgress: number; private perFileProgress: number;

View file

@ -2,11 +2,10 @@ import HTTPService from 'services/HTTPService';
import { getEndpoint } from 'utils/common/apiUtil'; import { getEndpoint } from 'utils/common/apiUtil';
import { getToken } from 'utils/common/key'; import { getToken } from 'utils/common/key';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
import { UploadFile, UploadURL } from './uploadService'; import { EnteFile } from 'types/file';
import { File } from '../fileService'; import { CustomError, handleUploadError } from 'utils/error';
import { CustomError, handleUploadError } from 'utils/common/errorUtil';
import { retryAsyncFunction } from 'utils/network'; import { retryAsyncFunction } from 'utils/network';
import { MultipartUploadURLs } from './multiPartUploadService'; import { UploadFile, UploadURL, MultipartUploadURLs } from 'types/upload';
const ENDPOINT = getEndpoint(); const ENDPOINT = getEndpoint();
const MAX_URL_REQUESTS = 50; const MAX_URL_REQUESTS = 50;
@ -14,7 +13,7 @@ const MAX_URL_REQUESTS = 50;
class UploadHttpClient { class UploadHttpClient {
private uploadURLFetchInProgress = null; private uploadURLFetchInProgress = null;
async uploadFile(uploadFile: UploadFile): Promise<File> { async uploadFile(uploadFile: UploadFile): Promise<EnteFile> {
try { try {
const token = getToken(); const token = getToken();
if (!token) { if (!token) {

View file

@ -1,6 +1,6 @@
import { File, getLocalFiles, setLocalFiles } from '../fileService'; import { getLocalFiles, setLocalFiles } from '../fileService';
import { Collection, getLocalCollections } from '../collectionService'; import { getLocalCollections } from '../collectionService';
import { SetFiles } from 'pages/gallery'; import { SetFiles } from 'types/gallery';
import { ComlinkWorker, getDedicatedCryptoWorker } from 'utils/crypto'; import { ComlinkWorker, getDedicatedCryptoWorker } from 'utils/crypto';
import { import {
sortFilesIntoCollections, sortFilesIntoCollections,
@ -8,52 +8,32 @@ import {
removeUnnecessaryFileProps, removeUnnecessaryFileProps,
} from 'utils/file'; } from 'utils/file';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
import { import { getMetadataMapKey, parseMetadataJSON } from './metadataService';
getMetadataMapKey,
ParsedMetaDataJSON,
parseMetadataJSON,
} from './metadataService';
import { segregateFiles } from 'utils/upload'; import { segregateFiles } from 'utils/upload';
import { ProgressUpdater } from 'components/pages/gallery/Upload';
import uploader from './uploader'; import uploader from './uploader';
import UIService from './uiService'; import UIService from './uiService';
import UploadService from './uploadService'; import UploadService from './uploadService';
import { CustomError } from 'utils/common/errorUtil'; import { CustomError } from 'utils/error';
import { Collection } from 'types/collection';
import { EnteFile } from 'types/file';
import {
FileWithCollection,
MetadataMap,
ParsedMetadataJSON,
ProgressUpdater,
} from 'types/upload';
import { UPLOAD_STAGES, FileUploadResults } from 'constants/upload';
const MAX_CONCURRENT_UPLOADS = 4; const MAX_CONCURRENT_UPLOADS = 4;
const FILE_UPLOAD_COMPLETED = 100; const FILE_UPLOAD_COMPLETED = 100;
export enum FileUploadResults {
FAILED = -1,
SKIPPED = -2,
UNSUPPORTED = -3,
BLOCKED = -4,
TOO_LARGE = -5,
UPLOADED = 100,
}
export interface FileWithCollection {
file: globalThis.File;
collectionID?: number;
collection?: Collection;
}
export enum UPLOAD_STAGES {
START,
READING_GOOGLE_METADATA_FILES,
UPLOADING,
FINISH,
}
export type MetadataMap = Map<string, ParsedMetaDataJSON>;
class UploadManager { class UploadManager {
private cryptoWorkers = new Array<ComlinkWorker>(MAX_CONCURRENT_UPLOADS); private cryptoWorkers = new Array<ComlinkWorker>(MAX_CONCURRENT_UPLOADS);
private metadataMap: MetadataMap; private metadataMap: MetadataMap;
private filesToBeUploaded: FileWithCollection[]; private filesToBeUploaded: FileWithCollection[];
private failedFiles: FileWithCollection[]; private failedFiles: FileWithCollection[];
private existingFilesCollectionWise: Map<number, File[]>; private existingFilesCollectionWise: Map<number, EnteFile[]>;
private existingFiles: File[]; private existingFiles: EnteFile[];
private setFiles: SetFiles; private setFiles: SetFiles;
private collections: Map<number, Collection>; private collections: Map<number, Collection>;
public initUploader(progressUpdater: ProgressUpdater, setFiles: SetFiles) { public initUploader(progressUpdater: ProgressUpdater, setFiles: SetFiles) {
@ -64,7 +44,7 @@ class UploadManager {
private async init(newCollections?: Collection[]) { private async init(newCollections?: Collection[]) {
this.filesToBeUploaded = []; this.filesToBeUploaded = [];
this.failedFiles = []; this.failedFiles = [];
this.metadataMap = new Map<string, ParsedMetaDataJSON>(); this.metadataMap = new Map<string, ParsedMetadataJSON>();
this.existingFiles = await getLocalFiles(); this.existingFiles = await getLocalFiles();
this.existingFilesCollectionWise = sortFilesIntoCollections( this.existingFilesCollectionWise = sortFilesIntoCollections(
this.existingFiles this.existingFiles
@ -112,20 +92,21 @@ class UploadManager {
private async seedMetadataMap(metadataFiles: FileWithCollection[]) { private async seedMetadataMap(metadataFiles: FileWithCollection[]) {
try { try {
UIService.reset(metadataFiles.length); UIService.reset(metadataFiles.length);
const reader = new FileReader();
for (const fileWithCollection of metadataFiles) { for (const fileWithCollection of metadataFiles) {
const parsedMetaDataJSONWithTitle = await parseMetadataJSON( const parsedMetadataJSONWithTitle = await parseMetadataJSON(
reader,
fileWithCollection.file fileWithCollection.file
); );
if (parsedMetaDataJSONWithTitle) { if (parsedMetadataJSONWithTitle) {
const { title, parsedMetaDataJSON } = const { title, parsedMetadataJSON } =
parsedMetaDataJSONWithTitle; parsedMetadataJSONWithTitle;
this.metadataMap.set( this.metadataMap.set(
getMetadataMapKey( getMetadataMapKey(
fileWithCollection.collectionID, fileWithCollection.collectionID,
title title
), ),
{ ...parsedMetaDataJSON } { ...parsedMetadataJSON }
); );
UIService.increaseFileUploaded(); UIService.increaseFileUploaded();
} }
@ -157,14 +138,15 @@ class UploadManager {
this.cryptoWorkers[i] = cryptoWorker; this.cryptoWorkers[i] = cryptoWorker;
uploadProcesses.push( uploadProcesses.push(
this.uploadNextFileInQueue( this.uploadNextFileInQueue(
await new this.cryptoWorkers[i].comlink() await new this.cryptoWorkers[i].comlink(),
new FileReader()
) )
); );
} }
await Promise.all(uploadProcesses); await Promise.all(uploadProcesses);
} }
private async uploadNextFileInQueue(worker: any) { private async uploadNextFileInQueue(worker: any, reader: FileReader) {
while (this.filesToBeUploaded.length > 0) { while (this.filesToBeUploaded.length > 0) {
const fileWithCollection = this.filesToBeUploaded.pop(); const fileWithCollection = this.filesToBeUploaded.pop();
const existingFilesInCollection = const existingFilesInCollection =
@ -177,6 +159,7 @@ class UploadManager {
fileWithCollection.collection = collection; fileWithCollection.collection = collection;
const { fileUploadResult, file } = await uploader( const { fileUploadResult, file } = await uploader(
worker, worker,
reader,
existingFilesInCollection, existingFilesInCollection,
fileWithCollection fileWithCollection
); );

View file

@ -1,99 +1,33 @@
import { fileAttribute, FILE_TYPE } from '../fileService'; import { Collection } from 'types/collection';
import { Collection } from '../collectionService';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
import UploadHttpClient from './uploadHttpClient'; import UploadHttpClient from './uploadHttpClient';
import { import { extractMetadata, getMetadataMapKey } from './metadataService';
extractMetadata,
getMetadataMapKey,
ParsedMetaDataJSON,
} from './metadataService';
import { generateThumbnail } from './thumbnailService'; import { generateThumbnail } from './thumbnailService';
import { import { getFileOriginalName, getFileData } from './readFileService';
getFileOriginalName,
getFileData,
FileTypeInfo,
} from './readFileService';
import { encryptFiledata } from './encryptionService'; import { encryptFiledata } from './encryptionService';
import { ENCRYPTION_CHUNK_SIZE } from 'types';
import { uploadStreamUsingMultipart } from './multiPartUploadService'; import { uploadStreamUsingMultipart } from './multiPartUploadService';
import UIService from './uiService'; import UIService from './uiService';
import { handleUploadError } from 'utils/common/errorUtil'; import { handleUploadError } from 'utils/error';
import { MetadataMap } from './uploadManager'; import {
B64EncryptionResult,
// this is the chunk size of the un-encrypted file which is read and encrypted before uploading it as a single part. BackupedFile,
export const MULTIPART_PART_SIZE = 20 * 1024 * 1024; EncryptedFile,
EncryptionResult,
export const FILE_READER_CHUNK_SIZE = ENCRYPTION_CHUNK_SIZE; FileInMemory,
FileTypeInfo,
export const FILE_CHUNKS_COMBINED_FOR_A_UPLOAD_PART = Math.floor( FileWithMetadata,
MULTIPART_PART_SIZE / FILE_READER_CHUNK_SIZE isDataStream,
); MetadataMap,
Metadata,
export interface UploadURL { ParsedMetadataJSON,
url: string; ProcessedFile,
objectKey: string; UploadFile,
} UploadURL,
} from 'types/upload';
export interface DataStream {
stream: ReadableStream<Uint8Array>;
chunkCount: number;
}
export function isDataStream(object: any): object is DataStream {
return 'stream' in object;
}
export interface EncryptionResult {
file: fileAttribute;
key: string;
}
export interface B64EncryptionResult {
encryptedData: string;
key: string;
nonce: string;
}
export interface MetadataObject {
title: string;
creationTime: number;
modificationTime: number;
latitude: number;
longitude: number;
fileType: FILE_TYPE;
hasStaticThumbnail?: boolean;
}
export interface FileInMemory {
filedata: Uint8Array | DataStream;
thumbnail: Uint8Array;
hasStaticThumbnail: boolean;
}
export interface FileWithMetadata
extends Omit<FileInMemory, 'hasStaticThumbnail'> {
metadata: MetadataObject;
}
export interface EncryptedFile {
file: ProcessedFile;
fileKey: B64EncryptionResult;
}
export interface ProcessedFile {
file: fileAttribute;
thumbnail: fileAttribute;
metadata: fileAttribute;
filename: string;
}
export interface BackupedFile extends Omit<ProcessedFile, 'filename'> {}
export interface UploadFile extends BackupedFile {
collectionID: number;
encryptedKey: string;
keyDecryptionNonce: string;
}
class UploadService { class UploadService {
private uploadURLs: UploadURL[] = []; private uploadURLs: UploadURL[] = [];
private metadataMap: Map<string, ParsedMetaDataJSON>; private metadataMap: Map<string, ParsedMetadataJSON>;
private pendingUploadCount: number = 0; private pendingUploadCount: number = 0;
async init(fileCount: number, metadataMap: MetadataMap) { async init(fileCount: number, metadataMap: MetadataMap) {
@ -104,16 +38,18 @@ class UploadService {
async readFile( async readFile(
worker: any, worker: any,
rawFile: globalThis.File, reader: FileReader,
rawFile: File,
fileTypeInfo: FileTypeInfo fileTypeInfo: FileTypeInfo
): Promise<FileInMemory> { ): Promise<FileInMemory> {
const { thumbnail, hasStaticThumbnail } = await generateThumbnail( const { thumbnail, hasStaticThumbnail } = await generateThumbnail(
worker, worker,
reader,
rawFile, rawFile,
fileTypeInfo fileTypeInfo
); );
const filedata = await getFileData(worker, rawFile); const filedata = await getFileData(reader, rawFile);
return { return {
filedata, filedata,
@ -126,13 +62,13 @@ class UploadService {
rawFile: File, rawFile: File,
collection: Collection, collection: Collection,
fileTypeInfo: FileTypeInfo fileTypeInfo: FileTypeInfo
): Promise<MetadataObject> { ): Promise<Metadata> {
const originalName = getFileOriginalName(rawFile); const originalName = getFileOriginalName(rawFile);
const googleMetadata = const googleMetadata =
this.metadataMap.get( this.metadataMap.get(
getMetadataMapKey(collection.id, originalName) getMetadataMapKey(collection.id, originalName)
) ?? {}; ) ?? {};
const extractedMetadata: MetadataObject = await extractMetadata( const extractedMetadata: Metadata = await extractMetadata(
rawFile, rawFile,
fileTypeInfo fileTypeInfo
); );

View file

@ -1,32 +1,37 @@
import { File, FILE_TYPE } from 'services/fileService'; import { EnteFile } from 'types/file';
import { sleep } from 'utils/common'; import { sleep } from 'utils/common';
import { handleUploadError, CustomError } from 'utils/common/errorUtil'; import { handleUploadError, CustomError } from 'utils/error';
import { decryptFile } from 'utils/file'; import { decryptFile } from 'utils/file';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
import { fileAlreadyInCollection } from 'utils/upload'; import { fileAlreadyInCollection } from 'utils/upload';
import UploadHttpClient from './uploadHttpClient'; import UploadHttpClient from './uploadHttpClient';
import UIService from './uiService'; import UIService from './uiService';
import { FileUploadResults, FileWithCollection } from './uploadManager'; import UploadService from './uploadService';
import UploadService, { import uploadService from './uploadService';
import { getFileType } from './readFileService';
import {
BackupedFile, BackupedFile,
EncryptedFile, EncryptedFile,
FileInMemory, FileInMemory,
FileTypeInfo,
FileWithCollection,
FileWithMetadata, FileWithMetadata,
MetadataObject, Metadata,
UploadFile, UploadFile,
} from './uploadService'; } from 'types/upload';
import uploadService from './uploadService'; import { FILE_TYPE } from 'constants/file';
import { FileTypeInfo, getFileType } from './readFileService'; import { FileUploadResults } from 'constants/upload';
const TwoSecondInMillSeconds = 2000; const TwoSecondInMillSeconds = 2000;
const FIVE_GB_IN_BYTES = 5 * 1024 * 1024 * 1024; const FIVE_GB_IN_BYTES = 5 * 1024 * 1024 * 1024;
interface UploadResponse { interface UploadResponse {
fileUploadResult: FileUploadResults; fileUploadResult: FileUploadResults;
file?: File; file?: EnteFile;
} }
export default async function uploader( export default async function uploader(
worker: any, worker: any,
existingFilesInCollection: File[], reader: FileReader,
existingFilesInCollection: EnteFile[],
fileWithCollection: FileWithCollection fileWithCollection: FileWithCollection
): Promise<UploadResponse> { ): Promise<UploadResponse> {
const { file: rawFile, collection } = fileWithCollection; const { file: rawFile, collection } = fileWithCollection;
@ -35,7 +40,7 @@ export default async function uploader(
let file: FileInMemory = null; let file: FileInMemory = null;
let encryptedFile: EncryptedFile = null; let encryptedFile: EncryptedFile = null;
let metadata: MetadataObject = null; let metadata: Metadata = null;
let fileTypeInfo: FileTypeInfo = null; let fileTypeInfo: FileTypeInfo = null;
let fileWithMetadata: FileWithMetadata = null; let fileWithMetadata: FileWithMetadata = null;
@ -49,7 +54,7 @@ export default async function uploader(
await sleep(TwoSecondInMillSeconds); await sleep(TwoSecondInMillSeconds);
return { fileUploadResult: FileUploadResults.TOO_LARGE }; return { fileUploadResult: FileUploadResults.TOO_LARGE };
} }
fileTypeInfo = await getFileType(worker, rawFile); fileTypeInfo = await getFileType(reader, rawFile);
if (fileTypeInfo.fileType === FILE_TYPE.OTHERS) { if (fileTypeInfo.fileType === FILE_TYPE.OTHERS) {
throw Error(CustomError.UNSUPPORTED_FILE_FORMAT); throw Error(CustomError.UNSUPPORTED_FILE_FORMAT);
} }
@ -66,7 +71,12 @@ export default async function uploader(
return { fileUploadResult: FileUploadResults.SKIPPED }; return { fileUploadResult: FileUploadResults.SKIPPED };
} }
file = await UploadService.readFile(worker, rawFile, fileTypeInfo); file = await UploadService.readFile(
worker,
reader,
rawFile,
fileTypeInfo
);
if (file.hasStaticThumbnail) { if (file.hasStaticThumbnail) {
metadata.hasStaticThumbnail = true; metadata.hasStaticThumbnail = true;
} }

View file

@ -1,4 +1,4 @@
import { KeyAttributes, PAGES } from 'types'; import { PAGES } from 'constants/pages';
import { getEndpoint } from 'utils/common/apiUtil'; import { getEndpoint } from 'utils/common/apiUtil';
import { clearKeys } from 'utils/storage/sessionStorage'; import { clearKeys } from 'utils/storage/sessionStorage';
import router from 'next/router'; import router from 'next/router';
@ -8,70 +8,20 @@ import { getToken } from 'utils/common/key';
import HTTPService from './HTTPService'; import HTTPService from './HTTPService';
import { B64EncryptionResult } from 'utils/crypto'; import { B64EncryptionResult } from 'utils/crypto';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
import { Subscription } from './billingService'; import {
KeyAttributes,
UpdatedKey,
RecoveryKey,
TwoFactorSecret,
TwoFactorVerificationResponse,
TwoFactorRecoveryResponse,
UserDetails,
} from 'types/user';
export interface UpdatedKey {
kekSalt: string;
encryptedKey: string;
keyDecryptionNonce: string;
memLimit: number;
opsLimit: number;
}
export interface RecoveryKey {
masterKeyEncryptedWithRecoveryKey: string;
masterKeyDecryptionNonce: string;
recoveryKeyEncryptedWithMasterKey: string;
recoveryKeyDecryptionNonce: string;
}
const ENDPOINT = getEndpoint(); const ENDPOINT = getEndpoint();
const HAS_SET_KEYS = 'hasSetKeys'; const HAS_SET_KEYS = 'hasSetKeys';
export const FIX_CREATION_TIME_VISIBLE_TO_USER_IDS = [1, 125, 243, 341];
export interface User {
id: number;
name: string;
email: string;
token: string;
encryptedToken: string;
isTwoFactorEnabled: boolean;
twoFactorSessionID: string;
}
export interface EmailVerificationResponse {
id: number;
keyAttributes?: KeyAttributes;
encryptedToken?: string;
token?: string;
twoFactorSessionID: string;
}
export interface TwoFactorVerificationResponse {
id: number;
keyAttributes: KeyAttributes;
encryptedToken?: string;
token?: string;
}
export interface TwoFactorSecret {
secretCode: string;
qrCode: string;
}
export interface TwoFactorRecoveryResponse {
encryptedSecret: string;
secretDecryptionNonce: string;
}
export interface UserDetails {
email: string;
usage: number;
fileCount: number;
sharedCollectionCount: number;
subscription: Subscription;
}
export const getOtt = (email: string) => export const getOtt = (email: string) =>
HTTPService.get(`${ENDPOINT}/users/ott`, { HTTPService.get(`${ENDPOINT}/users/ott`, {
email, email,

View file

@ -1,39 +0,0 @@
export interface KeyAttributes {
kekSalt: string;
encryptedKey: string;
keyDecryptionNonce: string;
opsLimit: number;
memLimit: number;
publicKey: string;
encryptedSecretKey: string;
secretKeyDecryptionNonce: string;
masterKeyEncryptedWithRecoveryKey: string;
masterKeyDecryptionNonce: string;
recoveryKeyEncryptedWithMasterKey: string;
recoveryKeyDecryptionNonce: string;
}
export const ENCRYPTION_CHUNK_SIZE = 4 * 1024 * 1024;
export const GAP_BTW_TILES = 4;
export const DATE_CONTAINER_HEIGHT = 48;
export const IMAGE_CONTAINER_MAX_HEIGHT = 200;
export const IMAGE_CONTAINER_MAX_WIDTH =
IMAGE_CONTAINER_MAX_HEIGHT - GAP_BTW_TILES;
export const MIN_COLUMNS = 4;
export const SPACE_BTW_DATES = 44;
export enum PAGES {
CHANGE_EMAIL = '/change-email',
CHANGE_PASSWORD = '/change-password',
CREDENTIALS = '/credentials',
GALLERY = '/gallery',
GENERATE = '/generate',
LOGIN = '/login',
RECOVER = '/recover',
SIGNUP = '/signup',
TWO_FACTOR_SETUP = '/two-factor/setup',
TWO_FACTOR_VERIFY = '/two-factor/verify',
TWO_FACTOR_RECOVER = '/two-factor/recover',
VERIFY = '/verify',
ROOT = '/',
}

View file

@ -0,0 +1,23 @@
export interface Subscription {
id: number;
userID: number;
productID: string;
storage: number;
originalTransactionID: string;
expiryTime: number;
paymentProvider: string;
attributes: {
isCancelled: boolean;
};
price: string;
period: string;
}
export interface Plan {
id: string;
androidID: string;
iosID: string;
storage: number;
price: string;
period: string;
stripeID: string;
}

View file

@ -0,0 +1,52 @@
import { User } from 'types/user';
import { EnteFile } from 'types/file';
import { CollectionType } from 'constants/collection';
export interface Collection {
id: number;
owner: User;
key?: string;
name?: string;
encryptedName?: string;
nameDecryptionNonce?: string;
type: CollectionType;
attributes: collectionAttributes;
sharees: User[];
updationTime: number;
encryptedKey: string;
keyDecryptionNonce: string;
isDeleted: boolean;
isSharedCollection?: boolean;
}
export interface EncryptedFileKey {
id: number;
encryptedKey: string;
keyDecryptionNonce: string;
}
export interface AddToCollectionRequest {
collectionID: number;
files: EncryptedFileKey[];
}
export interface MoveToCollectionRequest {
fromCollectionID: number;
toCollectionID: number;
files: EncryptedFileKey[];
}
export interface collectionAttributes {
encryptedPath?: string;
pathDecryptionNonce?: string;
}
export interface CollectionAndItsLatestFile {
collection: Collection;
file: EnteFile;
}
export interface RemoveFromCollectionRequest {
collectionID: number;
fileIDs: number[];
}

25
src/types/export/index.ts Normal file
View file

@ -0,0 +1,25 @@
import { ExportStage } from 'constants/export';
export type CollectionIDPathMap = Map<number, string>;
export interface ExportProgress {
current: number;
total: number;
}
export interface ExportedCollectionPaths {
[collectionID: number]: string;
}
export interface ExportStats {
failed: number;
success: number;
}
export interface ExportRecord {
version?: number;
stage?: ExportStage;
lastAttemptTimestamp?: number;
progress?: ExportProgress;
queuedFiles?: string[];
exportedFiles?: string[];
failedFiles?: string[];
exportedCollectionPaths?: ExportedCollectionPaths;
}

86
src/types/file/index.ts Normal file
View file

@ -0,0 +1,86 @@
import { VISIBILITY_STATE } from 'constants/file';
import { DataStream, Metadata } from 'types/upload';
export interface fileAttribute {
encryptedData?: DataStream | Uint8Array;
objectKey?: string;
decryptionHeader: string;
}
export interface MagicMetadataCore {
version: number;
count: number;
header: string;
data: Record<string, any>;
}
export interface EncryptedMagicMetadataCore
extends Omit<MagicMetadataCore, 'data'> {
data: string;
}
export interface MagicMetadataProps {
visibility?: VISIBILITY_STATE;
}
export interface MagicMetadata extends Omit<MagicMetadataCore, 'data'> {
data: MagicMetadataProps;
}
export interface PublicMagicMetadataProps {
editedTime?: number;
editedName?: string;
}
export interface PublicMagicMetadata extends Omit<MagicMetadataCore, 'data'> {
data: PublicMagicMetadataProps;
}
export interface EnteFile {
id: number;
collectionID: number;
ownerID: number;
file: fileAttribute;
thumbnail: fileAttribute;
metadata: Metadata;
magicMetadata: MagicMetadata;
pubMagicMetadata: PublicMagicMetadata;
encryptedKey: string;
keyDecryptionNonce: string;
key: string;
src: string;
msrc: string;
html: string;
w: number;
h: number;
isDeleted: boolean;
isTrashed?: boolean;
deleteBy?: number;
dataIndex: number;
updationTime: number;
}
export interface UpdateMagicMetadataRequest {
metadataList: UpdateMagicMetadata[];
}
export interface UpdateMagicMetadata {
id: number;
magicMetadata: EncryptedMagicMetadataCore;
}
export const NEW_MAGIC_METADATA: MagicMetadataCore = {
version: 0,
data: {},
header: null,
count: 0,
};
export interface TrashRequest {
items: TrashRequestItems[];
}
export interface TrashRequestItems {
fileID: number;
collectionID: number;
}

View file

@ -0,0 +1,31 @@
import { Collection } from 'types/collection';
import { EnteFile } from 'types/file';
import { DateValue, Bbox } from 'types/search';
export type SelectedState = {
[k: number]: boolean;
count: number;
collectionID: number;
};
export type SetFiles = React.Dispatch<React.SetStateAction<EnteFile[]>>;
export type SetCollections = React.Dispatch<React.SetStateAction<Collection[]>>;
export type SetLoading = React.Dispatch<React.SetStateAction<Boolean>>;
export type setSearchStats = React.Dispatch<React.SetStateAction<SearchStats>>;
export type Search = {
date?: DateValue;
location?: Bbox;
fileIndex?: number;
};
export interface SearchStats {
resultCount: number;
timeTaken: number;
}
export type GalleryContextType = {
thumbs: Map<number, string>;
files: Map<number, string>;
showPlanSelectorModal: () => void;
setActiveCollection: (collection: number) => void;
syncWithRemote: (force?: boolean, silent?: boolean) => Promise<void>;
};

26
src/types/search/index.ts Normal file
View file

@ -0,0 +1,26 @@
export type Bbox = [number, number, number, number];
export interface LocationSearchResponse {
place: string;
bbox: Bbox;
}
export enum SuggestionType {
DATE,
LOCATION,
COLLECTION,
IMAGE,
VIDEO,
}
export interface DateValue {
date?: number;
month?: number;
year?: number;
}
export interface Suggestion {
type: SuggestionType;
label: string;
value: Bbox | DateValue | number;
}

11
src/types/trash/index.ts Normal file
View file

@ -0,0 +1,11 @@
import { EnteFile } from 'types/file';
export interface TrashItem {
file: EnteFile;
isDeleted: boolean;
isRestored: boolean;
deleteBy: number;
createdAt: number;
updatedAt: number;
}
export type Trash = TrashItem[];

112
src/types/upload/index.ts Normal file
View file

@ -0,0 +1,112 @@
import { FILE_TYPE } from 'constants/file';
import { UPLOAD_STAGES } from 'constants/upload';
import { Collection } from 'types/collection';
import { fileAttribute } from 'types/file';
export interface DataStream {
stream: ReadableStream<Uint8Array>;
chunkCount: number;
}
export function isDataStream(object: any): object is DataStream {
return 'stream' in object;
}
export interface EncryptionResult {
file: fileAttribute;
key: string;
}
export interface Metadata {
title: string;
creationTime: number;
modificationTime: number;
latitude: number;
longitude: number;
fileType: FILE_TYPE;
hasStaticThumbnail?: boolean;
}
export interface Location {
latitude: number;
longitude: number;
}
export interface ParsedMetadataJSON {
creationTime: number;
modificationTime: number;
latitude: number;
longitude: number;
}
export interface MultipartUploadURLs {
objectKey: string;
partURLs: string[];
completeURL: string;
}
export interface FileTypeInfo {
fileType: FILE_TYPE;
exactType: string;
}
export interface ProgressUpdater {
setPercentComplete: React.Dispatch<React.SetStateAction<number>>;
setFileCounter: React.Dispatch<
React.SetStateAction<{
finished: number;
total: number;
}>
>;
setUploadStage: React.Dispatch<React.SetStateAction<UPLOAD_STAGES>>;
setFileProgress: React.Dispatch<React.SetStateAction<Map<string, number>>>;
setUploadResult: React.Dispatch<React.SetStateAction<Map<string, number>>>;
}
export interface FileWithCollection {
file: File;
collectionID?: number;
collection?: Collection;
}
export type MetadataMap = Map<string, ParsedMetadataJSON>;
export interface UploadURL {
url: string;
objectKey: string;
}
export interface B64EncryptionResult {
encryptedData: string;
key: string;
nonce: string;
}
export interface FileInMemory {
filedata: Uint8Array | DataStream;
thumbnail: Uint8Array;
hasStaticThumbnail: boolean;
}
export interface FileWithMetadata
extends Omit<FileInMemory, 'hasStaticThumbnail'> {
metadata: Metadata;
}
export interface EncryptedFile {
file: ProcessedFile;
fileKey: B64EncryptionResult;
}
export interface ProcessedFile {
file: fileAttribute;
thumbnail: fileAttribute;
metadata: fileAttribute;
filename: string;
}
export interface BackupedFile extends Omit<ProcessedFile, 'filename'> {}
export interface UploadFile extends BackupedFile {
collectionID: number;
encryptedKey: string;
keyDecryptionNonce: string;
}

76
src/types/user/index.ts Normal file
View file

@ -0,0 +1,76 @@
import { Subscription } from 'types/billing';
export interface KeyAttributes {
kekSalt: string;
encryptedKey: string;
keyDecryptionNonce: string;
opsLimit: number;
memLimit: number;
publicKey: string;
encryptedSecretKey: string;
secretKeyDecryptionNonce: string;
masterKeyEncryptedWithRecoveryKey: string;
masterKeyDecryptionNonce: string;
recoveryKeyEncryptedWithMasterKey: string;
recoveryKeyDecryptionNonce: string;
}
export interface KEK {
key: string;
opsLimit: number;
memLimit: number;
}
export interface UpdatedKey {
kekSalt: string;
encryptedKey: string;
keyDecryptionNonce: string;
memLimit: number;
opsLimit: number;
}
export interface RecoveryKey {
masterKeyEncryptedWithRecoveryKey: string;
masterKeyDecryptionNonce: string;
recoveryKeyEncryptedWithMasterKey: string;
recoveryKeyDecryptionNonce: string;
}
export interface User {
id: number;
name: string;
email: string;
token: string;
encryptedToken: string;
isTwoFactorEnabled: boolean;
twoFactorSessionID: string;
}
export interface EmailVerificationResponse {
id: number;
keyAttributes?: KeyAttributes;
encryptedToken?: string;
token?: string;
twoFactorSessionID: string;
}
export interface TwoFactorVerificationResponse {
id: number;
keyAttributes: KeyAttributes;
encryptedToken?: string;
token?: string;
}
export interface TwoFactorSecret {
secretCode: string;
qrCode: string;
}
export interface TwoFactorRecoveryResponse {
encryptedSecret: string;
secretDecryptionNonce: string;
}
export interface UserDetails {
email: string;
usage: number;
fileCount: number;
sharedCollectionCount: number;
subscription: Subscription;
}

View file

@ -1,17 +1,15 @@
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
import billingService, { import billingService from 'services/billingService';
FREE_PLAN, import { Plan, Subscription } from 'types/billing';
Plan,
Subscription,
} from 'services/billingService';
import { NextRouter } from 'next/router'; import { NextRouter } from 'next/router';
import { SetDialogMessage } from 'components/MessageDialog'; import { SetDialogMessage } from 'components/MessageDialog';
import { SetLoading } from 'pages/gallery'; import { SetLoading } from 'types/gallery';
import { getData, LS_KEYS } from './storage/localStorage'; import { getData, LS_KEYS } from '../storage/localStorage';
import { CustomError } from './common/errorUtil'; import { CustomError } from '../error';
import { logError } from './sentry'; import { logError } from '../sentry';
const STRIPE = 'stripe'; const PAYMENT_PROVIDER_STRIPE = 'stripe';
const FREE_PLAN = 'free';
enum FAILURE_REASON { enum FAILURE_REASON {
AUTHENTICATION_FAILED = 'authentication_failed', AUTHENTICATION_FAILED = 'authentication_failed',
@ -94,7 +92,7 @@ export function hasStripeSubscription(subscription: Subscription) {
return ( return (
hasPaidSubscription(subscription) && hasPaidSubscription(subscription) &&
subscription.paymentProvider.length > 0 && subscription.paymentProvider.length > 0 &&
subscription.paymentProvider === STRIPE subscription.paymentProvider === PAYMENT_PROVIDER_STRIPE
); );
} }

View file

@ -1,20 +1,21 @@
import { import {
addToCollection, addToCollection,
Collection,
CollectionType,
moveToCollection, moveToCollection,
removeFromCollection, removeFromCollection,
restoreToCollection, restoreToCollection,
} from 'services/collectionService'; } from 'services/collectionService';
import { downloadFiles, getSelectedFiles } from 'utils/file'; import { downloadFiles, getSelectedFiles } from 'utils/file';
import { File, getLocalFiles } from 'services/fileService'; import { getLocalFiles } from 'services/fileService';
import { CustomError } from 'utils/common/errorUtil'; import { EnteFile } from 'types/file';
import { SelectedState } from 'pages/gallery'; import { CustomError } from 'utils/error';
import { User } from 'services/userService'; import { SelectedState } from 'types/gallery';
import { User } from 'types/user';
import { getData, LS_KEYS } from 'utils/storage/localStorage'; import { getData, LS_KEYS } from 'utils/storage/localStorage';
import { SetDialogMessage } from 'components/MessageDialog'; import { SetDialogMessage } from 'components/MessageDialog';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
import { Collection } from 'types/collection';
import { CollectionType } from 'constants/collection';
export enum COLLECTION_OPS_TYPE { export enum COLLECTION_OPS_TYPE {
ADD, ADD,
@ -26,7 +27,7 @@ export async function handleCollectionOps(
type: COLLECTION_OPS_TYPE, type: COLLECTION_OPS_TYPE,
setCollectionSelectorView: (value: boolean) => void, setCollectionSelectorView: (value: boolean) => void,
selected: SelectedState, selected: SelectedState,
files: File[], files: EnteFile[],
setActiveCollection: (id: number) => void, setActiveCollection: (id: number) => void,
collection: Collection collection: Collection
) { ) {

View file

@ -2,7 +2,7 @@ import { B64EncryptionResult } from 'utils/crypto';
import CryptoWorker from 'utils/crypto'; import CryptoWorker from 'utils/crypto';
import { getData, LS_KEYS } from 'utils/storage/localStorage'; import { getData, LS_KEYS } from 'utils/storage/localStorage';
import { getKey, SESSION_KEYS } from 'utils/storage/sessionStorage'; import { getKey, SESSION_KEYS } from 'utils/storage/sessionStorage';
import { CustomError } from './errorUtil'; import { CustomError } from '../error';
export const getActualKey = async () => { export const getActualKey = async () => {
try { try {

View file

@ -1,5 +1,4 @@
import { KEK } from 'pages/generate'; import { KEK, KeyAttributes } from 'types/user';
import { KeyAttributes } from 'types';
import * as Comlink from 'comlink'; import * as Comlink from 'comlink';
import { runningInBrowser } from 'utils/common'; import { runningInBrowser } from 'utils/common';
import { SESSION_KEYS, setKey } from 'utils/storage/sessionStorage'; import { SESSION_KEYS, setKey } from 'utils/storage/sessionStorage';

View file

@ -1,5 +1,5 @@
import sodium, { StateAddress } from 'libsodium-wrappers'; import sodium, { StateAddress } from 'libsodium-wrappers';
import { ENCRYPTION_CHUNK_SIZE } from 'types'; import { ENCRYPTION_CHUNK_SIZE } from 'constants/crypto';
export async function decryptChaChaOneShot( export async function decryptChaChaOneShot(
data: Uint8Array, data: Uint8Array,

View file

@ -1,18 +1,18 @@
import { Collection } from 'services/collectionService'; import { Collection } from 'types/collection';
import exportService, { import exportService from 'services/exportService';
CollectionIDPathMap, import { CollectionIDPathMap, ExportRecord } from 'types/export';
ExportRecord,
METADATA_FOLDER_NAME,
} from 'services/exportService';
import { File } from 'services/fileService';
import { MetadataObject } from 'services/upload/uploadService';
import { formatDate, splitFilenameAndExtension } from 'utils/file';
export const getExportRecordFileUID = (file: File) => import { EnteFile } from 'types/file';
import { Metadata } from 'types/upload';
import { formatDate, splitFilenameAndExtension } from 'utils/file';
import { METADATA_FOLDER_NAME } from 'constants/export';
export const getExportRecordFileUID = (file: EnteFile) =>
`${file.id}_${file.collectionID}_${file.updationTime}`; `${file.id}_${file.collectionID}_${file.updationTime}`;
export const getExportQueuedFiles = ( export const getExportQueuedFiles = (
allFiles: File[], allFiles: EnteFile[],
exportRecord: ExportRecord exportRecord: ExportRecord
) => { ) => {
const queuedFiles = new Set(exportRecord?.queuedFiles); const queuedFiles = new Set(exportRecord?.queuedFiles);
@ -78,7 +78,7 @@ export const getCollectionsRenamedAfterLastExport = (
}; };
export const getFilesUploadedAfterLastExport = ( export const getFilesUploadedAfterLastExport = (
allFiles: File[], allFiles: EnteFile[],
exportRecord: ExportRecord exportRecord: ExportRecord
) => { ) => {
const exportedFiles = new Set(exportRecord?.exportedFiles); const exportedFiles = new Set(exportRecord?.exportedFiles);
@ -92,7 +92,7 @@ export const getFilesUploadedAfterLastExport = (
}; };
export const getExportedFiles = ( export const getExportedFiles = (
allFiles: File[], allFiles: EnteFile[],
exportRecord: ExportRecord exportRecord: ExportRecord
) => { ) => {
const exportedFileIds = new Set(exportRecord?.exportedFiles); const exportedFileIds = new Set(exportRecord?.exportedFiles);
@ -106,7 +106,7 @@ export const getExportedFiles = (
}; };
export const getExportFailedFiles = ( export const getExportFailedFiles = (
allFiles: File[], allFiles: EnteFile[],
exportRecord: ExportRecord exportRecord: ExportRecord
) => { ) => {
const failedFiles = new Set(exportRecord?.failedFiles); const failedFiles = new Set(exportRecord?.failedFiles);
@ -127,7 +127,7 @@ export const dedupe = (files: any[]) => {
export const getGoogleLikeMetadataFile = ( export const getGoogleLikeMetadataFile = (
fileSaveName: string, fileSaveName: string,
metadata: MetadataObject metadata: Metadata
) => { ) => {
const creationTime = Math.floor(metadata.creationTime / 1000000); const creationTime = Math.floor(metadata.creationTime / 1000000);
const modificationTime = Math.floor( const modificationTime = Math.floor(
@ -223,14 +223,17 @@ export const getOldCollectionFolderPath = (
collection: Collection collection: Collection
) => `${dir}/${collection.id}_${oldSanitizeName(collection.name)}`; ) => `${dir}/${collection.id}_${oldSanitizeName(collection.name)}`;
export const getOldFileSavePath = (collectionFolderPath: string, file: File) => export const getOldFileSavePath = (
collectionFolderPath: string,
file: EnteFile
) =>
`${collectionFolderPath}/${file.id}_${oldSanitizeName( `${collectionFolderPath}/${file.id}_${oldSanitizeName(
file.metadata.title file.metadata.title
)}`; )}`;
export const getOldFileMetadataSavePath = ( export const getOldFileMetadataSavePath = (
collectionFolderPath: string, collectionFolderPath: string,
file: File file: EnteFile
) => ) =>
`${collectionFolderPath}/${METADATA_FOLDER_NAME}/${ `${collectionFolderPath}/${METADATA_FOLDER_NAME}/${
file.id file.id

View file

@ -1,28 +1,28 @@
import { SelectedState } from 'pages/gallery'; import { SelectedState } from 'types/gallery';
import { Collection } from 'services/collectionService'; import { Collection } from 'types/collection';
import { import {
File, EnteFile,
fileAttribute, fileAttribute,
FILE_TYPE,
MagicMetadataProps, MagicMetadataProps,
NEW_MAGIC_METADATA, NEW_MAGIC_METADATA,
PublicMagicMetadataProps, PublicMagicMetadataProps,
VISIBILITY_STATE, } from 'types/file';
} from 'services/fileService';
import { decodeMotionPhoto } from 'services/motionPhotoService'; import { decodeMotionPhoto } from 'services/motionPhotoService';
import { getMimeTypeFromBlob } from 'services/upload/readFileService'; import { getMimeTypeFromBlob } from 'services/upload/readFileService';
import DownloadManager from 'services/downloadManager'; import DownloadManager from 'services/downloadManager';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
import { User } from 'services/userService'; import { User } from 'types/user';
import CryptoWorker from 'utils/crypto'; import CryptoWorker from 'utils/crypto';
import { getData, LS_KEYS } from 'utils/storage/localStorage'; import { getData, LS_KEYS } from 'utils/storage/localStorage';
import { updateFileCreationDateInEXIF } from 'services/upload/exifService'; import { updateFileCreationDateInEXIF } from 'services/upload/exifService';
import {
export const TYPE_HEIC = 'heic'; TYPE_JPEG,
export const TYPE_HEIF = 'heif'; TYPE_JPG,
export const TYPE_JPEG = 'jpeg'; TYPE_HEIC,
export const TYPE_JPG = 'jpg'; TYPE_HEIF,
const UNSUPPORTED_FORMATS = ['flv', 'mkv', '3gp', 'avi', 'wmv']; FILE_TYPE,
VISIBILITY_STATE,
} from 'constants/file';
export function downloadAsFile(filename: string, content: string) { export function downloadAsFile(filename: string, content: string) {
const file = new Blob([content], { const file = new Blob([content], {
@ -40,7 +40,7 @@ export function downloadAsFile(filename: string, content: string) {
a.remove(); a.remove();
} }
export async function downloadFile(file: File) { export async function downloadFile(file: EnteFile) {
const a = document.createElement('a'); const a = document.createElement('a');
a.style.display = 'none'; a.style.display = 'none';
let fileURL = await DownloadManager.getCachedOriginalFile(file); let fileURL = await DownloadManager.getCachedOriginalFile(file);
@ -60,6 +60,7 @@ export async function downloadFile(file: File) {
let fileBlob = await (await fetch(fileURL)).blob(); let fileBlob = await (await fetch(fileURL)).blob();
fileBlob = await updateFileCreationDateInEXIF( fileBlob = await updateFileCreationDateInEXIF(
new FileReader(),
fileBlob, fileBlob,
new Date(file.pubMagicMetadata.data.editedTime / 1000) new Date(file.pubMagicMetadata.data.editedTime / 1000)
); );
@ -89,8 +90,8 @@ export function isFileHEIC(mimeType: string) {
); );
} }
export function sortFilesIntoCollections(files: File[]) { export function sortFilesIntoCollections(files: EnteFile[]) {
const collectionWiseFiles = new Map<number, File[]>(); const collectionWiseFiles = new Map<number, EnteFile[]>();
for (const file of files) { for (const file of files) {
if (!collectionWiseFiles.has(file.collectionID)) { if (!collectionWiseFiles.has(file.collectionID)) {
collectionWiseFiles.set(file.collectionID, []); collectionWiseFiles.set(file.collectionID, []);
@ -111,10 +112,10 @@ function getSelectedFileIds(selectedFiles: SelectedState) {
} }
export function getSelectedFiles( export function getSelectedFiles(
selected: SelectedState, selected: SelectedState,
files: File[] files: EnteFile[]
): File[] { ): EnteFile[] {
const filesIDs = new Set(getSelectedFileIds(selected)); const filesIDs = new Set(getSelectedFileIds(selected));
const selectedFiles: File[] = []; const selectedFiles: EnteFile[] = [];
const foundFiles = new Set<number>(); const foundFiles = new Set<number>();
for (const file of files) { for (const file of files) {
if (filesIDs.has(file.id) && !foundFiles.has(file.id)) { if (filesIDs.has(file.id) && !foundFiles.has(file.id)) {
@ -125,14 +126,6 @@ export function getSelectedFiles(
return selectedFiles; return selectedFiles;
} }
export function checkFileFormatSupport(name: string) {
for (const format of UNSUPPORTED_FORMATS) {
if (name.toLowerCase().endsWith(format)) {
throw Error('unsupported format');
}
}
}
export function formatDate(date: number | Date) { export function formatDate(date: number | Date) {
const dateTimeFormat = new Intl.DateTimeFormat('en-IN', { const dateTimeFormat = new Intl.DateTimeFormat('en-IN', {
weekday: 'short', weekday: 'short',
@ -181,7 +174,7 @@ export function formatDateRelative(date: number) {
); );
} }
export function sortFiles(files: File[]) { export function sortFiles(files: EnteFile[]) {
// sort according to modification time first // sort according to modification time first
files = files.sort((a, b) => { files = files.sort((a, b) => {
if (!b.metadata?.modificationTime) { if (!b.metadata?.modificationTime) {
@ -209,7 +202,7 @@ export function sortFiles(files: File[]) {
return files; return files;
} }
export async function decryptFile(file: File, collection: Collection) { export async function decryptFile(file: EnteFile, collection: Collection) {
try { try {
const worker = await new CryptoWorker(); const worker = await new CryptoWorker();
file.key = await worker.decryptB64( file.key = await worker.decryptB64(
@ -244,7 +237,7 @@ export async function decryptFile(file: File, collection: Collection) {
} }
} }
export function removeUnnecessaryFileProps(files: File[]): File[] { export function removeUnnecessaryFileProps(files: EnteFile[]): EnteFile[] {
const stripedFiles = files.map((file) => { const stripedFiles = files.map((file) => {
delete file.src; delete file.src;
delete file.msrc; delete file.msrc;
@ -294,7 +287,7 @@ export function generateStreamFromArrayBuffer(data: Uint8Array) {
}); });
} }
export async function convertForPreview(file: File, fileBlob: Blob) { export async function convertForPreview(file: EnteFile, fileBlob: Blob) {
if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) { if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) {
const originalName = fileNameWithoutExtension(file.metadata.title); const originalName = fileNameWithoutExtension(file.metadata.title);
const motionPhoto = await decodeMotionPhoto(fileBlob, originalName); const motionPhoto = await decodeMotionPhoto(fileBlob, originalName);
@ -303,16 +296,17 @@ export async function convertForPreview(file: File, fileBlob: Blob) {
const typeFromExtension = getFileExtension(file.metadata.title); const typeFromExtension = getFileExtension(file.metadata.title);
const worker = await new CryptoWorker(); const worker = await new CryptoWorker();
const reader = new FileReader();
const mimeType = const mimeType =
(await getMimeTypeFromBlob(worker, fileBlob)) ?? typeFromExtension; (await getMimeTypeFromBlob(reader, fileBlob)) ?? typeFromExtension;
if (isFileHEIC(mimeType)) { if (isFileHEIC(mimeType)) {
fileBlob = await worker.convertHEIC2JPEG(fileBlob); fileBlob = await worker.convertHEIC2JPEG(fileBlob);
} }
return fileBlob; return fileBlob;
} }
export function fileIsArchived(file: File) { export function fileIsArchived(file: EnteFile) {
if ( if (
!file || !file ||
!file.magicMetadata || !file.magicMetadata ||
@ -326,7 +320,7 @@ export function fileIsArchived(file: File) {
} }
export async function updateMagicMetadataProps( export async function updateMagicMetadataProps(
file: File, file: EnteFile,
magicMetadataUpdates: MagicMetadataProps magicMetadataUpdates: MagicMetadataProps
) { ) {
const worker = await new CryptoWorker(); const worker = await new CryptoWorker();
@ -362,7 +356,7 @@ export async function updateMagicMetadataProps(
} }
} }
export async function updatePublicMagicMetadataProps( export async function updatePublicMagicMetadataProps(
file: File, file: EnteFile,
publicMetadataUpdates: PublicMagicMetadataProps publicMetadataUpdates: PublicMagicMetadataProps
) { ) {
const worker = await new CryptoWorker(); const worker = await new CryptoWorker();
@ -397,12 +391,12 @@ export async function updatePublicMagicMetadataProps(
} }
export async function changeFilesVisibility( export async function changeFilesVisibility(
files: File[], files: EnteFile[],
selected: SelectedState, selected: SelectedState,
visibility: VISIBILITY_STATE visibility: VISIBILITY_STATE
) { ) {
const selectedFiles = getSelectedFiles(selected, files); const selectedFiles = getSelectedFiles(selected, files);
const updatedFiles: File[] = []; const updatedFiles: EnteFile[] = [];
for (const file of selectedFiles) { for (const file of selectedFiles) {
const updatedMagicMetadataProps: MagicMetadataProps = { const updatedMagicMetadataProps: MagicMetadataProps = {
visibility, visibility,
@ -415,7 +409,10 @@ export async function changeFilesVisibility(
return updatedFiles; return updatedFiles;
} }
export async function changeFileCreationTime(file: File, editedTime: number) { export async function changeFileCreationTime(
file: EnteFile,
editedTime: number
) {
const updatedPublicMagicMetadataProps: PublicMagicMetadataProps = { const updatedPublicMagicMetadataProps: PublicMagicMetadataProps = {
editedTime, editedTime,
}; };
@ -426,7 +423,7 @@ export async function changeFileCreationTime(file: File, editedTime: number) {
); );
} }
export async function changeFileName(file: File, editedName: string) { export async function changeFileName(file: EnteFile, editedName: string) {
const updatedPublicMagicMetadataProps: PublicMagicMetadataProps = { const updatedPublicMagicMetadataProps: PublicMagicMetadataProps = {
editedName, editedName,
}; };
@ -437,7 +434,7 @@ export async function changeFileName(file: File, editedName: string) {
); );
} }
export function isSharedFile(file: File) { export function isSharedFile(file: EnteFile) {
const user: User = getData(LS_KEYS.USER); const user: User = getData(LS_KEYS.USER);
if (!user?.id || !file?.ownerID) { if (!user?.id || !file?.ownerID) {
@ -446,7 +443,7 @@ export function isSharedFile(file: File) {
return file.ownerID !== user.id; return file.ownerID !== user.id;
} }
export function mergeMetadata(files: File[]): File[] { export function mergeMetadata(files: EnteFile[]): EnteFile[] {
return files.map((file) => ({ return files.map((file) => ({
...file, ...file,
metadata: { metadata: {
@ -467,8 +464,8 @@ export function mergeMetadata(files: File[]): File[] {
} }
export function updateExistingFilePubMetadata( export function updateExistingFilePubMetadata(
existingFile: File, existingFile: EnteFile,
updatedFile: File updatedFile: EnteFile
) { ) {
existingFile.pubMagicMetadata = updatedFile.pubMagicMetadata; existingFile.pubMagicMetadata = updatedFile.pubMagicMetadata;
existingFile.metadata = mergeMetadata([existingFile])[0].metadata; existingFile.metadata = mergeMetadata([existingFile])[0].metadata;
@ -476,11 +473,11 @@ export function updateExistingFilePubMetadata(
export async function getFileFromURL(fileURL: string) { export async function getFileFromURL(fileURL: string) {
const fileBlob = await (await fetch(fileURL)).blob(); const fileBlob = await (await fetch(fileURL)).blob();
const fileFile = new globalThis.File([fileBlob], 'temp'); const fileFile = new File([fileBlob], 'temp');
return fileFile; return fileFile;
} }
export function getUniqueFiles(files: File[]) { export function getUniqueFiles(files: EnteFile[]) {
const idSet = new Set<number>(); const idSet = new Set<number>();
return files.filter((file) => { return files.filter((file) => {
if (!idSet.has(file.id)) { if (!idSet.has(file.id)) {
@ -491,7 +488,7 @@ export function getUniqueFiles(files: File[]) {
} }
}); });
} }
export function getNonTrashedUniqueUserFiles(files: File[]) { export function getNonTrashedUniqueUserFiles(files: EnteFile[]) {
const user: User = getData(LS_KEYS.USER) ?? {}; const user: User = getData(LS_KEYS.USER) ?? {};
return getUniqueFiles( return getUniqueFiles(
files.filter( files.filter(
@ -502,7 +499,7 @@ export function getNonTrashedUniqueUserFiles(files: File[]) {
); );
} }
export async function downloadFiles(files: File[]) { export async function downloadFiles(files: EnteFile[]) {
for (const file of files) { for (const file of files) {
try { try {
await downloadFile(file); await downloadFile(file);
@ -512,7 +509,7 @@ export async function downloadFiles(files: File[]) {
} }
} }
export function needsConversionForPreview(file: File) { export function needsConversionForPreview(file: EnteFile) {
const fileExtension = splitFilenameAndExtension(file.metadata.title)[1]; const fileExtension = splitFilenameAndExtension(file.metadata.title)[1];
if ( if (
file.metadata.fileType === FILE_TYPE.LIVE_PHOTO || file.metadata.fileType === FILE_TYPE.LIVE_PHOTO ||

View file

@ -1,6 +1,5 @@
import { DateValue } from 'components/SearchBar'; import { EnteFile } from 'types/file';
import { File } from 'services/fileService'; import { Bbox, DateValue } from 'types/search';
import { Bbox } from 'services/searchService';
export function isInsideBox( export function isInsideBox(
file: { longitude: number; latitude: number }, file: { longitude: number; latitude: number },
@ -36,7 +35,7 @@ export const isSameDay = (baseDate: DateValue) => (compareDate: Date) => {
}; };
export function getFilesWithCreationDay( export function getFilesWithCreationDay(
files: File[], files: EnteFile[],
searchedDate: DateValue searchedDate: DateValue
) { ) {
const isSearchedDate = isSameDay(searchedDate); const isSearchedDate = isSameDay(searchedDate);

View file

@ -1,5 +1,5 @@
import * as Sentry from '@sentry/nextjs'; import * as Sentry from '@sentry/nextjs';
import { errorWithContext } from 'utils/common/errorUtil'; import { errorWithContext } from 'utils/error';
import { getUserAnonymizedID } from 'utils/user'; import { getUserAnonymizedID } from 'utils/user';
export const logError = ( export const logError = (

View file

@ -1,11 +1,11 @@
import { FileWithCollection } from 'services/upload/uploadManager'; import { FileWithCollection, Metadata } from 'types/upload';
import { MetadataObject } from 'services/upload/uploadService'; import { EnteFile } from 'types/file';
import { File } from 'services/fileService';
const TYPE_JSON = 'json'; const TYPE_JSON = 'json';
export function fileAlreadyInCollection( export function fileAlreadyInCollection(
existingFilesInCollection: File[], existingFilesInCollection: EnteFile[],
newFileMetadata: MetadataObject newFileMetadata: Metadata
): boolean { ): boolean {
for (const existingFile of existingFilesInCollection) { for (const existingFile of existingFilesInCollection) {
if (areFilesSame(existingFile.metadata, newFileMetadata)) { if (areFilesSame(existingFile.metadata, newFileMetadata)) {
@ -15,8 +15,8 @@ export function fileAlreadyInCollection(
return false; return false;
} }
export function areFilesSame( export function areFilesSame(
existingFile: MetadataObject, existingFile: Metadata,
newFile: MetadataObject newFile: Metadata
): boolean { ): boolean {
if ( if (
existingFile.fileType === newFile.fileType && existingFile.fileType === newFile.fileType &&

View file

@ -149,30 +149,6 @@ export class Crypto {
return libsodium.fromHex(string); return libsodium.fromHex(string);
} }
// temporary fix for https://github.com/vercel/next.js/issues/25484
async getUint8ArrayView(file) {
try {
return await new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onabort = () =>
reject(Error('file reading was aborted'));
reader.onerror = () => reject(Error('file reading has failed'));
reader.onload = () => {
// Do whatever you want with the file contents
const result =
typeof reader.result === 'string'
? new TextEncoder().encode(reader.result)
: new Uint8Array(reader.result);
resolve(result);
};
reader.readAsArrayBuffer(file);
});
} catch (e) {
console.log(e, 'error reading file to byte-array');
throw e;
}
}
async convertHEIC2JPEG(file) { async convertHEIC2JPEG(file) {
return convertHEIC2JPEG(file); return convertHEIC2JPEG(file);
} }

View file

@ -1,12 +1,7 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es5", "target": "es5",
"lib": [ "lib": ["dom", "dom.iterable", "esnext", "webworker"],
"dom",
"dom.iterable",
"esnext",
"webworker"
],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"strict": false, "strict": false,
@ -25,9 +20,8 @@
"next-env.d.ts", "next-env.d.ts",
"**/*.ts", "**/*.ts",
"**/*.tsx", "**/*.tsx",
"src/pages/index.tsx" "src/pages/index.tsx",
"configUtil.js"
], ],
"exclude": [ "exclude": ["node_modules"]
"node_modules"
]
} }