Merge pull request #282 from ente-io/master

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

44
SECURITY.md Normal file
View file

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

55
configUtil.js Normal file
View file

@ -0,0 +1,55 @@
const cp = require('child_process');
const { getIsSentryEnabled } = require('./sentryConfigUtil');
module.exports = {
COOP_COEP_HEADERS: {
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Embedder-Policy': 'require-corp',
},
WEB_SECURITY_HEADERS: {
'Strict-Transport-Security': ' max-age=63072000',
'X-Content-Type-Options': 'nosniff',
'X-Download-Options': 'noopen',
'X-Frame-Options': 'deny',
'X-XSS-Protection': '1; mode=block',
'Referrer-Policy': 'same-origin',
},
CSP_DIRECTIVES: {
'default-src': "'none'",
'img-src': "'self' blob:",
'style-src': "'self' 'unsafe-inline'",
'font-src ': "'self'; script-src 'self' 'unsafe-eval' blob:",
'connect-src':
"'self' https://*.ente.io data: blob: https://ente-prod-eu.s3.eu-central-003.backblazeb2.com ",
'base-uri ': "'self'",
'frame-ancestors': " 'none'",
'form-action': "'none'",
'report-uri': 'https://csp-reporter.ente.io',
'report-to': 'https://csp-reporter.ente.io',
},
WORKBOX_CONFIG: {
swSrc: 'src/serviceWorker.js',
exclude: [/manifest\.json$/i],
},
ALL_ROUTES: '/(.*)',
buildCSPHeader: (directives) => ({
'Content-Security-Policy-Report-Only': Object.entries(
directives
).reduce((acc, [key, value]) => acc + `${key} ${value};`, ''),
}),
convertToNextHeaderFormat: (headers) =>
Object.entries(headers).map(([key, value]) => ({ key, value })),
getGitSha: () =>
cp.execSync('git rev-parse --short HEAD', {
cwd: __dirname,
encoding: 'utf8',
}),
getIsSentryEnabled: getIsSentryEnabled,
};

View file

@ -4,31 +4,57 @@ const withBundleAnalyzer = require('@next/bundle-analyzer')({
const withWorkbox = require('@ente-io/next-with-workbox');
const { 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,
}
);

View file

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

View file

@ -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(),

View file

@ -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
View file

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

View file

@ -9,7 +9,7 @@ import { changeEmail, getOTTForEmailChange } from 'services/userService';
import styled from 'styled-components';
import { 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;

View file

@ -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;

View file

@ -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}

View file

@ -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';

View file

@ -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;

View file

@ -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%;

View file

@ -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));
};

View file

@ -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;

View file

@ -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;

View file

@ -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;
}

View file

@ -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);

View file

@ -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);

View file

@ -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;

View file

@ -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;

View file

@ -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';

View file

@ -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>;

View file

@ -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">

View file

@ -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';

View file

@ -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';

View file

@ -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[];

View file

@ -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;

View file

@ -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();
}
};

View file

@ -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;

View file

@ -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,
});
}
};

View file

@ -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;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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>();

View file

@ -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();

View file

@ -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);

View file

@ -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>();

View file

@ -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;

View file

@ -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();

View file

@ -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');

View file

@ -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();

View file

@ -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');

View file

@ -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,

View file

@ -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';

View file

@ -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;

View file

@ -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 {

View file

@ -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) {

View file

@ -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) {

View file

@ -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) {

View file

@ -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;

View file

@ -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,

View file

@ -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
);

View file

@ -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) {

View file

@ -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,

View file

@ -1,12 +1,15 @@
import { SetFiles } from 'pages/gallery';
import { SetFiles } from 'types/gallery';
import { Collection } from 'types/collection';
import { getEndpoint } from 'utils/common/apiUtil';
import { 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 {

View file

@ -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(

View file

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

View file

@ -1,8 +1,9 @@
import { NULL_LOCATION } from 'constants/upload';
import { Location } from 'types/upload';
import exifr from 'exifr';
import 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);
});

View file

@ -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

View file

@ -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

View file

@ -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;
}

View file

@ -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');

View file

@ -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;

View file

@ -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) {

View file

@ -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
);

View file

@ -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
);

View file

@ -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;
}

View file

@ -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,

View file

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

View file

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

View file

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

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

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

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

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

View file

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

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

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

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

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

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

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

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

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

View file

@ -1,17 +1,15 @@
import constants from 'utils/strings/constants';
import 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
);
}

View file

@ -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
) {

View file

@ -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 {

View file

@ -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';

View file

@ -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,

View file

@ -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

View file

@ -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 ||

View file

@ -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);

View file

@ -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 = (

View file

@ -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 &&

View file

@ -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);
}

View 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"]
}