commit
d3856b4063
44
SECURITY.md
Normal file
44
SECURITY.md
Normal 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
55
configUtil.js
Normal 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,
|
||||||
|
};
|
|
@ -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,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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
10
sentryConfigUtil.js
Normal 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;
|
||||||
|
};
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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%;
|
||||||
|
|
|
@ -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));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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>;
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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[];
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
15
src/constants/collection/index.ts
Normal file
15
src/constants/collection/index.ts
Normal 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,
|
||||||
|
}
|
1
src/constants/crypto/index.ts
Normal file
1
src/constants/crypto/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export const ENCRYPTION_CHUNK_SIZE = 4 * 1024 * 1024;
|
28
src/constants/export/index.ts
Normal file
28
src/constants/export/index.ts
Normal 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,
|
||||||
|
}
|
22
src/constants/file/index.ts
Normal file
22
src/constants/file/index.ts
Normal 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,
|
||||||
|
}
|
7
src/constants/gallery/index.ts
Normal file
7
src/constants/gallery/index.ts
Normal 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;
|
15
src/constants/pages/index.ts
Normal file
15
src/constants/pages/index.ts
Normal 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 = '/',
|
||||||
|
}
|
10
src/constants/sentry/index.ts
Normal file
10
src/constants/sentry/index.ts
Normal 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';
|
39
src/constants/upload/index.ts
Normal file
39
src/constants/upload/index.ts
Normal 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,
|
||||||
|
}
|
1
src/constants/user/index.ts
Normal file
1
src/constants/user/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export const FIX_CREATION_TIME_VISIBLE_TO_USER_IDS = [1, 125, 243, 341];
|
|
@ -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('');
|
||||||
|
|
|
@ -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>();
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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>();
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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');
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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');
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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';
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
);
|
);
|
||||||
|
|
|
@ -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) {
|
|
@ -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,
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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');
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
);
|
);
|
||||||
|
|
|
@ -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
|
||||||
);
|
);
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
39
src/types.ts
39
src/types.ts
|
@ -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 = '/',
|
|
||||||
}
|
|
23
src/types/billing/index.ts
Normal file
23
src/types/billing/index.ts
Normal 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;
|
||||||
|
}
|
52
src/types/collection/index.ts
Normal file
52
src/types/collection/index.ts
Normal 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
25
src/types/export/index.ts
Normal 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
86
src/types/file/index.ts
Normal 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;
|
||||||
|
}
|
31
src/types/gallery/index.ts
Normal file
31
src/types/gallery/index.ts
Normal 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
26
src/types/search/index.ts
Normal 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
11
src/types/trash/index.ts
Normal 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
112
src/types/upload/index.ts
Normal 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
76
src/types/user/index.ts
Normal 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;
|
||||||
|
}
|
|
@ -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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 ||
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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 = (
|
||||||
|
|
|
@ -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 &&
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
}
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in a new issue