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 { withSentryConfig } = require('@sentry/nextjs');
|
||||
const { PHASE_DEVELOPMENT_SERVER } = require('next/constants');
|
||||
|
||||
const cp = require('child_process');
|
||||
const gitSha = cp.execSync('git rev-parse --short HEAD', {
|
||||
cwd: __dirname,
|
||||
encoding: 'utf8',
|
||||
});
|
||||
const {
|
||||
getGitSha,
|
||||
convertToNextHeaderFormat,
|
||||
buildCSPHeader,
|
||||
COOP_COEP_HEADERS,
|
||||
WEB_SECURITY_HEADERS,
|
||||
CSP_DIRECTIVES,
|
||||
WORKBOX_CONFIG,
|
||||
ALL_ROUTES,
|
||||
getIsSentryEnabled,
|
||||
} = require('./configUtil');
|
||||
|
||||
module.exports = withSentryConfig(
|
||||
withWorkbox(
|
||||
withBundleAnalyzer({
|
||||
env: {
|
||||
SENTRY_RELEASE: gitSha,
|
||||
},
|
||||
workbox: {
|
||||
swSrc: 'src/serviceWorker.js',
|
||||
exclude: [/manifest\.json$/i],
|
||||
},
|
||||
// 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: gitSha }
|
||||
);
|
||||
const GIT_SHA = getGitSha();
|
||||
|
||||
const IS_SENTRY_ENABLED = getIsSentryEnabled();
|
||||
|
||||
module.exports = (phase) =>
|
||||
withSentryConfig(
|
||||
withWorkbox(
|
||||
withBundleAnalyzer({
|
||||
env: {
|
||||
SENTRY_RELEASE: GIT_SHA,
|
||||
},
|
||||
workbox: WORKBOX_CONFIG,
|
||||
|
||||
headers() {
|
||||
return [
|
||||
{
|
||||
// Apply these headers to all routes in your application....
|
||||
source: ALL_ROUTES,
|
||||
headers: convertToNextHeaderFormat({
|
||||
...COOP_COEP_HEADERS,
|
||||
...WEB_SECURITY_HEADERS,
|
||||
...buildCSPHeader(CSP_DIRECTIVES),
|
||||
}),
|
||||
},
|
||||
];
|
||||
},
|
||||
// https://dev.to/marcinwosinek/how-to-add-resolve-fallback-to-webpack-5-in-nextjs-10-i6j
|
||||
webpack: (config, { isServer }) => {
|
||||
if (!isServer) {
|
||||
config.resolve.fallback.fs = false;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
})
|
||||
),
|
||||
{
|
||||
release: GIT_SHA,
|
||||
dryRun: phase === PHASE_DEVELOPMENT_SERVER || !IS_SENTRY_ENABLED,
|
||||
}
|
||||
);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "bada-frame",
|
||||
"version": "0.4.0",
|
||||
"version": "0.4.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
|
|
|
@ -1,18 +1,24 @@
|
|||
import * as Sentry from '@sentry/nextjs';
|
||||
import { getSentryTunnelUrl } from 'utils/common/apiUtil';
|
||||
import { getUserAnonymizedID } from 'utils/user';
|
||||
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_ENV = process.env.NEXT_PUBLIC_SENTRY_ENV ?? 'development';
|
||||
const SENTRY_DSN = getSentryDSN();
|
||||
const SENTRY_ENV = getSentryENV();
|
||||
const SENTRY_RELEASE = getSentryRelease();
|
||||
const IS_ENABLED = getIsSentryEnabled();
|
||||
|
||||
Sentry.setUser({ id: getUserAnonymizedID() });
|
||||
Sentry.init({
|
||||
dsn: SENTRY_DSN,
|
||||
enabled: SENTRY_ENV !== 'development',
|
||||
enabled: IS_ENABLED,
|
||||
environment: SENTRY_ENV,
|
||||
release: process.env.SENTRY_RELEASE,
|
||||
release: SENTRY_RELEASE,
|
||||
attachStacktrace: true,
|
||||
autoSessionTracking: false,
|
||||
tunnel: getSentryTunnelUrl(),
|
||||
|
|
|
@ -1,12 +1,20 @@
|
|||
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_ENV = process.env.NEXT_PUBLIC_SENTRY_ENV ?? 'development';
|
||||
const SENTRY_DSN = getSentryDSN();
|
||||
const SENTRY_ENV = getSentryENV();
|
||||
const SENTRY_RELEASE = getSentryRelease();
|
||||
const IS_ENABLED = getIsSentryEnabled();
|
||||
|
||||
Sentry.init({
|
||||
dsn: SENTRY_DSN,
|
||||
enabled: SENTRY_ENV !== 'development',
|
||||
enabled: IS_ENABLED,
|
||||
environment: SENTRY_ENV,
|
||||
release: process.env.SENTRY_RELEASE,
|
||||
release: SENTRY_RELEASE,
|
||||
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 { AppContext, FLASH_MESSAGE_TYPE } from 'pages/_app';
|
||||
import { getData, LS_KEYS, setData } from 'utils/storage/localStorage';
|
||||
import { PAGES } from 'types';
|
||||
import { PAGES } from 'constants/pages';
|
||||
|
||||
interface formValues {
|
||||
email: string;
|
||||
|
|
|
@ -6,15 +6,12 @@ import Form from 'react-bootstrap/Form';
|
|||
import FormControl from 'react-bootstrap/FormControl';
|
||||
import { Button, Col, Table } from 'react-bootstrap';
|
||||
import { DeadCenter } from 'pages/gallery';
|
||||
import { User } from 'services/userService';
|
||||
import {
|
||||
Collection,
|
||||
shareCollection,
|
||||
unshareCollection,
|
||||
} from 'services/collectionService';
|
||||
import { User } from 'types/user';
|
||||
import { shareCollection, unshareCollection } from 'services/collectionService';
|
||||
import { getData, LS_KEYS } from 'utils/storage/localStorage';
|
||||
import SubmitButton from './SubmitButton';
|
||||
import MessageDialog from './MessageDialog';
|
||||
import { Collection } from 'types/collection';
|
||||
|
||||
interface Props {
|
||||
show: boolean;
|
||||
|
|
|
@ -1,20 +1,33 @@
|
|||
import React from 'react';
|
||||
|
||||
import DatePicker from 'react-datepicker';
|
||||
import 'react-datepicker/dist/react-datepicker.css';
|
||||
import {
|
||||
MIN_EDITED_CREATION_TIME,
|
||||
MAX_EDITED_CREATION_TIME,
|
||||
ALL_TIME,
|
||||
} from 'services/fileService';
|
||||
|
||||
import DatePicker from 'react-datepicker';
|
||||
import 'react-datepicker/dist/react-datepicker.css';
|
||||
} from 'constants/file';
|
||||
|
||||
const isSameDay = (first, second) =>
|
||||
first.getFullYear() === second.getFullYear() &&
|
||||
first.getMonth() === second.getMonth() &&
|
||||
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
|
||||
disabled={loading}
|
||||
open={isInEditMode}
|
||||
selected={pickedTime}
|
||||
onChange={handleChange}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
import { Button } from 'react-bootstrap';
|
||||
import { ExportStats } from 'services/exportService';
|
||||
import { ExportStats } from 'types/export';
|
||||
import { formatDateTime } from 'utils/file';
|
||||
import constants from 'utils/strings/constants';
|
||||
import { Label, Row, Value } from './Container';
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import React from 'react';
|
||||
import { Button, ProgressBar } from 'react-bootstrap';
|
||||
import { ExportProgress, ExportStage } from 'services/exportService';
|
||||
import { ExportProgress } from 'types/export';
|
||||
import styled from 'styled-components';
|
||||
import constants from 'utils/strings/constants';
|
||||
import { ExportStage } from 'constants/export';
|
||||
|
||||
export const ComfySpan = styled.span`
|
||||
word-spacing: 1rem;
|
||||
|
|
|
@ -1,14 +1,10 @@
|
|||
import isElectron from 'is-electron';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Button } from 'react-bootstrap';
|
||||
import exportService, {
|
||||
ExportProgress,
|
||||
ExportStage,
|
||||
ExportStats,
|
||||
ExportType,
|
||||
} from 'services/exportService';
|
||||
import exportService from 'services/exportService';
|
||||
import { ExportProgress, ExportStats } from 'types/export';
|
||||
import { getLocalFiles } from 'services/fileService';
|
||||
import { User } from 'services/userService';
|
||||
import { User } from 'types/user';
|
||||
import styled from 'styled-components';
|
||||
import { sleep } from 'utils/common';
|
||||
import { getExportRecordFileUID } from 'utils/export';
|
||||
|
@ -22,6 +18,7 @@ import ExportInProgress from './ExportInProgress';
|
|||
import FolderIcon from './icons/FolderIcon';
|
||||
import InProgressIcon from './icons/InProgressIcon';
|
||||
import MessageDialog from './MessageDialog';
|
||||
import { ExportStage, ExportType } from 'constants/export';
|
||||
|
||||
const FolderIconWrapper = styled.div`
|
||||
width: 15%;
|
||||
|
|
|
@ -3,14 +3,14 @@ import MessageDialog from '../MessageDialog';
|
|||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { updateCreationTimeWithExif } from 'services/updateCreationTimeWithExif';
|
||||
import { GalleryContext } from 'pages/gallery';
|
||||
import { File } from 'services/fileService';
|
||||
import { EnteFile } from 'types/file';
|
||||
import FixCreationTimeRunning from './running';
|
||||
import FixCreationTimeFooter from './footer';
|
||||
import { Formik } from 'formik';
|
||||
|
||||
import FixCreationTimeOptions from './options';
|
||||
export interface FixCreationTimeAttributes {
|
||||
files: File[];
|
||||
files: EnteFile[];
|
||||
}
|
||||
|
||||
interface Props {
|
||||
|
@ -89,7 +89,6 @@ export default function FixCreationTime(props: Props) {
|
|||
}
|
||||
|
||||
const onSubmit = (values: formValues) => {
|
||||
console.log(values);
|
||||
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 Button from 'react-bootstrap/Button';
|
||||
import LogoImg from './LogoImg';
|
||||
import { PAGES } from 'types';
|
||||
import { PAGES } from 'constants/pages';
|
||||
|
||||
interface formValues {
|
||||
email: string;
|
||||
|
|
|
@ -1,14 +1,8 @@
|
|||
import {
|
||||
GalleryContext,
|
||||
Search,
|
||||
SelectedState,
|
||||
SetFiles,
|
||||
setSearchStats,
|
||||
} from 'pages/gallery';
|
||||
import { GalleryContext } from 'pages/gallery';
|
||||
import PreviewCard from './pages/gallery/PreviewCard';
|
||||
import React, { useContext, useEffect, useRef, useState } from 'react';
|
||||
import { Button } from 'react-bootstrap';
|
||||
import { File, FILE_TYPE } from 'services/fileService';
|
||||
import { EnteFile } from 'types/file';
|
||||
import styled from 'styled-components';
|
||||
import DownloadManager from 'services/downloadManager';
|
||||
import constants from 'utils/strings/constants';
|
||||
|
@ -21,10 +15,12 @@ import {
|
|||
ALL_SECTION,
|
||||
ARCHIVE_SECTION,
|
||||
TRASH_SECTION,
|
||||
} from './pages/gallery/Collections';
|
||||
} from 'constants/collection';
|
||||
import { isSharedFile } from 'utils/file';
|
||||
import { isPlaybackPossible } from 'utils/photoFrame';
|
||||
import { PhotoList } from './PhotoList';
|
||||
import { SetFiles, SelectedState, Search, setSearchStats } from 'types/gallery';
|
||||
import { FILE_TYPE } from 'constants/file';
|
||||
|
||||
const Container = styled.div`
|
||||
display: block;
|
||||
|
@ -53,7 +49,7 @@ const EmptyScreen = styled.div`
|
|||
`;
|
||||
|
||||
interface Props {
|
||||
files: File[];
|
||||
files: EnteFile[];
|
||||
setFiles: SetFiles;
|
||||
syncWithRemote: () => Promise<void>;
|
||||
favItemIds: Set<number>;
|
||||
|
@ -134,7 +130,7 @@ const PhotoFrame = ({
|
|||
onThumbnailClick(filteredDataIdx)();
|
||||
}
|
||||
}
|
||||
}, [search]);
|
||||
}, [search, filteredData]);
|
||||
|
||||
const resetFetching = () => {
|
||||
setFetching({});
|
||||
|
@ -289,14 +285,23 @@ const PhotoFrame = ({
|
|||
if (selected.collectionID !== activeCollection) {
|
||||
setSelected({ count: 0, collectionID: 0 });
|
||||
}
|
||||
if (checked) {
|
||||
setRangeStart(index);
|
||||
if (typeof index !== 'undefined') {
|
||||
if (checked) {
|
||||
setRangeStart(index);
|
||||
} else {
|
||||
setRangeStart(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
setSelected((selected) => ({
|
||||
...selected,
|
||||
[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,
|
||||
}));
|
||||
};
|
||||
|
@ -305,22 +310,28 @@ const PhotoFrame = ({
|
|||
};
|
||||
|
||||
const handleRangeSelect = (index: number) => () => {
|
||||
if (rangeStart !== index) {
|
||||
let leftEnd = -1;
|
||||
let rightEnd = -1;
|
||||
if (index < rangeStart) {
|
||||
leftEnd = index + 1;
|
||||
rightEnd = rangeStart - 1;
|
||||
} else {
|
||||
leftEnd = rangeStart + 1;
|
||||
rightEnd = index - 1;
|
||||
if (typeof rangeStart !== 'undefined' && rangeStart !== index) {
|
||||
const direction =
|
||||
(index - rangeStart) / Math.abs(index - rangeStart);
|
||||
let checked = true;
|
||||
for (
|
||||
let i = rangeStart;
|
||||
(index - i) * direction >= 0;
|
||||
i += direction
|
||||
) {
|
||||
checked = checked && !!selected[filteredData[i].id];
|
||||
}
|
||||
for (let i = leftEnd; i <= rightEnd; i++) {
|
||||
handleSelect(filteredData[i].id)(true);
|
||||
for (
|
||||
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
|
||||
key={`tile-${file[index].id}-selected-${
|
||||
selected[file[index].id] ?? false
|
||||
|
@ -337,9 +348,7 @@ const PhotoFrame = ({
|
|||
selectOnClick={selected.count > 0}
|
||||
onHover={onHoverOver(index)}
|
||||
onRangeSelect={handleRangeSelect(index)}
|
||||
isRangeSelectActive={
|
||||
isShiftKeyPressed && (rangeStart || rangeStart === 0)
|
||||
}
|
||||
isRangeSelectActive={isShiftKeyPressed && selected.count > 0}
|
||||
isInsSelectRange={
|
||||
(index >= rangeStart && index <= currentHover) ||
|
||||
(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) {
|
||||
try {
|
||||
let url: string;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useRef, useEffect } from 'react';
|
||||
import { VariableSizeList as List } from 'react-window';
|
||||
import styled from 'styled-components';
|
||||
import { File } from 'services/fileService';
|
||||
import { EnteFile } from 'types/file';
|
||||
import {
|
||||
IMAGE_CONTAINER_MAX_WIDTH,
|
||||
IMAGE_CONTAINER_MAX_HEIGHT,
|
||||
|
@ -9,7 +9,7 @@ import {
|
|||
DATE_CONTAINER_HEIGHT,
|
||||
GAP_BTW_TILES,
|
||||
SPACE_BTW_DATES,
|
||||
} from 'types';
|
||||
} from 'constants/gallery';
|
||||
import constants from 'utils/strings/constants';
|
||||
|
||||
const A_DAY = 24 * 60 * 60 * 1000;
|
||||
|
@ -23,7 +23,7 @@ enum ITEM_TYPE {
|
|||
|
||||
interface TimeStampListItem {
|
||||
itemType: ITEM_TYPE;
|
||||
items?: File[];
|
||||
items?: EnteFile[];
|
||||
itemStartIndex?: number;
|
||||
date?: string;
|
||||
dates?: {
|
||||
|
@ -102,9 +102,9 @@ const NothingContainer = styled.div<{ span: number }>`
|
|||
interface Props {
|
||||
height: number;
|
||||
width: number;
|
||||
filteredData: File[];
|
||||
filteredData: EnteFile[];
|
||||
showBanner: boolean;
|
||||
getThumbnail: (file: File[], index: number) => JSX.Element;
|
||||
getThumbnail: (file: EnteFile[], index: number) => JSX.Element;
|
||||
activeCollection: number;
|
||||
resetFetching: () => void;
|
||||
}
|
||||
|
|
|
@ -7,11 +7,8 @@ import {
|
|||
addToFavorites,
|
||||
removeFromFavorites,
|
||||
} from 'services/collectionService';
|
||||
import {
|
||||
File,
|
||||
MAX_EDITED_FILE_NAME_LENGTH,
|
||||
updatePublicMagicMetadata,
|
||||
} from 'services/fileService';
|
||||
import { updatePublicMagicMetadata } from 'services/fileService';
|
||||
import { EnteFile } from 'types/file';
|
||||
import constants from 'utils/strings/constants';
|
||||
import exifr from 'exifr';
|
||||
import Modal from 'react-bootstrap/Modal';
|
||||
|
@ -45,13 +42,23 @@ import { Formik } from 'formik';
|
|||
import * as Yup from 'yup';
|
||||
import EnteSpinner from 'components/EnteSpinner';
|
||||
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 {
|
||||
isOpen: boolean;
|
||||
items: any[];
|
||||
currentIndex?: number;
|
||||
onClose?: (needUpdate: boolean) => void;
|
||||
gettingData: (instance: any, index: number, item: File) => void;
|
||||
gettingData: (instance: any, index: number, item: EnteFile) => void;
|
||||
id?: string;
|
||||
className?: string;
|
||||
favItemIds: Set<number>;
|
||||
|
@ -87,9 +94,10 @@ function RenderCreationTime({
|
|||
file,
|
||||
scheduleUpdate,
|
||||
}: {
|
||||
file: File;
|
||||
file: EnteFile;
|
||||
scheduleUpdate: () => void;
|
||||
}) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const originalCreationTime = new Date(file?.metadata.creationTime / 1000);
|
||||
const [isInEditMode, setIsInEditMode] = useState(false);
|
||||
|
||||
|
@ -100,6 +108,7 @@ function RenderCreationTime({
|
|||
|
||||
const saveEdits = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
if (isInEditMode && file) {
|
||||
const unixTimeInMicroSec = pickedTime.getTime() * 1000;
|
||||
if (unixTimeInMicroSec === file?.metadata.creationTime) {
|
||||
|
@ -118,14 +127,16 @@ function RenderCreationTime({
|
|||
}
|
||||
} catch (e) {
|
||||
logError(e, 'failed to update creationTime');
|
||||
} finally {
|
||||
closeEditMode();
|
||||
setLoading(false);
|
||||
}
|
||||
closeEditMode();
|
||||
};
|
||||
const discardEdits = () => {
|
||||
setPickedTime(originalCreationTime);
|
||||
closeEditMode();
|
||||
};
|
||||
const handleChange = (newDate) => {
|
||||
const handleChange = (newDate: Date) => {
|
||||
if (newDate instanceof Date) {
|
||||
setPickedTime(newDate);
|
||||
}
|
||||
|
@ -137,6 +148,7 @@ function RenderCreationTime({
|
|||
<Value width={isInEditMode ? '50%' : '60%'}>
|
||||
{isInEditMode ? (
|
||||
<EnteDateTimePicker
|
||||
loading={loading}
|
||||
isInEditMode={isInEditMode}
|
||||
pickedTime={pickedTime}
|
||||
handleChange={handleChange}
|
||||
|
@ -155,7 +167,11 @@ function RenderCreationTime({
|
|||
) : (
|
||||
<>
|
||||
<IconButton onClick={saveEdits}>
|
||||
<TickIcon />
|
||||
{loading ? (
|
||||
<SmallLoadingSpinner />
|
||||
) : (
|
||||
<TickIcon />
|
||||
)}
|
||||
</IconButton>
|
||||
<IconButton onClick={discardEdits}>
|
||||
<CloseIcon />
|
||||
|
@ -239,12 +255,7 @@ const FileNameEditForm = ({ filename, saveEdits, discardEdits, extension }) => {
|
|||
<Value width={'16.67%'}>
|
||||
<IconButton type="submit" disabled={loading}>
|
||||
{loading ? (
|
||||
<EnteSpinner
|
||||
style={{
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
}}
|
||||
/>
|
||||
<SmallLoadingSpinner />
|
||||
) : (
|
||||
<TickIcon />
|
||||
)}
|
||||
|
@ -267,7 +278,7 @@ function RenderFileName({
|
|||
file,
|
||||
scheduleUpdate,
|
||||
}: {
|
||||
file: File;
|
||||
file: EnteFile;
|
||||
scheduleUpdate: () => void;
|
||||
}) {
|
||||
const originalTitle = file?.metadata.title;
|
||||
|
@ -456,7 +467,7 @@ function PhotoSwipe(props: Iprops) {
|
|||
const { isOpen, items } = props;
|
||||
const [isFav, setIsFav] = 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 needUpdate = useRef(false);
|
||||
|
||||
|
@ -605,26 +616,28 @@ function PhotoSwipe(props: Iprops) {
|
|||
}
|
||||
};
|
||||
|
||||
const checkExifAvailable = () => {
|
||||
const checkExifAvailable = async () => {
|
||||
setExif(null);
|
||||
setTimeout(() => {
|
||||
await sleep(100);
|
||||
try {
|
||||
const img: HTMLImageElement = document.querySelector(
|
||||
'.pswp__img:not(.pswp__img--placeholder)'
|
||||
);
|
||||
if (img) {
|
||||
exifr.parse(img).then(function (exifData) {
|
||||
if (!exifData) {
|
||||
return;
|
||||
}
|
||||
exifData.raw = prettyPrintExif(exifData);
|
||||
setExif(exifData);
|
||||
});
|
||||
const exifData = await exifr.parse(img);
|
||||
if (!exifData) {
|
||||
return;
|
||||
}
|
||||
exifData.raw = prettyPrintExif(exifData);
|
||||
setExif(exifData);
|
||||
}
|
||||
}, 100);
|
||||
} catch (e) {
|
||||
logError(e, 'exifr parsing failed');
|
||||
}
|
||||
};
|
||||
|
||||
function updateInfo() {
|
||||
const file: File = this?.currItem;
|
||||
const file: EnteFile = this?.currItem;
|
||||
if (file?.metadata) {
|
||||
setMetaData(file.metadata);
|
||||
setExif(null);
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
import { Search, SearchStats } from 'pages/gallery';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import AsyncSelect from 'react-select/async';
|
||||
import { components } from 'react-select';
|
||||
import debounce from 'debounce-promise';
|
||||
import {
|
||||
Bbox,
|
||||
getHolidaySuggestion,
|
||||
getYearSuggestion,
|
||||
parseHumanDate,
|
||||
|
@ -19,12 +17,16 @@ import LocationIcon from './icons/LocationIcon';
|
|||
import DateIcon from './icons/DateIcon';
|
||||
import SearchIcon from './icons/SearchIcon';
|
||||
import CloseIcon from './icons/CloseIcon';
|
||||
import { Collection } from 'services/collectionService';
|
||||
import { Collection } from 'types/collection';
|
||||
import CollectionIcon from './icons/CollectionIcon';
|
||||
import { File, FILE_TYPE } from 'services/fileService';
|
||||
|
||||
import ImageIcon from './icons/ImageIcon';
|
||||
import VideoIcon from './icons/VideoIcon';
|
||||
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 }>`
|
||||
position: fixed;
|
||||
|
@ -75,23 +77,6 @@ const SearchInput = styled.div`
|
|||
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 {
|
||||
isOpen: boolean;
|
||||
isFirstFetch: boolean;
|
||||
|
@ -101,7 +86,7 @@ interface Props {
|
|||
searchStats: SearchStats;
|
||||
collections: Collection[];
|
||||
setActiveCollection: (id: number) => void;
|
||||
files: File[];
|
||||
files: EnteFile[];
|
||||
}
|
||||
export default function SearchBar(props: Props) {
|
||||
const [value, setValue] = useState<Suggestion>(null);
|
||||
|
|
|
@ -13,10 +13,10 @@ import {
|
|||
isSubscriptionCancelled,
|
||||
isSubscribed,
|
||||
convertToHumanReadable,
|
||||
} from 'utils/billingUtil';
|
||||
} from 'utils/billing';
|
||||
|
||||
import isElectron from 'is-electron';
|
||||
import { Collection } from 'services/collectionService';
|
||||
import { Collection } from 'types/collection';
|
||||
import { useRouter } from 'next/router';
|
||||
import LinkButton from './pages/gallery/LinkButton';
|
||||
import { downloadApp } from 'utils/common';
|
||||
|
@ -27,16 +27,14 @@ import EnteSpinner from './EnteSpinner';
|
|||
import RecoveryKeyModal from './RecoveryKeyModal';
|
||||
import TwoFactorModal from './TwoFactorModal';
|
||||
import ExportModal from './ExportModal';
|
||||
import { GalleryContext, SetLoading } from 'pages/gallery';
|
||||
import { GalleryContext } from 'pages/gallery';
|
||||
import InProgressIcon from './icons/InProgressIcon';
|
||||
import exportService from 'services/exportService';
|
||||
import { Subscription } from 'services/billingService';
|
||||
import { PAGES } from 'types';
|
||||
import {
|
||||
ARCHIVE_SECTION,
|
||||
TRASH_SECTION,
|
||||
} from 'components/pages/gallery/Collections';
|
||||
import { Subscription } from 'types/billing';
|
||||
import { PAGES } from 'constants/pages';
|
||||
import { ARCHIVE_SECTION, TRASH_SECTION } from 'constants/collection';
|
||||
import FixLargeThumbnails from './FixLargeThumbnail';
|
||||
import { SetLoading } from 'types/gallery';
|
||||
interface Props {
|
||||
collections: Collection[];
|
||||
setDialogMessage: SetDialogMessage;
|
||||
|
|
|
@ -19,7 +19,7 @@ import { setJustSignedUp } from 'utils/storage';
|
|||
import LogoImg from './LogoImg';
|
||||
import { logError } from 'utils/sentry';
|
||||
import { SESSION_KEYS } from 'utils/storage/sessionStorage';
|
||||
import { PAGES } from 'types';
|
||||
import { PAGES } from 'constants/pages';
|
||||
|
||||
interface FormValues {
|
||||
email: string;
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
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 React, { useContext, useEffect, useState } from 'react';
|
||||
import { Button } from 'react-bootstrap';
|
||||
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 constants from 'utils/strings/constants';
|
||||
import { Label, Value, Row } from './Container';
|
||||
|
|
|
@ -1,16 +1,13 @@
|
|||
import React from 'react';
|
||||
import { SetDialogMessage } from 'components/MessageDialog';
|
||||
import { ListGroup, Popover } from 'react-bootstrap';
|
||||
import {
|
||||
Collection,
|
||||
deleteCollection,
|
||||
renameCollection,
|
||||
} from 'services/collectionService';
|
||||
import { deleteCollection, renameCollection } from 'services/collectionService';
|
||||
import { downloadCollection, getSelectedCollection } from 'utils/collection';
|
||||
import constants from 'utils/strings/constants';
|
||||
import { SetCollectionNamerAttributes } from './CollectionNamer';
|
||||
import LinkButton, { ButtonVariant, LinkButtonProps } from './LinkButton';
|
||||
import { sleep } from 'utils/common';
|
||||
import { Collection } from 'types/collection';
|
||||
|
||||
interface CollectionOptionsProps {
|
||||
syncWithRemote: () => Promise<void>;
|
||||
|
|
|
@ -1,15 +1,12 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { Card, Modal } from 'react-bootstrap';
|
||||
import styled from 'styled-components';
|
||||
import {
|
||||
Collection,
|
||||
CollectionAndItsLatestFile,
|
||||
CollectionType,
|
||||
} from 'services/collectionService';
|
||||
import AddCollectionButton from './AddCollectionButton';
|
||||
import PreviewCard from './PreviewCard';
|
||||
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`
|
||||
width: 200px;
|
||||
|
@ -53,7 +50,7 @@ function CollectionSelector({
|
|||
CollectionAndItsLatestFile[]
|
||||
>([]);
|
||||
useEffect(() => {
|
||||
if (!attributes) {
|
||||
if (!attributes || !props.show) {
|
||||
return;
|
||||
}
|
||||
const user: User = getData(LS_KEYS.USER);
|
||||
|
@ -86,6 +83,7 @@ function CollectionSelector({
|
|||
<PreviewCard
|
||||
file={item.file}
|
||||
updateUrl={() => {}}
|
||||
onSelect={() => {}}
|
||||
forcedEnable
|
||||
/>
|
||||
<Card.Text className="text-center">
|
||||
|
|
|
@ -2,7 +2,7 @@ import { IconButton } from 'components/Container';
|
|||
import SortIcon from 'components/icons/SortIcon';
|
||||
import React from 'react';
|
||||
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 CollectionSortOptions from './CollectionSortOptions';
|
||||
import { IconWithMessage } from './SelectedFileOptions';
|
||||
|
|
|
@ -2,7 +2,7 @@ import { Value } from 'components/Container';
|
|||
import TickIcon from 'components/icons/TickIcon';
|
||||
import React from 'react';
|
||||
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 constants from 'utils/strings/constants';
|
||||
import { MenuItem, MenuLink } from './CollectionOptions';
|
||||
|
|
|
@ -5,16 +5,11 @@ import NavigationButton, {
|
|||
} from 'components/NavigationButton';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { OverlayTrigger, Tooltip } from 'react-bootstrap';
|
||||
import {
|
||||
Collection,
|
||||
CollectionAndItsLatestFile,
|
||||
CollectionType,
|
||||
COLLECTION_SORT_BY,
|
||||
sortCollections,
|
||||
} from 'services/collectionService';
|
||||
import { User } from 'services/userService';
|
||||
import { sortCollections } from 'services/collectionService';
|
||||
import { User } from 'types/user';
|
||||
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 { getData, LS_KEYS } from 'utils/storage/localStorage';
|
||||
import constants from 'utils/strings/constants';
|
||||
|
@ -22,10 +17,13 @@ import { SetCollectionNamerAttributes } from './CollectionNamer';
|
|||
import CollectionOptions from './CollectionOptions';
|
||||
import CollectionSort from './CollectionSort';
|
||||
import OptionIcon, { OptionIconWrapper } from './OptionIcon';
|
||||
|
||||
export const ARCHIVE_SECTION = -1;
|
||||
export const TRASH_SECTION = -2;
|
||||
export const ALL_SECTION = 0;
|
||||
import {
|
||||
ALL_SECTION,
|
||||
ARCHIVE_SECTION,
|
||||
CollectionType,
|
||||
COLLECTION_SORT_BY,
|
||||
TRASH_SECTION,
|
||||
} from 'constants/collection';
|
||||
|
||||
interface CollectionProps {
|
||||
collections: Collection[];
|
||||
|
|
|
@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react';
|
|||
import { Form, Modal, Button } from 'react-bootstrap';
|
||||
import constants from 'utils/strings/constants';
|
||||
import styled from 'styled-components';
|
||||
import billingService, { Plan, Subscription } from 'services/billingService';
|
||||
import { Plan, Subscription } from 'types/billing';
|
||||
import {
|
||||
convertBytesToGBs,
|
||||
getUserSubscription,
|
||||
|
@ -16,12 +16,14 @@ import {
|
|||
hasPaidSubscription,
|
||||
isOnFreePlan,
|
||||
planForSubscription,
|
||||
} from 'utils/billingUtil';
|
||||
} from 'utils/billing';
|
||||
import { reverseString } from 'utils/common';
|
||||
import { SetDialogMessage } from 'components/MessageDialog';
|
||||
import ArrowEast from 'components/icons/ArrowEast';
|
||||
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 }>`
|
||||
border-radius: 20px;
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
import React, { useContext, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { File } from 'services/fileService';
|
||||
import { EnteFile } from 'types/file';
|
||||
import styled from 'styled-components';
|
||||
import PlayCircleOutline from 'components/icons/PlayCircleOutline';
|
||||
import DownloadManager from 'services/downloadManager';
|
||||
import useLongPress from 'utils/common/useLongPress';
|
||||
import { GalleryContext } from 'pages/gallery';
|
||||
import { GAP_BTW_TILES } from 'types';
|
||||
import { GAP_BTW_TILES } from 'constants/gallery';
|
||||
|
||||
interface IProps {
|
||||
file: File;
|
||||
file: EnteFile;
|
||||
updateUrl: (url: string) => void;
|
||||
onClick?: () => void;
|
||||
forcedEnable?: boolean;
|
||||
selectable?: boolean;
|
||||
selected?: boolean;
|
||||
onSelect?: (checked: boolean) => void;
|
||||
onSelect: (checked: boolean) => void;
|
||||
onHover?: () => void;
|
||||
onRangeSelect?: () => void;
|
||||
isRangeSelectActive?: boolean;
|
||||
|
@ -217,8 +217,9 @@ export default function PreviewCard(props: IProps) {
|
|||
if (selectOnClick) {
|
||||
if (isRangeSelectActive) {
|
||||
onRangeSelect();
|
||||
} else {
|
||||
onSelect(!selected);
|
||||
}
|
||||
onSelect?.(!selected);
|
||||
} else if (file?.msrc || imgSrc) {
|
||||
onClick?.();
|
||||
}
|
||||
|
@ -227,15 +228,16 @@ export default function PreviewCard(props: IProps) {
|
|||
const handleSelect: React.ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
if (isRangeSelectActive) {
|
||||
onRangeSelect?.();
|
||||
} else {
|
||||
onSelect(e.target.checked);
|
||||
}
|
||||
onSelect?.(e.target.checked);
|
||||
};
|
||||
|
||||
const longPressCallback = () => {
|
||||
onSelect(!selected);
|
||||
};
|
||||
const handleHover = () => {
|
||||
if (selectOnClick) {
|
||||
if (isRangeSelectActive) {
|
||||
onHover();
|
||||
}
|
||||
};
|
||||
|
|
|
@ -11,19 +11,21 @@ import constants from 'utils/strings/constants';
|
|||
import Archive from 'components/icons/Archive';
|
||||
import MoveIcon from 'components/icons/MoveIcon';
|
||||
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 { OverlayTrigger } from 'react-bootstrap';
|
||||
import { Collection } from 'services/collectionService';
|
||||
import { Collection } from 'types/collection';
|
||||
import RemoveIcon from 'components/icons/RemoveIcon';
|
||||
import RestoreIcon from 'components/icons/RestoreIcon';
|
||||
import ClockIcon from 'components/icons/ClockIcon';
|
||||
import { getData, LS_KEYS } from 'utils/storage/localStorage';
|
||||
import {
|
||||
FIX_CREATION_TIME_VISIBLE_TO_USER_IDS,
|
||||
User,
|
||||
} from 'services/userService';
|
||||
import { FIX_CREATION_TIME_VISIBLE_TO_USER_IDS } from 'constants/user';
|
||||
import DownloadIcon from 'components/icons/DownloadIcon';
|
||||
import { User } from 'types/user';
|
||||
|
||||
interface Props {
|
||||
addToCollectionHelper: (collection: Collection) => void;
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
import React, { useContext, useEffect, useState } from 'react';
|
||||
|
||||
import {
|
||||
Collection,
|
||||
syncCollections,
|
||||
createAlbum,
|
||||
} from 'services/collectionService';
|
||||
import { syncCollections, createAlbum } from 'services/collectionService';
|
||||
import constants from 'utils/strings/constants';
|
||||
import { SetDialogMessage } from 'components/MessageDialog';
|
||||
import UploadProgress from './UploadProgress';
|
||||
|
@ -12,24 +8,25 @@ import UploadProgress from './UploadProgress';
|
|||
import ChoiceModal from './ChoiceModal';
|
||||
import { SetCollectionNamerAttributes } from './CollectionNamer';
|
||||
import { SetCollectionSelectorAttributes } from './CollectionSelector';
|
||||
import { GalleryContext, SetFiles, SetLoading } from 'pages/gallery';
|
||||
import { GalleryContext } from 'pages/gallery';
|
||||
import { AppContext } from 'pages/_app';
|
||||
import { logError } from 'utils/sentry';
|
||||
import { FileRejection } from 'react-dropzone';
|
||||
import 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 { getUserFacingErrorMessage } from 'utils/common/errorUtil';
|
||||
import { METADATA_FOLDER_NAME } from 'constants/export';
|
||||
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';
|
||||
|
||||
interface Props {
|
||||
syncWithRemote: (force?: boolean, silent?: boolean) => Promise<void>;
|
||||
setBannerMessage: (message: string | JSX.Element) => void;
|
||||
acceptedFiles: globalThis.File[];
|
||||
acceptedFiles: File[];
|
||||
closeCollectionSelector: () => void;
|
||||
setCollectionSelectorAttributes: SetCollectionSelectorAttributes;
|
||||
setCollectionNamerAttributes: SetCollectionNamerAttributes;
|
||||
|
@ -42,7 +39,7 @@ interface Props {
|
|||
isFirstUpload: boolean;
|
||||
}
|
||||
|
||||
export enum UPLOAD_STRATEGY {
|
||||
enum UPLOAD_STRATEGY {
|
||||
SINGLE_COLLECTION,
|
||||
COLLECTION_PER_FOLDER,
|
||||
}
|
||||
|
@ -51,18 +48,6 @@ interface AnalysisResult {
|
|||
suggestedCollectionName: string;
|
||||
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) {
|
||||
const [progressView, setProgressView] = useState(false);
|
||||
|
@ -156,12 +141,12 @@ export default function Upload(props: Props) {
|
|||
}
|
||||
}
|
||||
return {
|
||||
suggestedCollectionName: commonPathPrefix,
|
||||
suggestedCollectionName: commonPathPrefix || null,
|
||||
multipleFolders: firstFileFolder !== lastFileFolder,
|
||||
};
|
||||
}
|
||||
function getCollectionWiseFiles() {
|
||||
const collectionWiseFiles = new Map<string, globalThis.File[]>();
|
||||
const collectionWiseFiles = new Map<string, File[]>();
|
||||
for (const file of props.acceptedFiles) {
|
||||
const filePath = file['path'] as string;
|
||||
|
||||
|
@ -203,7 +188,7 @@ export default function Upload(props: Props) {
|
|||
|
||||
const filesWithCollectionToUpload: FileWithCollection[] = [];
|
||||
const collections: Collection[] = [];
|
||||
let collectionWiseFiles = new Map<string, globalThis.File[]>();
|
||||
let collectionWiseFiles = new Map<string, File[]>();
|
||||
if (strategy === UPLOAD_STRATEGY.SINGLE_COLLECTION) {
|
||||
collectionWiseFiles.set(collectionName, props.acceptedFiles);
|
||||
} else {
|
||||
|
@ -288,16 +273,20 @@ export default function Upload(props: Props) {
|
|||
};
|
||||
|
||||
const uploadToSingleNewCollection = (collectionName: string) => {
|
||||
uploadFilesToNewCollections(
|
||||
UPLOAD_STRATEGY.SINGLE_COLLECTION,
|
||||
collectionName
|
||||
);
|
||||
if (collectionName) {
|
||||
uploadFilesToNewCollections(
|
||||
UPLOAD_STRATEGY.SINGLE_COLLECTION,
|
||||
collectionName
|
||||
);
|
||||
} else {
|
||||
showCollectionCreateModal();
|
||||
}
|
||||
};
|
||||
const showCollectionCreateModal = (analysisResult: AnalysisResult) => {
|
||||
const showCollectionCreateModal = () => {
|
||||
props.setCollectionNamerAttributes({
|
||||
title: constants.CREATE_COLLECTION,
|
||||
buttonText: constants.CREATE,
|
||||
autoFilledName: analysisResult?.suggestedCollectionName,
|
||||
autoFilledName: null,
|
||||
callback: uploadToSingleNewCollection,
|
||||
});
|
||||
};
|
||||
|
@ -306,25 +295,26 @@ export default function Upload(props: Props) {
|
|||
analysisResult: AnalysisResult,
|
||||
isFirstUpload: boolean
|
||||
) => {
|
||||
if (!analysisResult.suggestedCollectionName) {
|
||||
if (isFirstUpload) {
|
||||
uploadToSingleNewCollection(FIRST_ALBUM_NAME);
|
||||
} else {
|
||||
props.setCollectionSelectorAttributes({
|
||||
callback: uploadFilesToExistingCollection,
|
||||
showNextModal: () =>
|
||||
showCollectionCreateModal(analysisResult),
|
||||
title: constants.UPLOAD_TO_COLLECTION,
|
||||
});
|
||||
}
|
||||
if (isFirstUpload) {
|
||||
const collectionName =
|
||||
analysisResult.suggestedCollectionName ?? FIRST_ALBUM_NAME;
|
||||
|
||||
uploadToSingleNewCollection(collectionName);
|
||||
} else {
|
||||
let showNextModal = () => {};
|
||||
if (analysisResult.multipleFolders) {
|
||||
setChoiceModalView(true);
|
||||
} else if (analysisResult.suggestedCollectionName) {
|
||||
uploadToSingleNewCollection(
|
||||
analysisResult.suggestedCollectionName
|
||||
);
|
||||
showNextModal = () => setChoiceModalView(true);
|
||||
} else {
|
||||
showNextModal = () =>
|
||||
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 { Button, Modal, ProgressBar } from 'react-bootstrap';
|
||||
import { FileRejection } from 'react-dropzone';
|
||||
import {
|
||||
FileUploadResults,
|
||||
UPLOAD_STAGES,
|
||||
} from 'services/upload/uploadManager';
|
||||
|
||||
import styled from 'styled-components';
|
||||
import { DESKTOP_APP_DOWNLOAD_URL } from 'utils/common';
|
||||
import constants from 'utils/strings/constants';
|
||||
import { Collapse } from 'react-collapse';
|
||||
import { ButtonVariant, getVariantColor } from './LinkButton';
|
||||
import { FileUploadResults, UPLOAD_STAGES } from 'constants/upload';
|
||||
|
||||
interface Props {
|
||||
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 ChangeEmailForm from 'components/ChangeEmail';
|
||||
import EnteCard from 'components/EnteCard';
|
||||
import { PAGES } from 'types';
|
||||
import { PAGES } from 'constants/pages';
|
||||
|
||||
function ChangeEmailPage() {
|
||||
const [email, setEmail] = useState('');
|
||||
|
|
|
@ -8,17 +8,12 @@ import CryptoWorker, {
|
|||
B64EncryptionResult,
|
||||
} from 'utils/crypto';
|
||||
import { getActualKey } from 'utils/common/key';
|
||||
import { setKeys, UpdatedKey } from 'services/userService';
|
||||
import { setKeys } from 'services/userService';
|
||||
import SetPasswordForm from 'components/SetPasswordForm';
|
||||
import { AppContext } from 'pages/_app';
|
||||
import { SESSION_KEYS } from 'utils/storage/sessionStorage';
|
||||
import { PAGES } from 'types';
|
||||
|
||||
export interface KEK {
|
||||
key: string;
|
||||
opsLimit: number;
|
||||
memLimit: number;
|
||||
}
|
||||
import { PAGES } from 'constants/pages';
|
||||
import { KEK, UpdatedKey } from 'types/user';
|
||||
|
||||
export default function Generate() {
|
||||
const [token, setToken] = useState<string>();
|
||||
|
|
|
@ -3,7 +3,7 @@ import React, { useContext, useEffect, useState } from 'react';
|
|||
import constants from 'utils/strings/constants';
|
||||
import { clearData, getData, LS_KEYS } from 'utils/storage/localStorage';
|
||||
import { useRouter } from 'next/router';
|
||||
import { KeyAttributes, PAGES } from 'types';
|
||||
import { PAGES } from 'constants/pages';
|
||||
import { SESSION_KEYS, getKey } from 'utils/storage/sessionStorage';
|
||||
import CryptoWorker, {
|
||||
decryptAndStoreToken,
|
||||
|
@ -18,6 +18,7 @@ import { Button, Card } from 'react-bootstrap';
|
|||
import { AppContext } from 'pages/_app';
|
||||
import LogoImg from 'components/LogoImg';
|
||||
import { logError } from 'utils/sentry';
|
||||
import { KeyAttributes } from 'types/user';
|
||||
|
||||
export default function Credentials() {
|
||||
const router = useRouter();
|
||||
|
|
|
@ -8,30 +8,25 @@ import React, {
|
|||
import { useRouter } from 'next/router';
|
||||
import { clearKeys, getKey, SESSION_KEYS } from 'utils/storage/sessionStorage';
|
||||
import {
|
||||
File,
|
||||
getLocalFiles,
|
||||
syncFiles,
|
||||
updateMagicMetadata,
|
||||
VISIBILITY_STATE,
|
||||
trashFiles,
|
||||
deleteFromTrash,
|
||||
} from 'services/fileService';
|
||||
import styled from 'styled-components';
|
||||
import LoadingBar from 'react-top-loading-bar';
|
||||
import {
|
||||
Collection,
|
||||
syncCollections,
|
||||
CollectionAndItsLatestFile,
|
||||
getCollectionsAndTheirLatestFile,
|
||||
getFavItemIds,
|
||||
getLocalCollections,
|
||||
getNonEmptyCollections,
|
||||
createCollection,
|
||||
CollectionType,
|
||||
} from 'services/collectionService';
|
||||
import constants from 'utils/strings/constants';
|
||||
import billingService from 'services/billingService';
|
||||
import { checkSubscriptionPurchase } from 'utils/billingUtil';
|
||||
import { checkSubscriptionPurchase } from 'utils/billing';
|
||||
|
||||
import FullScreenDropZone from 'components/FullScreenDropZone';
|
||||
import Sidebar from 'components/Sidebar';
|
||||
|
@ -57,8 +52,7 @@ import {
|
|||
sortFiles,
|
||||
sortFilesIntoCollections,
|
||||
} from 'utils/file';
|
||||
import SearchBar, { DateValue } from 'components/SearchBar';
|
||||
import { Bbox } from 'services/searchService';
|
||||
import SearchBar from 'components/SearchBar';
|
||||
import SelectedFileOptions from 'components/pages/gallery/SelectedFileOptions';
|
||||
import CollectionSelector, {
|
||||
CollectionSelectorAttributes,
|
||||
|
@ -70,14 +64,15 @@ import AlertBanner from 'components/pages/gallery/AlertBanner';
|
|||
import UploadButton from 'components/pages/gallery/UploadButton';
|
||||
import PlanSelector from 'components/pages/gallery/PlanSelector';
|
||||
import Upload from 'components/pages/gallery/Upload';
|
||||
import Collections, {
|
||||
import {
|
||||
ALL_SECTION,
|
||||
ARCHIVE_SECTION,
|
||||
CollectionType,
|
||||
TRASH_SECTION,
|
||||
} from 'components/pages/gallery/Collections';
|
||||
} from 'constants/collection';
|
||||
import { AppContext } from 'pages/_app';
|
||||
import { CustomError, ServerErrorCodes } from 'utils/common/errorUtil';
|
||||
import { PAGES } from 'types';
|
||||
import { CustomError, ServerErrorCodes } from 'utils/error';
|
||||
import { PAGES } from 'constants/pages';
|
||||
import {
|
||||
COLLECTION_OPS_TYPE,
|
||||
isSharedCollection,
|
||||
|
@ -92,12 +87,18 @@ import {
|
|||
getLocalTrash,
|
||||
getTrashedFiles,
|
||||
syncTrash,
|
||||
Trash,
|
||||
} from 'services/trashService';
|
||||
import { Trash } from 'types/trash';
|
||||
|
||||
import DeleteBtn from 'components/DeleteBtn';
|
||||
import FixCreationTime, {
|
||||
FixCreationTimeAttributes,
|
||||
} 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`
|
||||
flex: 1;
|
||||
|
@ -114,34 +115,6 @@ const AlertContainer = styled.div`
|
|||
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 = {
|
||||
thumbs: new Map(),
|
||||
files: new Map(),
|
||||
|
@ -159,7 +132,7 @@ export default function Gallery() {
|
|||
const [collections, setCollections] = useState<Collection[]>([]);
|
||||
const [collectionsAndTheirLatestFile, setCollectionsAndTheirLatestFile] =
|
||||
useState<CollectionAndItsLatestFile[]>([]);
|
||||
const [files, setFiles] = useState<File[]>(null);
|
||||
const [files, setFiles] = useState<EnteFile[]>(null);
|
||||
const [favItemIds, setFavItemIds] = useState<Set<number>>();
|
||||
const [bannerMessage, setBannerMessage] = useState<JSX.Element | string>(
|
||||
null
|
||||
|
@ -339,7 +312,7 @@ export default function Gallery() {
|
|||
|
||||
const setDerivativeState = async (
|
||||
collections: Collection[],
|
||||
files: File[]
|
||||
files: EnteFile[]
|
||||
) => {
|
||||
const favItemIds = await getFavItemIds(files);
|
||||
setFavItemIds(favItemIds);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useState, useEffect, useContext } from 'react';
|
||||
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 { useRouter } from 'next/router';
|
||||
import { getKey, SESSION_KEYS } from 'utils/storage/sessionStorage';
|
||||
|
@ -12,17 +12,12 @@ import {
|
|||
import SetPasswordForm from 'components/SetPasswordForm';
|
||||
import { justSignedUp, setJustSignedUp } from 'utils/storage';
|
||||
import RecoveryKeyModal from 'components/RecoveryKeyModal';
|
||||
import { KeyAttributes, PAGES } from 'types';
|
||||
import { PAGES } from 'constants/pages';
|
||||
import Container from 'components/Container';
|
||||
import EnteSpinner from 'components/EnteSpinner';
|
||||
import { AppContext } from 'pages/_app';
|
||||
import { logError } from 'utils/sentry';
|
||||
|
||||
export interface KEK {
|
||||
key: string;
|
||||
opsLimit: number;
|
||||
memLimit: number;
|
||||
}
|
||||
import { KeyAttributes, User } from 'types/user';
|
||||
|
||||
export default function Generate() {
|
||||
const [token, setToken] = useState<string>();
|
||||
|
|
|
@ -12,7 +12,7 @@ import constants from 'utils/strings/constants';
|
|||
import localForage from 'utils/storage/localForage';
|
||||
import IncognitoWarning from 'components/IncognitoWarning';
|
||||
import { logError } from 'utils/sentry';
|
||||
import { PAGES } from 'types';
|
||||
import { PAGES } from 'constants/pages';
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
|
|
|
@ -6,7 +6,7 @@ import Login from 'components/Login';
|
|||
import Container from 'components/Container';
|
||||
import { getData, LS_KEYS } from 'utils/storage/localStorage';
|
||||
import Card from 'react-bootstrap/Card';
|
||||
import { PAGES } from 'types';
|
||||
import { PAGES } from 'constants/pages';
|
||||
|
||||
export default function Home() {
|
||||
const router = useRouter();
|
||||
|
|
|
@ -7,7 +7,7 @@ import {
|
|||
setData,
|
||||
} from 'utils/storage/localStorage';
|
||||
import { useRouter } from 'next/router';
|
||||
import { KeyAttributes, PAGES } from 'types';
|
||||
import { PAGES } from 'constants/pages';
|
||||
import CryptoWorker, {
|
||||
decryptAndStoreToken,
|
||||
SaveKeyInSessionStore,
|
||||
|
@ -20,7 +20,7 @@ import { AppContext } from 'pages/_app';
|
|||
import LogoImg from 'components/LogoImg';
|
||||
import { logError } from 'utils/sentry';
|
||||
import { getKey, SESSION_KEYS } from 'utils/storage/sessionStorage';
|
||||
import { User } from 'services/userService';
|
||||
import { KeyAttributes, User } from 'types/user';
|
||||
const bip39 = require('bip39');
|
||||
// mobile client library only supports english.
|
||||
bip39.setDefaultWordlist('english');
|
||||
|
|
|
@ -6,7 +6,7 @@ import Container from 'components/Container';
|
|||
import EnteSpinner from 'components/EnteSpinner';
|
||||
import { getData, LS_KEYS } from 'utils/storage/localStorage';
|
||||
import SignUp from 'components/SignUp';
|
||||
import { PAGES } from 'types';
|
||||
import { PAGES } from 'constants/pages';
|
||||
|
||||
export default function SignUpPage() {
|
||||
const router = useRouter();
|
||||
|
|
|
@ -11,7 +11,7 @@ import LogoImg from 'components/LogoImg';
|
|||
import { logError } from 'utils/sentry';
|
||||
import { recoverTwoFactor, removeTwoFactor } from 'services/userService';
|
||||
import { AppContext, FLASH_MESSAGE_TYPE } from 'pages/_app';
|
||||
import { PAGES } from 'types';
|
||||
import { PAGES } from 'constants/pages';
|
||||
const bip39 = require('bip39');
|
||||
// mobile client library only supports english.
|
||||
bip39.setDefaultWordlist('english');
|
||||
|
|
|
@ -4,11 +4,7 @@ import { CodeBlock, FreeFlowText } from 'components/RecoveryKeyModal';
|
|||
import { DeadCenter } from 'pages/gallery';
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { Button, Card } from 'react-bootstrap';
|
||||
import {
|
||||
enableTwoFactor,
|
||||
setupTwoFactor,
|
||||
TwoFactorSecret,
|
||||
} from 'services/userService';
|
||||
import { enableTwoFactor, setupTwoFactor } from 'services/userService';
|
||||
import styled from 'styled-components';
|
||||
import constants from 'utils/strings/constants';
|
||||
import Container from 'components/Container';
|
||||
|
@ -18,7 +14,8 @@ import { B64EncryptionResult } from 'utils/crypto';
|
|||
import { encryptWithRecoveryKey } from 'utils/crypto';
|
||||
import { setData, LS_KEYS, getData } from 'utils/storage/localStorage';
|
||||
import { AppContext, FLASH_MESSAGE_TYPE } from 'pages/_app';
|
||||
import { PAGES } from 'types';
|
||||
import { PAGES } from 'constants/pages';
|
||||
import { TwoFactorSecret } from 'types/user';
|
||||
|
||||
enum SetupMode {
|
||||
QR_CODE,
|
||||
|
|
|
@ -4,8 +4,9 @@ import VerifyTwoFactor from 'components/VerifyTwoFactor';
|
|||
import router from 'next/router';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Button, Card } from 'react-bootstrap';
|
||||
import { logoutUser, User, verifyTwoFactor } from 'services/userService';
|
||||
import { PAGES } from 'types';
|
||||
import { logoutUser, verifyTwoFactor } from 'services/userService';
|
||||
import { PAGES } from 'constants/pages';
|
||||
import { User } from 'types/user';
|
||||
import { setData, LS_KEYS, getData } from 'utils/storage/localStorage';
|
||||
import constants from 'utils/strings/constants';
|
||||
|
||||
|
|
|
@ -12,8 +12,6 @@ import {
|
|||
getOtt,
|
||||
logoutUser,
|
||||
clearFiles,
|
||||
EmailVerificationResponse,
|
||||
User,
|
||||
putAttributes,
|
||||
} from 'services/userService';
|
||||
import { setIsFirstLogin } from 'utils/storage';
|
||||
|
@ -21,7 +19,8 @@ import SubmitButton from 'components/SubmitButton';
|
|||
import { clearKeys } from 'utils/storage/sessionStorage';
|
||||
import { AppContext } from 'pages/_app';
|
||||
import LogoImg from 'components/LogoImg';
|
||||
import { KeyAttributes, PAGES } from 'types';
|
||||
import { PAGES } from 'constants/pages';
|
||||
import { KeyAttributes, EmailVerificationResponse, User } from 'types/user';
|
||||
|
||||
interface formValues {
|
||||
ott: string;
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import { getEndpoint, getPaymentsUrl } from 'utils/common/apiUtil';
|
||||
import { getToken } from 'utils/common/key';
|
||||
import { setData, LS_KEYS } from 'utils/storage/localStorage';
|
||||
import { convertToHumanReadable } from 'utils/billingUtil';
|
||||
import { convertToHumanReadable } from 'utils/billing';
|
||||
import HTTPService from './HTTPService';
|
||||
import { logError } from 'utils/sentry';
|
||||
import { getPaymentToken } from './userService';
|
||||
import { Plan, Subscription } from 'types/billing';
|
||||
|
||||
const ENDPOINT = getEndpoint();
|
||||
|
||||
|
@ -12,31 +13,7 @@ enum PaymentActionType {
|
|||
Buy = 'buy',
|
||||
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 {
|
||||
public async getPlans(): Promise<Plan[]> {
|
||||
try {
|
||||
|
|
|
@ -6,79 +6,26 @@ import { getActualKey, getToken } from 'utils/common/key';
|
|||
import CryptoWorker from 'utils/crypto';
|
||||
import { SetDialogMessage } from 'components/MessageDialog';
|
||||
import constants from 'utils/strings/constants';
|
||||
import { getPublicKey, User } from './userService';
|
||||
import { getPublicKey } from './userService';
|
||||
import { B64EncryptionResult } from 'utils/crypto';
|
||||
import HTTPService from './HTTPService';
|
||||
import { File } from './fileService';
|
||||
import { EnteFile } from 'types/file';
|
||||
import { logError } from 'utils/sentry';
|
||||
import { CustomError } from 'utils/common/errorUtil';
|
||||
import { CustomError } from 'utils/error';
|
||||
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();
|
||||
|
||||
export enum CollectionType {
|
||||
folder = 'folder',
|
||||
favorites = 'favorites',
|
||||
album = 'album',
|
||||
}
|
||||
|
||||
const COLLECTION_TABLE = 'collections';
|
||||
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 (
|
||||
collection: Collection,
|
||||
|
@ -164,7 +111,7 @@ const getCollections = async (
|
|||
|
||||
export const getLocalCollections = async (): Promise<Collection[]> => {
|
||||
const collections: Collection[] =
|
||||
(await localForage.getItem(COLLECTIONS)) ?? [];
|
||||
(await localForage.getItem(COLLECTION_TABLE)) ?? [];
|
||||
return collections;
|
||||
};
|
||||
|
||||
|
@ -212,7 +159,7 @@ export const syncCollections = async () => {
|
|||
[],
|
||||
COLLECTION_SORT_BY.MODIFICATION_TIME
|
||||
);
|
||||
await localForage.setItem(COLLECTIONS, collections);
|
||||
await localForage.setItem(COLLECTION_TABLE, collections);
|
||||
await localForage.setItem(COLLECTION_UPDATION_TIME, updationTime);
|
||||
return collections;
|
||||
};
|
||||
|
@ -243,9 +190,9 @@ export const getCollection = async (
|
|||
|
||||
export const getCollectionsAndTheirLatestFile = (
|
||||
collections: Collection[],
|
||||
files: File[]
|
||||
files: EnteFile[]
|
||||
): CollectionAndItsLatestFile[] => {
|
||||
const latestFile = new Map<number, File>();
|
||||
const latestFile = new Map<number, EnteFile>();
|
||||
|
||||
files.forEach((file) => {
|
||||
if (!latestFile.has(file.collectionID)) {
|
||||
|
@ -263,7 +210,9 @@ export const getCollectionsAndTheirLatestFile = (
|
|||
return collectionsAndTheirLatestFile;
|
||||
};
|
||||
|
||||
export const getFavItemIds = async (files: File[]): Promise<Set<number>> => {
|
||||
export const getFavItemIds = async (
|
||||
files: EnteFile[]
|
||||
): Promise<Set<number>> => {
|
||||
const favCollection = await getFavCollection();
|
||||
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 {
|
||||
let favCollection = await getFavCollection();
|
||||
if (!favCollection) {
|
||||
|
@ -365,7 +314,7 @@ export const addToFavorites = async (file: File) => {
|
|||
CollectionType.favorites
|
||||
);
|
||||
const localCollections = await getLocalCollections();
|
||||
await localForage.setItem(COLLECTIONS, [
|
||||
await localForage.setItem(COLLECTION_TABLE, [
|
||||
...localCollections,
|
||||
favCollection,
|
||||
]);
|
||||
|
@ -376,7 +325,7 @@ export const addToFavorites = async (file: File) => {
|
|||
}
|
||||
};
|
||||
|
||||
export const removeFromFavorites = async (file: File) => {
|
||||
export const removeFromFavorites = async (file: EnteFile) => {
|
||||
try {
|
||||
const favCollection = await getFavCollection();
|
||||
if (!favCollection) {
|
||||
|
@ -390,7 +339,7 @@ export const removeFromFavorites = async (file: File) => {
|
|||
|
||||
export const addToCollection = async (
|
||||
collection: Collection,
|
||||
files: File[]
|
||||
files: EnteFile[]
|
||||
) => {
|
||||
try {
|
||||
const token = getToken();
|
||||
|
@ -417,7 +366,7 @@ export const addToCollection = async (
|
|||
|
||||
export const restoreToCollection = async (
|
||||
collection: Collection,
|
||||
files: File[]
|
||||
files: EnteFile[]
|
||||
) => {
|
||||
try {
|
||||
const token = getToken();
|
||||
|
@ -444,7 +393,7 @@ export const restoreToCollection = async (
|
|||
export const moveToCollection = async (
|
||||
fromCollectionID: number,
|
||||
toCollection: Collection,
|
||||
files: File[]
|
||||
files: EnteFile[]
|
||||
) => {
|
||||
try {
|
||||
const token = getToken();
|
||||
|
@ -472,7 +421,7 @@ export const moveToCollection = async (
|
|||
|
||||
const encryptWithNewCollectionKey = async (
|
||||
newCollection: Collection,
|
||||
files: File[]
|
||||
files: EnteFile[]
|
||||
): Promise<EncryptedFileKey[]> => {
|
||||
const fileKeysEncryptedWithNewCollection: EncryptedFileKey[] = [];
|
||||
const worker = await new CryptoWorker();
|
||||
|
@ -494,7 +443,7 @@ const encryptWithNewCollectionKey = async (
|
|||
};
|
||||
export const removeFromCollection = async (
|
||||
collection: Collection,
|
||||
files: File[]
|
||||
files: EnteFile[]
|
||||
) => {
|
||||
try {
|
||||
const token = getToken();
|
||||
|
@ -637,7 +586,7 @@ export const getFavCollection = async () => {
|
|||
|
||||
export const getNonEmptyCollections = (
|
||||
collections: Collection[],
|
||||
files: File[]
|
||||
files: EnteFile[]
|
||||
) => {
|
||||
const nonEmptyCollectionsIds = new Set<number>();
|
||||
for (const file of files) {
|
||||
|
|
|
@ -7,14 +7,16 @@ import {
|
|||
needsConversionForPreview,
|
||||
} from 'utils/file';
|
||||
import HTTPService from './HTTPService';
|
||||
import { File, FILE_TYPE } from './fileService';
|
||||
import { EnteFile } from 'types/file';
|
||||
|
||||
import { logError } from 'utils/sentry';
|
||||
import { FILE_TYPE } from 'constants/file';
|
||||
|
||||
class DownloadManager {
|
||||
private fileObjectUrlPromise = new Map<string, Promise<string>>();
|
||||
private thumbnailObjectUrlPromise = new Map<number, Promise<string>>();
|
||||
|
||||
public async getThumbnail(file: File) {
|
||||
public async getThumbnail(file: EnteFile) {
|
||||
try {
|
||||
const token = getToken();
|
||||
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(
|
||||
getThumbnailUrl(file.id),
|
||||
null,
|
||||
|
@ -68,7 +70,7 @@ class DownloadManager {
|
|||
return decrypted;
|
||||
};
|
||||
|
||||
getFile = async (file: File, forPreview = false) => {
|
||||
getFile = async (file: EnteFile, forPreview = false) => {
|
||||
const shouldBeConverted = forPreview && needsConversionForPreview(file);
|
||||
const fileKey = shouldBeConverted
|
||||
? `${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());
|
||||
}
|
||||
|
||||
async downloadFile(file: File) {
|
||||
async downloadFile(file: EnteFile) {
|
||||
const worker = await new CryptoWorker();
|
||||
const token = getToken();
|
||||
if (!token) {
|
||||
|
|
|
@ -23,80 +23,36 @@ import { retryAsyncFunction } from 'utils/network';
|
|||
import { logError } from 'utils/sentry';
|
||||
import { getData, LS_KEYS } from 'utils/storage/localStorage';
|
||||
import {
|
||||
Collection,
|
||||
getLocalCollections,
|
||||
getNonEmptyCollections,
|
||||
} from './collectionService';
|
||||
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 {
|
||||
fileNameWithoutExtension,
|
||||
generateStreamFromArrayBuffer,
|
||||
getFileExtension,
|
||||
mergeMetadata,
|
||||
TYPE_JPEG,
|
||||
TYPE_JPG,
|
||||
} 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>;
|
||||
export interface ExportProgress {
|
||||
current: number;
|
||||
total: number;
|
||||
}
|
||||
export interface ExportedCollectionPaths {
|
||||
[collectionID: number]: string;
|
||||
}
|
||||
export interface ExportStats {
|
||||
failed: number;
|
||||
success: number;
|
||||
}
|
||||
import { updateFileCreationDateInEXIF } from './upload/exifService';
|
||||
import { Metadata } from 'types/upload';
|
||||
import QueueProcessor from './queueProcessor';
|
||||
import { Collection } from 'types/collection';
|
||||
import {
|
||||
ExportProgress,
|
||||
CollectionIDPathMap,
|
||||
ExportRecord,
|
||||
} from 'types/export';
|
||||
import { User } from 'types/user';
|
||||
import { FILE_TYPE, TYPE_JPEG, TYPE_JPG } from 'constants/file';
|
||||
import { ExportType, ExportNotification, RecordType } from 'constants/export';
|
||||
|
||||
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';
|
||||
export const METADATA_FOLDER_NAME = 'metadata';
|
||||
|
||||
class ExportService {
|
||||
ElectronAPIs: any;
|
||||
|
@ -106,6 +62,7 @@ class ExportService {
|
|||
private stopExport: boolean = false;
|
||||
private pauseExport: boolean = false;
|
||||
private allElectronAPIsExist: boolean = false;
|
||||
private fileReader: FileReader = null;
|
||||
|
||||
constructor() {
|
||||
this.ElectronAPIs = runningInBrowser() && window['ElectronAPIs'];
|
||||
|
@ -139,7 +96,7 @@ class ExportService {
|
|||
}
|
||||
const user: User = getData(LS_KEYS.USER);
|
||||
|
||||
let filesToExport: File[];
|
||||
let filesToExport: EnteFile[];
|
||||
const localFiles = await getLocalFiles();
|
||||
const userPersonalFiles = localFiles
|
||||
.filter((file) => file.ownerID === user?.id)
|
||||
|
@ -210,7 +167,7 @@ class ExportService {
|
|||
}
|
||||
|
||||
async fileExporter(
|
||||
files: File[],
|
||||
files: EnteFile[],
|
||||
newCollections: Collection[],
|
||||
renamedCollections: Collection[],
|
||||
collectionIDPathMap: CollectionIDPathMap,
|
||||
|
@ -318,13 +275,17 @@ class ExportService {
|
|||
throw e;
|
||||
}
|
||||
}
|
||||
async addFilesQueuedRecord(folder: string, files: File[]) {
|
||||
async addFilesQueuedRecord(folder: string, files: EnteFile[]) {
|
||||
const exportRecord = await this.getExportRecord(folder);
|
||||
exportRecord.queuedFiles = files.map(getExportRecordFileUID);
|
||||
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 exportRecord = await this.getExportRecord(folder);
|
||||
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;
|
||||
const fileSaveName = getUniqueFileSaveName(
|
||||
collectionPath,
|
||||
|
@ -478,7 +439,11 @@ class ExportService {
|
|||
(fileType === TYPE_JPEG || fileType === TYPE_JPG)
|
||||
) {
|
||||
const fileBlob = await new Response(fileStream).blob();
|
||||
if (!this.fileReader) {
|
||||
this.fileReader = new FileReader();
|
||||
}
|
||||
const updatedFileBlob = await updateFileCreationDateInEXIF(
|
||||
this.fileReader,
|
||||
fileBlob,
|
||||
new Date(file.pubMagicMetadata.data.editedTime / 1000)
|
||||
);
|
||||
|
@ -498,7 +463,7 @@ class ExportService {
|
|||
|
||||
private async exportMotionPhoto(
|
||||
fileStream: ReadableStream<any>,
|
||||
file: File,
|
||||
file: EnteFile,
|
||||
collectionPath: string
|
||||
) {
|
||||
const fileBlob = await new Response(fileStream).blob();
|
||||
|
@ -544,7 +509,7 @@ class ExportService {
|
|||
private async saveMetadataFile(
|
||||
collectionFolderPath: string,
|
||||
fileSaveName: string,
|
||||
metadata: MetadataObject
|
||||
metadata: Metadata
|
||||
) {
|
||||
await this.ElectronAPIs.saveFileToDisk(
|
||||
getFileMetadataSavePath(collectionFolderPath, fileSaveName),
|
||||
|
@ -572,7 +537,7 @@ class ExportService {
|
|||
private async migrateExport(
|
||||
exportDir: string,
|
||||
collections: Collection[],
|
||||
allFiles: File[]
|
||||
allFiles: EnteFile[]
|
||||
) {
|
||||
const exportRecord = await this.getExportRecord(exportDir);
|
||||
const currentVersion = exportRecord?.version ?? 0;
|
||||
|
@ -633,7 +598,7 @@ class ExportService {
|
|||
`fileID_fileName` to newer `fileName(numbered)` format
|
||||
*/
|
||||
private async migrateFiles(
|
||||
files: File[],
|
||||
files: EnteFile[],
|
||||
collectionIDPathMap: Map<number, string>
|
||||
) {
|
||||
for (let file of files) {
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import { createFFmpeg, FFmpeg } from '@ffmpeg/ffmpeg';
|
||||
import { CustomError } from 'utils/common/errorUtil';
|
||||
import { CustomError } from 'utils/error';
|
||||
import { logError } from 'utils/sentry';
|
||||
import QueueProcessor from './upload/queueProcessor';
|
||||
import QueueProcessor from './queueProcessor';
|
||||
import { getUint8ArrayView } from './upload/readFileService';
|
||||
|
||||
class FFmpegService {
|
||||
private ffmpeg: FFmpeg = null;
|
||||
private isLoading = null;
|
||||
private fileReader: FileReader = null;
|
||||
|
||||
private generateThumbnailProcessor = new QueueProcessor<Uint8Array>(1);
|
||||
async init() {
|
||||
|
@ -29,11 +30,19 @@ class FFmpegService {
|
|||
if (!this.ffmpeg) {
|
||||
await this.init();
|
||||
}
|
||||
if (!this.fileReader) {
|
||||
this.fileReader = new FileReader();
|
||||
}
|
||||
if (this.isLoading) {
|
||||
await this.isLoading;
|
||||
}
|
||||
const response = this.generateThumbnailProcessor.queueUpRequest(
|
||||
generateThumbnailHelper.bind(null, this.ffmpeg, file)
|
||||
generateThumbnailHelper.bind(
|
||||
null,
|
||||
this.ffmpeg,
|
||||
this.fileReader,
|
||||
file
|
||||
)
|
||||
);
|
||||
try {
|
||||
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 {
|
||||
const inputFileName = `${Date.now().toString()}-${file.name}`;
|
||||
const thumbFileName = `${Date.now().toString()}-thumb.jpeg`;
|
||||
ffmpeg.FS(
|
||||
'writeFile',
|
||||
inputFileName,
|
||||
await getUint8ArrayView(new FileReader(), file)
|
||||
await getUint8ArrayView(reader, file)
|
||||
);
|
||||
let seekTime = 1.0;
|
||||
let thumb = null;
|
||||
|
|
|
@ -2,140 +2,24 @@ import { getEndpoint } from 'utils/common/apiUtil';
|
|||
import localForage from 'utils/storage/localForage';
|
||||
|
||||
import { getToken } from 'utils/common/key';
|
||||
import {
|
||||
DataStream,
|
||||
EncryptionResult,
|
||||
MetadataObject,
|
||||
} from './upload/uploadService';
|
||||
import { Collection } from './collectionService';
|
||||
import { EncryptionResult } from 'types/upload';
|
||||
import { Collection } from 'types/collection';
|
||||
import HTTPService from './HTTPService';
|
||||
import { logError } from 'utils/sentry';
|
||||
import { decryptFile, mergeMetadata, sortFiles } from 'utils/file';
|
||||
import CryptoWorker from 'utils/crypto';
|
||||
import { EnteFile, TrashRequest, UpdateMagicMetadataRequest } from 'types/file';
|
||||
|
||||
const ENDPOINT = getEndpoint();
|
||||
|
||||
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 () => {
|
||||
const files: Array<File> =
|
||||
(await localForage.getItem<File[]>(FILES_TABLE)) || [];
|
||||
const files: Array<EnteFile> =
|
||||
(await localForage.getItem<EnteFile[]>(FILES_TABLE)) || [];
|
||||
return files;
|
||||
};
|
||||
|
||||
export const setLocalFiles = async (files: File[]) => {
|
||||
export const setLocalFiles = async (files: EnteFile[]) => {
|
||||
await localForage.setItem(FILES_TABLE, files);
|
||||
};
|
||||
|
||||
|
@ -144,7 +28,7 @@ const getCollectionLastSyncTime = async (collection: Collection) =>
|
|||
|
||||
export const syncFiles = async (
|
||||
collections: Collection[],
|
||||
setFiles: (files: File[]) => void
|
||||
setFiles: (files: EnteFile[]) => void
|
||||
) => {
|
||||
const localFiles = await getLocalFiles();
|
||||
let files = await removeDeletedCollectionFiles(collections, localFiles);
|
||||
|
@ -163,7 +47,7 @@ export const syncFiles = async (
|
|||
const fetchedFiles =
|
||||
(await getFiles(collection, lastSyncTime, files, setFiles)) ?? [];
|
||||
files.push(...fetchedFiles);
|
||||
const latestVersionFiles = new Map<string, File>();
|
||||
const latestVersionFiles = new Map<string, EnteFile>();
|
||||
files.forEach((file) => {
|
||||
const uid = `${file.collectionID}-${file.id}`;
|
||||
if (
|
||||
|
@ -194,11 +78,11 @@ export const syncFiles = async (
|
|||
export const getFiles = async (
|
||||
collection: Collection,
|
||||
sinceTime: number,
|
||||
files: File[],
|
||||
setFiles: (files: File[]) => void
|
||||
): Promise<File[]> => {
|
||||
files: EnteFile[],
|
||||
setFiles: (files: EnteFile[]) => void
|
||||
): Promise<EnteFile[]> => {
|
||||
try {
|
||||
const decryptedFiles: File[] = [];
|
||||
const decryptedFiles: EnteFile[] = [];
|
||||
let time = sinceTime;
|
||||
let resp;
|
||||
do {
|
||||
|
@ -219,12 +103,12 @@ export const getFiles = async (
|
|||
|
||||
decryptedFiles.push(
|
||||
...(await Promise.all(
|
||||
resp.data.diff.map(async (file: File) => {
|
||||
resp.data.diff.map(async (file: EnteFile) => {
|
||||
if (!file.isDeleted) {
|
||||
file = await decryptFile(file, collection);
|
||||
}
|
||||
return file;
|
||||
}) as Promise<File>[]
|
||||
}) as Promise<EnteFile>[]
|
||||
))
|
||||
);
|
||||
|
||||
|
@ -249,7 +133,7 @@ export const getFiles = async (
|
|||
|
||||
const removeDeletedCollectionFiles = async (
|
||||
collections: Collection[],
|
||||
files: File[]
|
||||
files: EnteFile[]
|
||||
) => {
|
||||
const syncedCollectionIds = new Set<number>();
|
||||
for (const collection of collections) {
|
||||
|
@ -259,7 +143,7 @@ const removeDeletedCollectionFiles = async (
|
|||
return files;
|
||||
};
|
||||
|
||||
export const trashFiles = async (filesToTrash: File[]) => {
|
||||
export const trashFiles = async (filesToTrash: EnteFile[]) => {
|
||||
try {
|
||||
const token = getToken();
|
||||
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();
|
||||
if (!token) {
|
||||
return;
|
||||
|
@ -324,7 +208,7 @@ export const updateMagicMetadata = async (files: File[]) => {
|
|||
'X-Auth-Token': token,
|
||||
});
|
||||
return files.map(
|
||||
(file): File => ({
|
||||
(file): EnteFile => ({
|
||||
...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();
|
||||
if (!token) {
|
||||
return;
|
||||
|
@ -363,7 +247,7 @@ export const updatePublicMagicMetadata = async (files: File[]) => {
|
|||
}
|
||||
);
|
||||
return files.map(
|
||||
(file): File => ({
|
||||
(file): EnteFile => ({
|
||||
...file,
|
||||
pubMagicMetadata: {
|
||||
...file.pubMagicMetadata,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import downloadManager from 'services/downloadManager';
|
||||
import { fileAttribute, getLocalFiles } from 'services/fileService';
|
||||
import { getLocalFiles } from 'services/fileService';
|
||||
import { generateThumbnail } from 'services/upload/thumbnailService';
|
||||
import { getToken } from 'utils/common/key';
|
||||
import { logError } from 'utils/sentry';
|
||||
|
@ -7,10 +7,11 @@ import { getEndpoint } from 'utils/common/apiUtil';
|
|||
import HTTPService from 'services/HTTPService';
|
||||
import CryptoWorker from 'utils/crypto';
|
||||
import uploadHttpClient from 'services/upload/uploadHttpClient';
|
||||
import { EncryptionResult, UploadURL } from 'services/upload/uploadService';
|
||||
import { SetProgressTracker } from 'components/FixLargeThumbnail';
|
||||
import { getFileType } from './upload/readFileService';
|
||||
import { getLocalTrash, getTrashedFiles } from './trashService';
|
||||
import { EncryptionResult, UploadURL } from 'types/upload';
|
||||
import { fileAttribute } from 'types/file';
|
||||
|
||||
const ENDPOINT = getEndpoint();
|
||||
const REPLACE_THUMBNAIL_THRESHOLD = 500 * 1024; // 500KB
|
||||
|
@ -43,6 +44,7 @@ export async function replaceThumbnail(
|
|||
try {
|
||||
const token = getToken();
|
||||
const worker = await new CryptoWorker();
|
||||
const reader = new FileReader();
|
||||
const files = await getLocalFiles();
|
||||
const trash = await getLocalTrash();
|
||||
const trashFiles = getTrashedFiles(trash);
|
||||
|
@ -71,13 +73,14 @@ export async function replaceThumbnail(
|
|||
token,
|
||||
file
|
||||
);
|
||||
const dummyImageFile = new globalThis.File(
|
||||
const dummyImageFile = new File(
|
||||
[originalThumbnail],
|
||||
file.metadata.title
|
||||
);
|
||||
const fileTypeInfo = await getFileType(worker, dummyImageFile);
|
||||
const fileTypeInfo = await getFileType(reader, dummyImageFile);
|
||||
const { thumbnail: newThumbnail } = await generateThumbnail(
|
||||
worker,
|
||||
reader,
|
||||
dummyImageFile,
|
||||
fileTypeInfo
|
||||
);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { CustomError } from 'utils/common/errorUtil';
|
||||
import { CustomError } from 'utils/error';
|
||||
|
||||
interface RequestQueueItem {
|
||||
request: (canceller?: RequestCanceller) => Promise<any>;
|
||||
|
@ -43,7 +43,7 @@ export default class QueueProcessor<T> {
|
|||
return { promise, canceller };
|
||||
}
|
||||
|
||||
async pollQueue() {
|
||||
private async pollQueue() {
|
||||
if (this.requestInProcessing < this.maxParallelProcesses) {
|
||||
this.requestInProcessing++;
|
||||
await this.processQueue();
|
||||
|
@ -51,9 +51,9 @@ export default class QueueProcessor<T> {
|
|||
}
|
||||
}
|
||||
|
||||
public async processQueue() {
|
||||
private async processQueue() {
|
||||
while (this.requestQueue.length > 0) {
|
||||
const queueItem = this.requestQueue.pop();
|
||||
const queueItem = this.requestQueue.shift();
|
||||
let response = null;
|
||||
|
||||
if (queueItem.isCanceled.status) {
|
|
@ -1,20 +1,21 @@
|
|||
import * as chrono from 'chrono-node';
|
||||
import { getEndpoint } from 'utils/common/apiUtil';
|
||||
import { getToken } from 'utils/common/key';
|
||||
import { DateValue, Suggestion, SuggestionType } from 'components/SearchBar';
|
||||
import HTTPService from './HTTPService';
|
||||
import { Collection } from './collectionService';
|
||||
import { File } from './fileService';
|
||||
import { Collection } from 'types/collection';
|
||||
import { EnteFile } from 'types/file';
|
||||
|
||||
import { logError } from 'utils/sentry';
|
||||
import {
|
||||
DateValue,
|
||||
LocationSearchResponse,
|
||||
Suggestion,
|
||||
SuggestionType,
|
||||
} from 'types/search';
|
||||
|
||||
const ENDPOINT = getEndpoint();
|
||||
|
||||
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[] {
|
||||
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
|
||||
.map((file, idx) => ({
|
||||
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 { getToken } from 'utils/common/key';
|
||||
import { decryptFile, mergeMetadata, sortFiles } from 'utils/file';
|
||||
import { logError } from 'utils/sentry';
|
||||
import localForage from 'utils/storage/localForage';
|
||||
import { Collection, getCollection } from './collectionService';
|
||||
import { File } from './fileService';
|
||||
import { getCollection } from './collectionService';
|
||||
import { EnteFile } from 'types/file';
|
||||
|
||||
import HTTPService from './HTTPService';
|
||||
import { Trash, TrashItem } from 'types/trash';
|
||||
|
||||
const TRASH = 'file-trash';
|
||||
const TRASH_TIME = 'trash-time';
|
||||
|
@ -14,16 +17,6 @@ const DELETED_COLLECTION = 'deleted-collection';
|
|||
|
||||
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() {
|
||||
const trash = (await localForage.getItem<Trash>(TRASH)) || [];
|
||||
return trash;
|
||||
|
@ -52,7 +45,7 @@ async function getLastSyncTime() {
|
|||
export async function syncTrash(
|
||||
collections: Collection[],
|
||||
setFiles: SetFiles,
|
||||
files: File[]
|
||||
files: EnteFile[]
|
||||
): Promise<Trash> {
|
||||
const trash = await getLocalTrash();
|
||||
collections = [...collections, ...(await getLocalDeletedCollections())];
|
||||
|
@ -79,7 +72,7 @@ export const updateTrash = async (
|
|||
collections: Map<number, Collection>,
|
||||
sinceTime: number,
|
||||
setFiles: SetFiles,
|
||||
files: File[],
|
||||
files: EnteFile[],
|
||||
currentTrash: Trash
|
||||
): Promise<Trash> => {
|
||||
try {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { FIX_OPTIONS } from 'components/FixCreationTime';
|
||||
import { SetProgressTracker } from 'components/FixLargeThumbnail';
|
||||
import CryptoWorker from 'utils/crypto';
|
||||
import {
|
||||
changeFileCreationTime,
|
||||
getFileFromURL,
|
||||
|
@ -8,12 +7,15 @@ import {
|
|||
} from 'utils/file';
|
||||
import { logError } from 'utils/sentry';
|
||||
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 { getFileType } from './upload/readFileService';
|
||||
import { FILE_TYPE } from 'constants/file';
|
||||
|
||||
export async function updateCreationTimeWithExif(
|
||||
filesToBeUpdated: File[],
|
||||
filesToBeUpdated: EnteFile[],
|
||||
fixOption: FIX_OPTIONS,
|
||||
customTime: Date,
|
||||
setProgressTracker: SetProgressTracker
|
||||
|
@ -35,8 +37,8 @@ export async function updateCreationTimeWithExif(
|
|||
} else {
|
||||
const fileURL = await downloadManager.getFile(file);
|
||||
const fileObject = await getFileFromURL(fileURL);
|
||||
const worker = await new CryptoWorker();
|
||||
const fileTypeInfo = await getFileType(worker, fileObject);
|
||||
const reader = new FileReader();
|
||||
const fileTypeInfo = await getFileType(reader, fileObject);
|
||||
const exifData = await getRawExif(fileObject, fileTypeInfo);
|
||||
if (fixOption === FIX_OPTIONS.DATE_TIME_ORIGINAL) {
|
||||
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) {
|
||||
const { stream, chunkCount } = fileData;
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { NULL_LOCATION } from 'constants/upload';
|
||||
import { Location } from 'types/upload';
|
||||
import exifr from 'exifr';
|
||||
import piexif from 'piexifjs';
|
||||
import { FileTypeInfo } from 'types/upload';
|
||||
import { logError } from 'utils/sentry';
|
||||
import { NULL_LOCATION, Location } from './metadataService';
|
||||
import { FileTypeInfo } from './readFileService';
|
||||
|
||||
const EXIF_TAGS_NEEDED = [
|
||||
'DateTimeOriginal',
|
||||
|
@ -28,7 +29,7 @@ interface ParsedEXIFData {
|
|||
}
|
||||
|
||||
export async function getExifData(
|
||||
receivedFile: globalThis.File,
|
||||
receivedFile: File,
|
||||
fileTypeInfo: FileTypeInfo
|
||||
): Promise<ParsedEXIFData> {
|
||||
const nullExifData: ParsedEXIFData = {
|
||||
|
@ -56,12 +57,13 @@ export async function getExifData(
|
|||
}
|
||||
|
||||
export async function updateFileCreationDateInEXIF(
|
||||
reader: FileReader,
|
||||
fileBlob: Blob,
|
||||
updatedDate: Date
|
||||
) {
|
||||
try {
|
||||
const fileURL = URL.createObjectURL(fileBlob);
|
||||
let imageDataURL = await convertImageToDataURL(fileURL);
|
||||
let imageDataURL = await convertImageToDataURL(reader, fileURL);
|
||||
imageDataURL =
|
||||
'data:image/jpeg;base64' +
|
||||
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 dataUrl = await new Promise<string>((resolve) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => resolve(reader.result as string);
|
||||
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 { getExifData } from './exifService';
|
||||
import { FileTypeInfo } from './readFileService';
|
||||
import { MetadataObject } from './uploadService';
|
||||
import {
|
||||
Metadata,
|
||||
ParsedMetadataJSON,
|
||||
Location,
|
||||
FileTypeInfo,
|
||||
} from 'types/upload';
|
||||
import { NULL_LOCATION } from 'constants/upload';
|
||||
|
||||
export interface Location {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
}
|
||||
|
||||
export interface ParsedMetaDataJSON {
|
||||
creationTime: number;
|
||||
modificationTime: number;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
}
|
||||
interface ParsedMetaDataJSONWithTitle {
|
||||
interface ParsedMetadataJSONWithTitle {
|
||||
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,
|
||||
modificationTime: null,
|
||||
...NULL_LOCATION,
|
||||
};
|
||||
|
||||
export async function extractMetadata(
|
||||
receivedFile: globalThis.File,
|
||||
receivedFile: File,
|
||||
fileTypeInfo: FileTypeInfo
|
||||
) {
|
||||
let exifData = null;
|
||||
|
@ -37,7 +29,7 @@ export async function extractMetadata(
|
|||
exifData = await getExifData(receivedFile, fileTypeInfo);
|
||||
}
|
||||
|
||||
const extractedMetadata: MetadataObject = {
|
||||
const extractedMetadata: Metadata = {
|
||||
title: receivedFile.name,
|
||||
creationTime:
|
||||
exifData?.creationTime ?? receivedFile.lastModified * 1000,
|
||||
|
@ -52,10 +44,12 @@ export async function extractMetadata(
|
|||
export const getMetadataMapKey = (collectionID: number, title: string) =>
|
||||
`${collectionID}_${title}`;
|
||||
|
||||
export async function parseMetadataJSON(receivedFile: globalThis.File) {
|
||||
export async function parseMetadataJSON(
|
||||
reader: FileReader,
|
||||
receivedFile: File
|
||||
) {
|
||||
try {
|
||||
const metadataJSON: object = 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 = () => {
|
||||
|
@ -68,7 +62,7 @@ export async function parseMetadataJSON(receivedFile: globalThis.File) {
|
|||
reader.readAsText(receivedFile);
|
||||
});
|
||||
|
||||
const parsedMetaDataJSON: ParsedMetaDataJSON =
|
||||
const parsedMetadataJSON: ParsedMetadataJSON =
|
||||
NULL_PARSED_METADATA_JSON;
|
||||
if (!metadataJSON || !metadataJSON['title']) {
|
||||
return;
|
||||
|
@ -79,20 +73,20 @@ export async function parseMetadataJSON(receivedFile: globalThis.File) {
|
|||
metadataJSON['photoTakenTime'] &&
|
||||
metadataJSON['photoTakenTime']['timestamp']
|
||||
) {
|
||||
parsedMetaDataJSON.creationTime =
|
||||
parsedMetadataJSON.creationTime =
|
||||
metadataJSON['photoTakenTime']['timestamp'] * 1000000;
|
||||
} else if (
|
||||
metadataJSON['creationTime'] &&
|
||||
metadataJSON['creationTime']['timestamp']
|
||||
) {
|
||||
parsedMetaDataJSON.creationTime =
|
||||
parsedMetadataJSON.creationTime =
|
||||
metadataJSON['creationTime']['timestamp'] * 1000000;
|
||||
}
|
||||
if (
|
||||
metadataJSON['modificationTime'] &&
|
||||
metadataJSON['modificationTime']['timestamp']
|
||||
) {
|
||||
parsedMetaDataJSON.modificationTime =
|
||||
parsedMetadataJSON.modificationTime =
|
||||
metadataJSON['modificationTime']['timestamp'] * 1000000;
|
||||
}
|
||||
let locationData: Location = NULL_LOCATION;
|
||||
|
@ -110,10 +104,10 @@ export async function parseMetadataJSON(receivedFile: globalThis.File) {
|
|||
locationData = metadataJSON['geoDataExif'];
|
||||
}
|
||||
if (locationData !== null) {
|
||||
parsedMetaDataJSON.latitude = locationData.latitude;
|
||||
parsedMetaDataJSON.longitude = locationData.longitude;
|
||||
parsedMetadataJSON.latitude = locationData.latitude;
|
||||
parsedMetadataJSON.longitude = locationData.longitude;
|
||||
}
|
||||
return { title, parsedMetaDataJSON } as ParsedMetaDataJSONWithTitle;
|
||||
return { title, parsedMetadataJSON } as ParsedMetadataJSONWithTitle;
|
||||
} catch (e) {
|
||||
logError(e, 'parseMetadataJSON failed');
|
||||
// ignore
|
||||
|
|
|
@ -1,23 +1,18 @@
|
|||
import {
|
||||
FILE_CHUNKS_COMBINED_FOR_A_UPLOAD_PART,
|
||||
DataStream,
|
||||
} from './uploadService';
|
||||
RANDOM_PERCENTAGE_PROGRESS_FOR_PUT,
|
||||
} from 'constants/upload';
|
||||
import UIService from './uiService';
|
||||
import UploadHttpClient from './uploadHttpClient';
|
||||
import * as convert from 'xml-js';
|
||||
import UIService, { RANDOM_PERCENTAGE_PROGRESS_FOR_PUT } from './uiService';
|
||||
import { CustomError } from 'utils/common/errorUtil';
|
||||
import { CustomError } from 'utils/error';
|
||||
import { DataStream, MultipartUploadURLs } from 'types/upload';
|
||||
|
||||
interface PartEtag {
|
||||
PartNumber: number;
|
||||
ETag: string;
|
||||
}
|
||||
|
||||
export interface MultipartUploadURLs {
|
||||
objectKey: string;
|
||||
partURLs: string[];
|
||||
completeURL: string;
|
||||
}
|
||||
|
||||
function calculatePartCount(chunkCount: number) {
|
||||
const partCount = Math.ceil(
|
||||
chunkCount / FILE_CHUNKS_COMBINED_FOR_A_UPLOAD_PART
|
||||
|
|
|
@ -1,38 +1,35 @@
|
|||
import {
|
||||
FILE_TYPE,
|
||||
FORMAT_MISSED_BY_FILE_TYPE_LIB,
|
||||
} from 'services/fileService';
|
||||
import { FILE_TYPE } from 'constants/file';
|
||||
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 { CustomError } from 'utils/common/errorUtil';
|
||||
import { getFileExtension } from 'utils/file';
|
||||
import { CustomError } from 'utils/error';
|
||||
import { getFileExtension, splitFilenameAndExtension } from 'utils/file';
|
||||
import { FileTypeInfo } from 'types/upload';
|
||||
|
||||
const TYPE_VIDEO = 'video';
|
||||
const TYPE_IMAGE = 'image';
|
||||
const EDITED_FILE_SUFFIX = '-edited';
|
||||
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) {
|
||||
return getFileStream(worker, file, FILE_READER_CHUNK_SIZE);
|
||||
return getFileStream(reader, file, FILE_READER_CHUNK_SIZE);
|
||||
} else {
|
||||
return await worker.getUint8ArrayView(file);
|
||||
return await getUint8ArrayView(reader, file);
|
||||
}
|
||||
}
|
||||
|
||||
export interface FileTypeInfo {
|
||||
fileType: FILE_TYPE;
|
||||
exactType: string;
|
||||
}
|
||||
|
||||
export async function getFileType(
|
||||
worker,
|
||||
receivedFile: globalThis.File
|
||||
reader: FileReader,
|
||||
receivedFile: File
|
||||
): Promise<FileTypeInfo> {
|
||||
try {
|
||||
let fileType: FILE_TYPE;
|
||||
const mimeType = await getMimeType(worker, receivedFile);
|
||||
const mimeType = await getMimeType(reader, receivedFile);
|
||||
const typeParts = mimeType?.split('/');
|
||||
if (typeParts?.length !== 2) {
|
||||
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
|
||||
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;
|
||||
const [nameWithoutExtension, extension] = splitFilenameAndExtension(
|
||||
file.name
|
||||
);
|
||||
|
||||
const isEditedFile = file.name.endsWith(EDITED_FILE_SUFFIX);
|
||||
const isEditedFile = nameWithoutExtension.endsWith(EDITED_FILE_SUFFIX);
|
||||
if (isEditedFile) {
|
||||
originalName = file.name.slice(0, -1 * EDITED_FILE_SUFFIX.length);
|
||||
originalName = nameWithoutExtension.slice(
|
||||
0,
|
||||
-1 * EDITED_FILE_SUFFIX.length
|
||||
);
|
||||
} else {
|
||||
originalName = file.name;
|
||||
originalName = nameWithoutExtension;
|
||||
}
|
||||
if (extension) {
|
||||
originalName += '.' + extension;
|
||||
}
|
||||
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);
|
||||
return getMimeTypeFromBlob(worker, fileChunkBlob);
|
||||
return getMimeTypeFromBlob(reader, fileChunkBlob);
|
||||
}
|
||||
|
||||
export async function getMimeTypeFromBlob(worker, fileBlob: Blob) {
|
||||
export async function getMimeTypeFromBlob(reader: FileReader, fileBlob: Blob) {
|
||||
try {
|
||||
const initialFiledata = await worker.getUint8ArrayView(fileBlob);
|
||||
const initialFiledata = await getUint8ArrayView(reader, fileBlob);
|
||||
const result = await FileType.fromBuffer(initialFiledata);
|
||||
return result.mime;
|
||||
} catch (e) {
|
||||
|
@ -94,8 +100,8 @@ export async function getMimeTypeFromBlob(worker, fileBlob: Blob) {
|
|||
}
|
||||
}
|
||||
|
||||
function getFileStream(worker, file: globalThis.File, chunkSize: number) {
|
||||
const fileChunkReader = fileChunkReaderMaker(worker, file, chunkSize);
|
||||
function getFileStream(reader: FileReader, file: File, chunkSize: number) {
|
||||
const fileChunkReader = fileChunkReaderMaker(reader, file, chunkSize);
|
||||
|
||||
const stream = new ReadableStream<Uint8Array>({
|
||||
async pull(controller: ReadableStreamDefaultController) {
|
||||
|
@ -115,14 +121,14 @@ function getFileStream(worker, file: globalThis.File, chunkSize: number) {
|
|||
}
|
||||
|
||||
async function* fileChunkReaderMaker(
|
||||
worker,
|
||||
file: globalThis.File,
|
||||
reader: FileReader,
|
||||
file: File,
|
||||
chunkSize: number
|
||||
) {
|
||||
let offset = 0;
|
||||
while (offset < file.size) {
|
||||
const blob = file.slice(offset, chunkSize + offset);
|
||||
const fileChunk = await worker.getUint8ArrayView(blob);
|
||||
const fileChunk = await getUint8ArrayView(reader, blob);
|
||||
yield fileChunk;
|
||||
offset += chunkSize;
|
||||
}
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
import { FILE_TYPE } from 'services/fileService';
|
||||
import { CustomError, errorWithContext } from 'utils/common/errorUtil';
|
||||
import { FILE_TYPE } from 'constants/file';
|
||||
import { CustomError, errorWithContext } from 'utils/error';
|
||||
import { logError } from 'utils/sentry';
|
||||
import { BLACK_THUMBNAIL_BASE64 } from '../../../public/images/black-thumbnail-b64';
|
||||
import FFmpegService from 'services/ffmpegService';
|
||||
import { convertToHumanReadable } from 'utils/billingUtil';
|
||||
import { convertToHumanReadable } from 'utils/billing';
|
||||
import { isFileHEIC } from 'utils/file';
|
||||
import { FileTypeInfo } from './readFileService';
|
||||
import { FileTypeInfo } from 'types/upload';
|
||||
import { getUint8ArrayView } from './readFileService';
|
||||
|
||||
const MAX_THUMBNAIL_DIMENSION = 720;
|
||||
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 MAX_QUALITY = 0.7;
|
||||
|
||||
|
@ -22,7 +23,8 @@ interface Dimension {
|
|||
|
||||
export async function generateThumbnail(
|
||||
worker,
|
||||
file: globalThis.File,
|
||||
reader: FileReader,
|
||||
file: File,
|
||||
fileTypeInfo: FileTypeInfo
|
||||
): Promise<{ thumbnail: Uint8Array; hasStaticThumbnail: boolean }> {
|
||||
try {
|
||||
|
@ -50,7 +52,7 @@ export async function generateThumbnail(
|
|||
}
|
||||
}
|
||||
const thumbnailBlob = await thumbnailCanvasToBlob(canvas);
|
||||
thumbnail = await worker.getUint8ArrayView(thumbnailBlob);
|
||||
thumbnail = await getUint8ArrayView(reader, thumbnailBlob);
|
||||
if (thumbnail.length === 0) {
|
||||
throw Error('EMPTY THUMBNAIL');
|
||||
}
|
||||
|
@ -72,7 +74,7 @@ export async function generateThumbnail(
|
|||
|
||||
export async function generateImageThumbnail(
|
||||
worker,
|
||||
file: globalThis.File,
|
||||
file: File,
|
||||
isHEIC: boolean
|
||||
) {
|
||||
const canvas = document.createElement('canvas');
|
||||
|
@ -82,11 +84,7 @@ export async function generateImageThumbnail(
|
|||
let timeout = null;
|
||||
|
||||
if (isHEIC) {
|
||||
file = new globalThis.File(
|
||||
[await worker.convertHEIC2JPEG(file)],
|
||||
null,
|
||||
null
|
||||
);
|
||||
file = new File([await worker.convertHEIC2JPEG(file)], null, null);
|
||||
}
|
||||
let image = new Image();
|
||||
imageURL = URL.createObjectURL(file);
|
||||
|
@ -130,7 +128,7 @@ export async function generateImageThumbnail(
|
|||
return canvas;
|
||||
}
|
||||
|
||||
export async function generateVideoThumbnail(file: globalThis.File) {
|
||||
export async function generateVideoThumbnail(file: File) {
|
||||
const canvas = document.createElement('canvas');
|
||||
const canvasCTX = canvas.getContext('2d');
|
||||
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { ProgressUpdater } from 'components/pages/gallery/Upload';
|
||||
import { UPLOAD_STAGES } from './uploadManager';
|
||||
|
||||
export const RANDOM_PERCENTAGE_PROGRESS_FOR_PUT = () => 90 + 10 * Math.random();
|
||||
import {
|
||||
RANDOM_PERCENTAGE_PROGRESS_FOR_PUT,
|
||||
UPLOAD_STAGES,
|
||||
} from 'constants/upload';
|
||||
import { ProgressUpdater } from 'types/upload';
|
||||
|
||||
class UIService {
|
||||
private perFileProgress: number;
|
||||
|
|
|
@ -2,11 +2,10 @@ import HTTPService from 'services/HTTPService';
|
|||
import { getEndpoint } from 'utils/common/apiUtil';
|
||||
import { getToken } from 'utils/common/key';
|
||||
import { logError } from 'utils/sentry';
|
||||
import { UploadFile, UploadURL } from './uploadService';
|
||||
import { File } from '../fileService';
|
||||
import { CustomError, handleUploadError } from 'utils/common/errorUtil';
|
||||
import { EnteFile } from 'types/file';
|
||||
import { CustomError, handleUploadError } from 'utils/error';
|
||||
import { retryAsyncFunction } from 'utils/network';
|
||||
import { MultipartUploadURLs } from './multiPartUploadService';
|
||||
import { UploadFile, UploadURL, MultipartUploadURLs } from 'types/upload';
|
||||
|
||||
const ENDPOINT = getEndpoint();
|
||||
const MAX_URL_REQUESTS = 50;
|
||||
|
@ -14,7 +13,7 @@ const MAX_URL_REQUESTS = 50;
|
|||
class UploadHttpClient {
|
||||
private uploadURLFetchInProgress = null;
|
||||
|
||||
async uploadFile(uploadFile: UploadFile): Promise<File> {
|
||||
async uploadFile(uploadFile: UploadFile): Promise<EnteFile> {
|
||||
try {
|
||||
const token = getToken();
|
||||
if (!token) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { File, getLocalFiles, setLocalFiles } from '../fileService';
|
||||
import { Collection, getLocalCollections } from '../collectionService';
|
||||
import { SetFiles } from 'pages/gallery';
|
||||
import { getLocalFiles, setLocalFiles } from '../fileService';
|
||||
import { getLocalCollections } from '../collectionService';
|
||||
import { SetFiles } from 'types/gallery';
|
||||
import { ComlinkWorker, getDedicatedCryptoWorker } from 'utils/crypto';
|
||||
import {
|
||||
sortFilesIntoCollections,
|
||||
|
@ -8,52 +8,32 @@ import {
|
|||
removeUnnecessaryFileProps,
|
||||
} from 'utils/file';
|
||||
import { logError } from 'utils/sentry';
|
||||
import {
|
||||
getMetadataMapKey,
|
||||
ParsedMetaDataJSON,
|
||||
parseMetadataJSON,
|
||||
} from './metadataService';
|
||||
import { getMetadataMapKey, parseMetadataJSON } from './metadataService';
|
||||
import { segregateFiles } from 'utils/upload';
|
||||
import { ProgressUpdater } from 'components/pages/gallery/Upload';
|
||||
import uploader from './uploader';
|
||||
import UIService from './uiService';
|
||||
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 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 {
|
||||
private cryptoWorkers = new Array<ComlinkWorker>(MAX_CONCURRENT_UPLOADS);
|
||||
private metadataMap: MetadataMap;
|
||||
private filesToBeUploaded: FileWithCollection[];
|
||||
private failedFiles: FileWithCollection[];
|
||||
private existingFilesCollectionWise: Map<number, File[]>;
|
||||
private existingFiles: File[];
|
||||
private existingFilesCollectionWise: Map<number, EnteFile[]>;
|
||||
private existingFiles: EnteFile[];
|
||||
private setFiles: SetFiles;
|
||||
private collections: Map<number, Collection>;
|
||||
public initUploader(progressUpdater: ProgressUpdater, setFiles: SetFiles) {
|
||||
|
@ -64,7 +44,7 @@ class UploadManager {
|
|||
private async init(newCollections?: Collection[]) {
|
||||
this.filesToBeUploaded = [];
|
||||
this.failedFiles = [];
|
||||
this.metadataMap = new Map<string, ParsedMetaDataJSON>();
|
||||
this.metadataMap = new Map<string, ParsedMetadataJSON>();
|
||||
this.existingFiles = await getLocalFiles();
|
||||
this.existingFilesCollectionWise = sortFilesIntoCollections(
|
||||
this.existingFiles
|
||||
|
@ -112,20 +92,21 @@ class UploadManager {
|
|||
private async seedMetadataMap(metadataFiles: FileWithCollection[]) {
|
||||
try {
|
||||
UIService.reset(metadataFiles.length);
|
||||
|
||||
const reader = new FileReader();
|
||||
for (const fileWithCollection of metadataFiles) {
|
||||
const parsedMetaDataJSONWithTitle = await parseMetadataJSON(
|
||||
const parsedMetadataJSONWithTitle = await parseMetadataJSON(
|
||||
reader,
|
||||
fileWithCollection.file
|
||||
);
|
||||
if (parsedMetaDataJSONWithTitle) {
|
||||
const { title, parsedMetaDataJSON } =
|
||||
parsedMetaDataJSONWithTitle;
|
||||
if (parsedMetadataJSONWithTitle) {
|
||||
const { title, parsedMetadataJSON } =
|
||||
parsedMetadataJSONWithTitle;
|
||||
this.metadataMap.set(
|
||||
getMetadataMapKey(
|
||||
fileWithCollection.collectionID,
|
||||
title
|
||||
),
|
||||
{ ...parsedMetaDataJSON }
|
||||
{ ...parsedMetadataJSON }
|
||||
);
|
||||
UIService.increaseFileUploaded();
|
||||
}
|
||||
|
@ -157,14 +138,15 @@ class UploadManager {
|
|||
this.cryptoWorkers[i] = cryptoWorker;
|
||||
uploadProcesses.push(
|
||||
this.uploadNextFileInQueue(
|
||||
await new this.cryptoWorkers[i].comlink()
|
||||
await new this.cryptoWorkers[i].comlink(),
|
||||
new FileReader()
|
||||
)
|
||||
);
|
||||
}
|
||||
await Promise.all(uploadProcesses);
|
||||
}
|
||||
|
||||
private async uploadNextFileInQueue(worker: any) {
|
||||
private async uploadNextFileInQueue(worker: any, reader: FileReader) {
|
||||
while (this.filesToBeUploaded.length > 0) {
|
||||
const fileWithCollection = this.filesToBeUploaded.pop();
|
||||
const existingFilesInCollection =
|
||||
|
@ -177,6 +159,7 @@ class UploadManager {
|
|||
fileWithCollection.collection = collection;
|
||||
const { fileUploadResult, file } = await uploader(
|
||||
worker,
|
||||
reader,
|
||||
existingFilesInCollection,
|
||||
fileWithCollection
|
||||
);
|
||||
|
|
|
@ -1,99 +1,33 @@
|
|||
import { fileAttribute, FILE_TYPE } from '../fileService';
|
||||
import { Collection } from '../collectionService';
|
||||
import { Collection } from 'types/collection';
|
||||
import { logError } from 'utils/sentry';
|
||||
import UploadHttpClient from './uploadHttpClient';
|
||||
import {
|
||||
extractMetadata,
|
||||
getMetadataMapKey,
|
||||
ParsedMetaDataJSON,
|
||||
} from './metadataService';
|
||||
import { extractMetadata, getMetadataMapKey } from './metadataService';
|
||||
import { generateThumbnail } from './thumbnailService';
|
||||
import {
|
||||
getFileOriginalName,
|
||||
getFileData,
|
||||
FileTypeInfo,
|
||||
} from './readFileService';
|
||||
import { getFileOriginalName, getFileData } from './readFileService';
|
||||
import { encryptFiledata } from './encryptionService';
|
||||
import { ENCRYPTION_CHUNK_SIZE } from 'types';
|
||||
import { uploadStreamUsingMultipart } from './multiPartUploadService';
|
||||
import UIService from './uiService';
|
||||
import { handleUploadError } from 'utils/common/errorUtil';
|
||||
import { MetadataMap } from './uploadManager';
|
||||
|
||||
// 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 interface UploadURL {
|
||||
url: string;
|
||||
objectKey: string;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
import { handleUploadError } from 'utils/error';
|
||||
import {
|
||||
B64EncryptionResult,
|
||||
BackupedFile,
|
||||
EncryptedFile,
|
||||
EncryptionResult,
|
||||
FileInMemory,
|
||||
FileTypeInfo,
|
||||
FileWithMetadata,
|
||||
isDataStream,
|
||||
MetadataMap,
|
||||
Metadata,
|
||||
ParsedMetadataJSON,
|
||||
ProcessedFile,
|
||||
UploadFile,
|
||||
UploadURL,
|
||||
} from 'types/upload';
|
||||
|
||||
class UploadService {
|
||||
private uploadURLs: UploadURL[] = [];
|
||||
private metadataMap: Map<string, ParsedMetaDataJSON>;
|
||||
private metadataMap: Map<string, ParsedMetadataJSON>;
|
||||
private pendingUploadCount: number = 0;
|
||||
|
||||
async init(fileCount: number, metadataMap: MetadataMap) {
|
||||
|
@ -104,16 +38,18 @@ class UploadService {
|
|||
|
||||
async readFile(
|
||||
worker: any,
|
||||
rawFile: globalThis.File,
|
||||
reader: FileReader,
|
||||
rawFile: File,
|
||||
fileTypeInfo: FileTypeInfo
|
||||
): Promise<FileInMemory> {
|
||||
const { thumbnail, hasStaticThumbnail } = await generateThumbnail(
|
||||
worker,
|
||||
reader,
|
||||
rawFile,
|
||||
fileTypeInfo
|
||||
);
|
||||
|
||||
const filedata = await getFileData(worker, rawFile);
|
||||
const filedata = await getFileData(reader, rawFile);
|
||||
|
||||
return {
|
||||
filedata,
|
||||
|
@ -126,13 +62,13 @@ class UploadService {
|
|||
rawFile: File,
|
||||
collection: Collection,
|
||||
fileTypeInfo: FileTypeInfo
|
||||
): Promise<MetadataObject> {
|
||||
): Promise<Metadata> {
|
||||
const originalName = getFileOriginalName(rawFile);
|
||||
const googleMetadata =
|
||||
this.metadataMap.get(
|
||||
getMetadataMapKey(collection.id, originalName)
|
||||
) ?? {};
|
||||
const extractedMetadata: MetadataObject = await extractMetadata(
|
||||
const extractedMetadata: Metadata = await extractMetadata(
|
||||
rawFile,
|
||||
fileTypeInfo
|
||||
);
|
||||
|
|
|
@ -1,32 +1,37 @@
|
|||
import { File, FILE_TYPE } from 'services/fileService';
|
||||
import { EnteFile } from 'types/file';
|
||||
import { sleep } from 'utils/common';
|
||||
import { handleUploadError, CustomError } from 'utils/common/errorUtil';
|
||||
import { handleUploadError, CustomError } from 'utils/error';
|
||||
import { decryptFile } from 'utils/file';
|
||||
import { logError } from 'utils/sentry';
|
||||
import { fileAlreadyInCollection } from 'utils/upload';
|
||||
import UploadHttpClient from './uploadHttpClient';
|
||||
import UIService from './uiService';
|
||||
import { FileUploadResults, FileWithCollection } from './uploadManager';
|
||||
import UploadService, {
|
||||
import UploadService from './uploadService';
|
||||
import uploadService from './uploadService';
|
||||
import { getFileType } from './readFileService';
|
||||
import {
|
||||
BackupedFile,
|
||||
EncryptedFile,
|
||||
FileInMemory,
|
||||
FileTypeInfo,
|
||||
FileWithCollection,
|
||||
FileWithMetadata,
|
||||
MetadataObject,
|
||||
Metadata,
|
||||
UploadFile,
|
||||
} from './uploadService';
|
||||
import uploadService from './uploadService';
|
||||
import { FileTypeInfo, getFileType } from './readFileService';
|
||||
} from 'types/upload';
|
||||
import { FILE_TYPE } from 'constants/file';
|
||||
import { FileUploadResults } from 'constants/upload';
|
||||
|
||||
const TwoSecondInMillSeconds = 2000;
|
||||
const FIVE_GB_IN_BYTES = 5 * 1024 * 1024 * 1024;
|
||||
interface UploadResponse {
|
||||
fileUploadResult: FileUploadResults;
|
||||
file?: File;
|
||||
file?: EnteFile;
|
||||
}
|
||||
export default async function uploader(
|
||||
worker: any,
|
||||
existingFilesInCollection: File[],
|
||||
reader: FileReader,
|
||||
existingFilesInCollection: EnteFile[],
|
||||
fileWithCollection: FileWithCollection
|
||||
): Promise<UploadResponse> {
|
||||
const { file: rawFile, collection } = fileWithCollection;
|
||||
|
@ -35,7 +40,7 @@ export default async function uploader(
|
|||
|
||||
let file: FileInMemory = null;
|
||||
let encryptedFile: EncryptedFile = null;
|
||||
let metadata: MetadataObject = null;
|
||||
let metadata: Metadata = null;
|
||||
let fileTypeInfo: FileTypeInfo = null;
|
||||
let fileWithMetadata: FileWithMetadata = null;
|
||||
|
||||
|
@ -49,7 +54,7 @@ export default async function uploader(
|
|||
await sleep(TwoSecondInMillSeconds);
|
||||
return { fileUploadResult: FileUploadResults.TOO_LARGE };
|
||||
}
|
||||
fileTypeInfo = await getFileType(worker, rawFile);
|
||||
fileTypeInfo = await getFileType(reader, rawFile);
|
||||
if (fileTypeInfo.fileType === FILE_TYPE.OTHERS) {
|
||||
throw Error(CustomError.UNSUPPORTED_FILE_FORMAT);
|
||||
}
|
||||
|
@ -66,7 +71,12 @@ export default async function uploader(
|
|||
return { fileUploadResult: FileUploadResults.SKIPPED };
|
||||
}
|
||||
|
||||
file = await UploadService.readFile(worker, rawFile, fileTypeInfo);
|
||||
file = await UploadService.readFile(
|
||||
worker,
|
||||
reader,
|
||||
rawFile,
|
||||
fileTypeInfo
|
||||
);
|
||||
if (file.hasStaticThumbnail) {
|
||||
metadata.hasStaticThumbnail = true;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { KeyAttributes, PAGES } from 'types';
|
||||
import { PAGES } from 'constants/pages';
|
||||
import { getEndpoint } from 'utils/common/apiUtil';
|
||||
import { clearKeys } from 'utils/storage/sessionStorage';
|
||||
import router from 'next/router';
|
||||
|
@ -8,70 +8,20 @@ import { getToken } from 'utils/common/key';
|
|||
import HTTPService from './HTTPService';
|
||||
import { B64EncryptionResult } from 'utils/crypto';
|
||||
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 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) =>
|
||||
HTTPService.get(`${ENDPOINT}/users/ott`, {
|
||||
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 billingService, {
|
||||
FREE_PLAN,
|
||||
Plan,
|
||||
Subscription,
|
||||
} from 'services/billingService';
|
||||
import billingService from 'services/billingService';
|
||||
import { Plan, Subscription } from 'types/billing';
|
||||
import { NextRouter } from 'next/router';
|
||||
import { SetDialogMessage } from 'components/MessageDialog';
|
||||
import { SetLoading } from 'pages/gallery';
|
||||
import { getData, LS_KEYS } from './storage/localStorage';
|
||||
import { CustomError } from './common/errorUtil';
|
||||
import { logError } from './sentry';
|
||||
import { SetLoading } from 'types/gallery';
|
||||
import { getData, LS_KEYS } from '../storage/localStorage';
|
||||
import { CustomError } from '../error';
|
||||
import { logError } from '../sentry';
|
||||
|
||||
const STRIPE = 'stripe';
|
||||
const PAYMENT_PROVIDER_STRIPE = 'stripe';
|
||||
const FREE_PLAN = 'free';
|
||||
|
||||
enum FAILURE_REASON {
|
||||
AUTHENTICATION_FAILED = 'authentication_failed',
|
||||
|
@ -94,7 +92,7 @@ export function hasStripeSubscription(subscription: Subscription) {
|
|||
return (
|
||||
hasPaidSubscription(subscription) &&
|
||||
subscription.paymentProvider.length > 0 &&
|
||||
subscription.paymentProvider === STRIPE
|
||||
subscription.paymentProvider === PAYMENT_PROVIDER_STRIPE
|
||||
);
|
||||
}
|
||||
|
|
@ -1,20 +1,21 @@
|
|||
import {
|
||||
addToCollection,
|
||||
Collection,
|
||||
CollectionType,
|
||||
moveToCollection,
|
||||
removeFromCollection,
|
||||
restoreToCollection,
|
||||
} from 'services/collectionService';
|
||||
import { downloadFiles, getSelectedFiles } from 'utils/file';
|
||||
import { File, getLocalFiles } from 'services/fileService';
|
||||
import { CustomError } from 'utils/common/errorUtil';
|
||||
import { SelectedState } from 'pages/gallery';
|
||||
import { User } from 'services/userService';
|
||||
import { getLocalFiles } from 'services/fileService';
|
||||
import { EnteFile } from 'types/file';
|
||||
import { CustomError } from 'utils/error';
|
||||
import { SelectedState } from 'types/gallery';
|
||||
import { User } from 'types/user';
|
||||
import { getData, LS_KEYS } from 'utils/storage/localStorage';
|
||||
import { SetDialogMessage } from 'components/MessageDialog';
|
||||
import { logError } from 'utils/sentry';
|
||||
import constants from 'utils/strings/constants';
|
||||
import { Collection } from 'types/collection';
|
||||
import { CollectionType } from 'constants/collection';
|
||||
|
||||
export enum COLLECTION_OPS_TYPE {
|
||||
ADD,
|
||||
|
@ -26,7 +27,7 @@ export async function handleCollectionOps(
|
|||
type: COLLECTION_OPS_TYPE,
|
||||
setCollectionSelectorView: (value: boolean) => void,
|
||||
selected: SelectedState,
|
||||
files: File[],
|
||||
files: EnteFile[],
|
||||
setActiveCollection: (id: number) => void,
|
||||
collection: Collection
|
||||
) {
|
||||
|
|
|
@ -2,7 +2,7 @@ import { B64EncryptionResult } from 'utils/crypto';
|
|||
import CryptoWorker from 'utils/crypto';
|
||||
import { getData, LS_KEYS } from 'utils/storage/localStorage';
|
||||
import { getKey, SESSION_KEYS } from 'utils/storage/sessionStorage';
|
||||
import { CustomError } from './errorUtil';
|
||||
import { CustomError } from '../error';
|
||||
|
||||
export const getActualKey = async () => {
|
||||
try {
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { KEK } from 'pages/generate';
|
||||
import { KeyAttributes } from 'types';
|
||||
import { KEK, KeyAttributes } from 'types/user';
|
||||
import * as Comlink from 'comlink';
|
||||
import { runningInBrowser } from 'utils/common';
|
||||
import { SESSION_KEYS, setKey } from 'utils/storage/sessionStorage';
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import sodium, { StateAddress } from 'libsodium-wrappers';
|
||||
import { ENCRYPTION_CHUNK_SIZE } from 'types';
|
||||
import { ENCRYPTION_CHUNK_SIZE } from 'constants/crypto';
|
||||
|
||||
export async function decryptChaChaOneShot(
|
||||
data: Uint8Array,
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
import { Collection } from 'services/collectionService';
|
||||
import exportService, {
|
||||
CollectionIDPathMap,
|
||||
ExportRecord,
|
||||
METADATA_FOLDER_NAME,
|
||||
} from 'services/exportService';
|
||||
import { File } from 'services/fileService';
|
||||
import { MetadataObject } from 'services/upload/uploadService';
|
||||
import { formatDate, splitFilenameAndExtension } from 'utils/file';
|
||||
import { Collection } from 'types/collection';
|
||||
import exportService from 'services/exportService';
|
||||
import { CollectionIDPathMap, ExportRecord } from 'types/export';
|
||||
|
||||
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}`;
|
||||
|
||||
export const getExportQueuedFiles = (
|
||||
allFiles: File[],
|
||||
allFiles: EnteFile[],
|
||||
exportRecord: ExportRecord
|
||||
) => {
|
||||
const queuedFiles = new Set(exportRecord?.queuedFiles);
|
||||
|
@ -78,7 +78,7 @@ export const getCollectionsRenamedAfterLastExport = (
|
|||
};
|
||||
|
||||
export const getFilesUploadedAfterLastExport = (
|
||||
allFiles: File[],
|
||||
allFiles: EnteFile[],
|
||||
exportRecord: ExportRecord
|
||||
) => {
|
||||
const exportedFiles = new Set(exportRecord?.exportedFiles);
|
||||
|
@ -92,7 +92,7 @@ export const getFilesUploadedAfterLastExport = (
|
|||
};
|
||||
|
||||
export const getExportedFiles = (
|
||||
allFiles: File[],
|
||||
allFiles: EnteFile[],
|
||||
exportRecord: ExportRecord
|
||||
) => {
|
||||
const exportedFileIds = new Set(exportRecord?.exportedFiles);
|
||||
|
@ -106,7 +106,7 @@ export const getExportedFiles = (
|
|||
};
|
||||
|
||||
export const getExportFailedFiles = (
|
||||
allFiles: File[],
|
||||
allFiles: EnteFile[],
|
||||
exportRecord: ExportRecord
|
||||
) => {
|
||||
const failedFiles = new Set(exportRecord?.failedFiles);
|
||||
|
@ -127,7 +127,7 @@ export const dedupe = (files: any[]) => {
|
|||
|
||||
export const getGoogleLikeMetadataFile = (
|
||||
fileSaveName: string,
|
||||
metadata: MetadataObject
|
||||
metadata: Metadata
|
||||
) => {
|
||||
const creationTime = Math.floor(metadata.creationTime / 1000000);
|
||||
const modificationTime = Math.floor(
|
||||
|
@ -223,14 +223,17 @@ export const getOldCollectionFolderPath = (
|
|||
collection: Collection
|
||||
) => `${dir}/${collection.id}_${oldSanitizeName(collection.name)}`;
|
||||
|
||||
export const getOldFileSavePath = (collectionFolderPath: string, file: File) =>
|
||||
export const getOldFileSavePath = (
|
||||
collectionFolderPath: string,
|
||||
file: EnteFile
|
||||
) =>
|
||||
`${collectionFolderPath}/${file.id}_${oldSanitizeName(
|
||||
file.metadata.title
|
||||
)}`;
|
||||
|
||||
export const getOldFileMetadataSavePath = (
|
||||
collectionFolderPath: string,
|
||||
file: File
|
||||
file: EnteFile
|
||||
) =>
|
||||
`${collectionFolderPath}/${METADATA_FOLDER_NAME}/${
|
||||
file.id
|
||||
|
|
|
@ -1,28 +1,28 @@
|
|||
import { SelectedState } from 'pages/gallery';
|
||||
import { Collection } from 'services/collectionService';
|
||||
import { SelectedState } from 'types/gallery';
|
||||
import { Collection } from 'types/collection';
|
||||
import {
|
||||
File,
|
||||
EnteFile,
|
||||
fileAttribute,
|
||||
FILE_TYPE,
|
||||
MagicMetadataProps,
|
||||
NEW_MAGIC_METADATA,
|
||||
PublicMagicMetadataProps,
|
||||
VISIBILITY_STATE,
|
||||
} from 'services/fileService';
|
||||
} from 'types/file';
|
||||
import { decodeMotionPhoto } from 'services/motionPhotoService';
|
||||
import { getMimeTypeFromBlob } from 'services/upload/readFileService';
|
||||
import DownloadManager from 'services/downloadManager';
|
||||
import { logError } from 'utils/sentry';
|
||||
import { User } from 'services/userService';
|
||||
import { User } from 'types/user';
|
||||
import CryptoWorker from 'utils/crypto';
|
||||
import { getData, LS_KEYS } from 'utils/storage/localStorage';
|
||||
import { updateFileCreationDateInEXIF } from 'services/upload/exifService';
|
||||
|
||||
export const TYPE_HEIC = 'heic';
|
||||
export const TYPE_HEIF = 'heif';
|
||||
export const TYPE_JPEG = 'jpeg';
|
||||
export const TYPE_JPG = 'jpg';
|
||||
const UNSUPPORTED_FORMATS = ['flv', 'mkv', '3gp', 'avi', 'wmv'];
|
||||
import {
|
||||
TYPE_JPEG,
|
||||
TYPE_JPG,
|
||||
TYPE_HEIC,
|
||||
TYPE_HEIF,
|
||||
FILE_TYPE,
|
||||
VISIBILITY_STATE,
|
||||
} from 'constants/file';
|
||||
|
||||
export function downloadAsFile(filename: string, content: string) {
|
||||
const file = new Blob([content], {
|
||||
|
@ -40,7 +40,7 @@ export function downloadAsFile(filename: string, content: string) {
|
|||
a.remove();
|
||||
}
|
||||
|
||||
export async function downloadFile(file: File) {
|
||||
export async function downloadFile(file: EnteFile) {
|
||||
const a = document.createElement('a');
|
||||
a.style.display = 'none';
|
||||
let fileURL = await DownloadManager.getCachedOriginalFile(file);
|
||||
|
@ -60,6 +60,7 @@ export async function downloadFile(file: File) {
|
|||
let fileBlob = await (await fetch(fileURL)).blob();
|
||||
|
||||
fileBlob = await updateFileCreationDateInEXIF(
|
||||
new FileReader(),
|
||||
fileBlob,
|
||||
new Date(file.pubMagicMetadata.data.editedTime / 1000)
|
||||
);
|
||||
|
@ -89,8 +90,8 @@ export function isFileHEIC(mimeType: string) {
|
|||
);
|
||||
}
|
||||
|
||||
export function sortFilesIntoCollections(files: File[]) {
|
||||
const collectionWiseFiles = new Map<number, File[]>();
|
||||
export function sortFilesIntoCollections(files: EnteFile[]) {
|
||||
const collectionWiseFiles = new Map<number, EnteFile[]>();
|
||||
for (const file of files) {
|
||||
if (!collectionWiseFiles.has(file.collectionID)) {
|
||||
collectionWiseFiles.set(file.collectionID, []);
|
||||
|
@ -111,10 +112,10 @@ function getSelectedFileIds(selectedFiles: SelectedState) {
|
|||
}
|
||||
export function getSelectedFiles(
|
||||
selected: SelectedState,
|
||||
files: File[]
|
||||
): File[] {
|
||||
files: EnteFile[]
|
||||
): EnteFile[] {
|
||||
const filesIDs = new Set(getSelectedFileIds(selected));
|
||||
const selectedFiles: File[] = [];
|
||||
const selectedFiles: EnteFile[] = [];
|
||||
const foundFiles = new Set<number>();
|
||||
for (const file of files) {
|
||||
if (filesIDs.has(file.id) && !foundFiles.has(file.id)) {
|
||||
|
@ -125,14 +126,6 @@ export function getSelectedFiles(
|
|||
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) {
|
||||
const dateTimeFormat = new Intl.DateTimeFormat('en-IN', {
|
||||
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
|
||||
files = files.sort((a, b) => {
|
||||
if (!b.metadata?.modificationTime) {
|
||||
|
@ -209,7 +202,7 @@ export function sortFiles(files: File[]) {
|
|||
return files;
|
||||
}
|
||||
|
||||
export async function decryptFile(file: File, collection: Collection) {
|
||||
export async function decryptFile(file: EnteFile, collection: Collection) {
|
||||
try {
|
||||
const worker = await new CryptoWorker();
|
||||
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) => {
|
||||
delete file.src;
|
||||
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) {
|
||||
const originalName = fileNameWithoutExtension(file.metadata.title);
|
||||
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 worker = await new CryptoWorker();
|
||||
const reader = new FileReader();
|
||||
|
||||
const mimeType =
|
||||
(await getMimeTypeFromBlob(worker, fileBlob)) ?? typeFromExtension;
|
||||
(await getMimeTypeFromBlob(reader, fileBlob)) ?? typeFromExtension;
|
||||
if (isFileHEIC(mimeType)) {
|
||||
fileBlob = await worker.convertHEIC2JPEG(fileBlob);
|
||||
}
|
||||
return fileBlob;
|
||||
}
|
||||
|
||||
export function fileIsArchived(file: File) {
|
||||
export function fileIsArchived(file: EnteFile) {
|
||||
if (
|
||||
!file ||
|
||||
!file.magicMetadata ||
|
||||
|
@ -326,7 +320,7 @@ export function fileIsArchived(file: File) {
|
|||
}
|
||||
|
||||
export async function updateMagicMetadataProps(
|
||||
file: File,
|
||||
file: EnteFile,
|
||||
magicMetadataUpdates: MagicMetadataProps
|
||||
) {
|
||||
const worker = await new CryptoWorker();
|
||||
|
@ -362,7 +356,7 @@ export async function updateMagicMetadataProps(
|
|||
}
|
||||
}
|
||||
export async function updatePublicMagicMetadataProps(
|
||||
file: File,
|
||||
file: EnteFile,
|
||||
publicMetadataUpdates: PublicMagicMetadataProps
|
||||
) {
|
||||
const worker = await new CryptoWorker();
|
||||
|
@ -397,12 +391,12 @@ export async function updatePublicMagicMetadataProps(
|
|||
}
|
||||
|
||||
export async function changeFilesVisibility(
|
||||
files: File[],
|
||||
files: EnteFile[],
|
||||
selected: SelectedState,
|
||||
visibility: VISIBILITY_STATE
|
||||
) {
|
||||
const selectedFiles = getSelectedFiles(selected, files);
|
||||
const updatedFiles: File[] = [];
|
||||
const updatedFiles: EnteFile[] = [];
|
||||
for (const file of selectedFiles) {
|
||||
const updatedMagicMetadataProps: MagicMetadataProps = {
|
||||
visibility,
|
||||
|
@ -415,7 +409,10 @@ export async function changeFilesVisibility(
|
|||
return updatedFiles;
|
||||
}
|
||||
|
||||
export async function changeFileCreationTime(file: File, editedTime: number) {
|
||||
export async function changeFileCreationTime(
|
||||
file: EnteFile,
|
||||
editedTime: number
|
||||
) {
|
||||
const updatedPublicMagicMetadataProps: PublicMagicMetadataProps = {
|
||||
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 = {
|
||||
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);
|
||||
|
||||
if (!user?.id || !file?.ownerID) {
|
||||
|
@ -446,7 +443,7 @@ export function isSharedFile(file: File) {
|
|||
return file.ownerID !== user.id;
|
||||
}
|
||||
|
||||
export function mergeMetadata(files: File[]): File[] {
|
||||
export function mergeMetadata(files: EnteFile[]): EnteFile[] {
|
||||
return files.map((file) => ({
|
||||
...file,
|
||||
metadata: {
|
||||
|
@ -467,8 +464,8 @@ export function mergeMetadata(files: File[]): File[] {
|
|||
}
|
||||
|
||||
export function updateExistingFilePubMetadata(
|
||||
existingFile: File,
|
||||
updatedFile: File
|
||||
existingFile: EnteFile,
|
||||
updatedFile: EnteFile
|
||||
) {
|
||||
existingFile.pubMagicMetadata = updatedFile.pubMagicMetadata;
|
||||
existingFile.metadata = mergeMetadata([existingFile])[0].metadata;
|
||||
|
@ -476,11 +473,11 @@ export function updateExistingFilePubMetadata(
|
|||
|
||||
export async function getFileFromURL(fileURL: string) {
|
||||
const fileBlob = await (await fetch(fileURL)).blob();
|
||||
const fileFile = new globalThis.File([fileBlob], 'temp');
|
||||
const fileFile = new File([fileBlob], 'temp');
|
||||
return fileFile;
|
||||
}
|
||||
|
||||
export function getUniqueFiles(files: File[]) {
|
||||
export function getUniqueFiles(files: EnteFile[]) {
|
||||
const idSet = new Set<number>();
|
||||
return files.filter((file) => {
|
||||
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) ?? {};
|
||||
return getUniqueFiles(
|
||||
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) {
|
||||
try {
|
||||
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];
|
||||
if (
|
||||
file.metadata.fileType === FILE_TYPE.LIVE_PHOTO ||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { DateValue } from 'components/SearchBar';
|
||||
import { File } from 'services/fileService';
|
||||
import { Bbox } from 'services/searchService';
|
||||
import { EnteFile } from 'types/file';
|
||||
import { Bbox, DateValue } from 'types/search';
|
||||
|
||||
export function isInsideBox(
|
||||
file: { longitude: number; latitude: number },
|
||||
|
@ -36,7 +35,7 @@ export const isSameDay = (baseDate: DateValue) => (compareDate: Date) => {
|
|||
};
|
||||
|
||||
export function getFilesWithCreationDay(
|
||||
files: File[],
|
||||
files: EnteFile[],
|
||||
searchedDate: DateValue
|
||||
) {
|
||||
const isSearchedDate = isSameDay(searchedDate);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import * as Sentry from '@sentry/nextjs';
|
||||
import { errorWithContext } from 'utils/common/errorUtil';
|
||||
import { errorWithContext } from 'utils/error';
|
||||
import { getUserAnonymizedID } from 'utils/user';
|
||||
|
||||
export const logError = (
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { FileWithCollection } from 'services/upload/uploadManager';
|
||||
import { MetadataObject } from 'services/upload/uploadService';
|
||||
import { File } from 'services/fileService';
|
||||
import { FileWithCollection, Metadata } from 'types/upload';
|
||||
import { EnteFile } from 'types/file';
|
||||
|
||||
const TYPE_JSON = 'json';
|
||||
|
||||
export function fileAlreadyInCollection(
|
||||
existingFilesInCollection: File[],
|
||||
newFileMetadata: MetadataObject
|
||||
existingFilesInCollection: EnteFile[],
|
||||
newFileMetadata: Metadata
|
||||
): boolean {
|
||||
for (const existingFile of existingFilesInCollection) {
|
||||
if (areFilesSame(existingFile.metadata, newFileMetadata)) {
|
||||
|
@ -15,8 +15,8 @@ export function fileAlreadyInCollection(
|
|||
return false;
|
||||
}
|
||||
export function areFilesSame(
|
||||
existingFile: MetadataObject,
|
||||
newFile: MetadataObject
|
||||
existingFile: Metadata,
|
||||
newFile: Metadata
|
||||
): boolean {
|
||||
if (
|
||||
existingFile.fileType === newFile.fileType &&
|
||||
|
|
|
@ -149,30 +149,6 @@ export class Crypto {
|
|||
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) {
|
||||
return convertHEIC2JPEG(file);
|
||||
}
|
||||
|
|
|
@ -1,12 +1,7 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext",
|
||||
"webworker"
|
||||
],
|
||||
"lib": ["dom", "dom.iterable", "esnext", "webworker"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": false,
|
||||
|
@ -25,9 +20,8 @@
|
|||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"src/pages/index.tsx"
|
||||
"src/pages/index.tsx",
|
||||
"configUtil.js"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
"exclude": ["node_modules"]
|
||||
}
|
Loading…
Reference in a new issue