diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..1642c8307 --- /dev/null +++ b/SECURITY.md @@ -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! diff --git a/configUtil.js b/configUtil.js new file mode 100644 index 000000000..438114a99 --- /dev/null +++ b/configUtil.js @@ -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, +}; diff --git a/next.config.js b/next.config.js index eb784081a..056b654ad 100644 --- a/next.config.js +++ b/next.config.js @@ -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, + } + ); diff --git a/package.json b/package.json index f06fcb6f2..3fe166638 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bada-frame", - "version": "0.4.0", + "version": "0.4.2", "private": true, "scripts": { "dev": "next dev", diff --git a/sentry.client.config.js b/sentry.client.config.js index 25276ba34..09e2fe20d 100644 --- a/sentry.client.config.js +++ b/sentry.client.config.js @@ -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(), diff --git a/sentry.server.config.js b/sentry.server.config.js index 33af4278b..5d8714c99 100644 --- a/sentry.server.config.js +++ b/sentry.server.config.js @@ -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, }); diff --git a/sentryConfigUtil.js b/sentryConfigUtil.js new file mode 100644 index 000000000..1b19f2f6b --- /dev/null +++ b/sentryConfigUtil.js @@ -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; +}; diff --git a/src/components/ChangeEmail.tsx b/src/components/ChangeEmail.tsx index 328ae1182..0eb94a694 100644 --- a/src/components/ChangeEmail.tsx +++ b/src/components/ChangeEmail.tsx @@ -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; diff --git a/src/components/CollectionShare.tsx b/src/components/CollectionShare.tsx index 25051f266..3aabd3b0b 100644 --- a/src/components/CollectionShare.tsx +++ b/src/components/CollectionShare.tsx @@ -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; diff --git a/src/components/EnteDateTimePicker.tsx b/src/components/EnteDateTimePicker.tsx index de4d37f71..164417fd9 100644 --- a/src/components/EnteDateTimePicker.tsx +++ b/src/components/EnteDateTimePicker.tsx @@ -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) => ( { - console.log(values); startFix(Number(values.option), new Date(values.customTime)); }; diff --git a/src/components/Login.tsx b/src/components/Login.tsx index ada00fa11..1309d2b97 100644 --- a/src/components/Login.tsx +++ b/src/components/Login.tsx @@ -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; diff --git a/src/components/PhotoFrame.tsx b/src/components/PhotoFrame.tsx index b91ea03ca..2532c6aeb 100644 --- a/src/components/PhotoFrame.tsx +++ b/src/components/PhotoFrame.tsx @@ -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; favItemIds: Set; @@ -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) => ( 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; diff --git a/src/components/PhotoList.tsx b/src/components/PhotoList.tsx index f8dce46ab..0c34407ba 100644 --- a/src/components/PhotoList.tsx +++ b/src/components/PhotoList.tsx @@ -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; } diff --git a/src/components/PhotoSwipe/PhotoSwipe.tsx b/src/components/PhotoSwipe/PhotoSwipe.tsx index 58fd6852b..22d922c75 100644 --- a/src/components/PhotoSwipe/PhotoSwipe.tsx +++ b/src/components/PhotoSwipe/PhotoSwipe.tsx @@ -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 = () => ( + +); 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; @@ -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({ {isInEditMode ? ( - + {loading ? ( + + ) : ( + + )} @@ -239,12 +255,7 @@ const FileNameEditForm = ({ filename, saveEdits, discardEdits, extension }) => { {loading ? ( - + ) : ( )} @@ -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(null); + const [metadata, setMetaData] = useState(null); const [exif, setExif] = useState(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); diff --git a/src/components/SearchBar.tsx b/src/components/SearchBar.tsx index f3f42615d..b47680be9 100644 --- a/src/components/SearchBar.tsx +++ b/src/components/SearchBar.tsx @@ -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(null); diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 8d7358048..29ae3390f 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -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; diff --git a/src/components/SignUp.tsx b/src/components/SignUp.tsx index 4d46dea42..8a2399f86 100644 --- a/src/components/SignUp.tsx +++ b/src/components/SignUp.tsx @@ -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; diff --git a/src/components/TwoFactorModal.tsx b/src/components/TwoFactorModal.tsx index db4421f9d..c7597fac2 100644 --- a/src/components/TwoFactorModal.tsx +++ b/src/components/TwoFactorModal.tsx @@ -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'; diff --git a/src/components/pages/gallery/CollectionOptions.tsx b/src/components/pages/gallery/CollectionOptions.tsx index e34be88f4..dd9ce740a 100644 --- a/src/components/pages/gallery/CollectionOptions.tsx +++ b/src/components/pages/gallery/CollectionOptions.tsx @@ -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; diff --git a/src/components/pages/gallery/CollectionSelector.tsx b/src/components/pages/gallery/CollectionSelector.tsx index b5d691eee..ad39e3782 100644 --- a/src/components/pages/gallery/CollectionSelector.tsx +++ b/src/components/pages/gallery/CollectionSelector.tsx @@ -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({ {}} + onSelect={() => {}} forcedEnable /> diff --git a/src/components/pages/gallery/CollectionSort.tsx b/src/components/pages/gallery/CollectionSort.tsx index 9d0c2a68c..7f0c52b6b 100644 --- a/src/components/pages/gallery/CollectionSort.tsx +++ b/src/components/pages/gallery/CollectionSort.tsx @@ -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'; diff --git a/src/components/pages/gallery/CollectionSortOptions.tsx b/src/components/pages/gallery/CollectionSortOptions.tsx index aff226140..a209c8998 100644 --- a/src/components/pages/gallery/CollectionSortOptions.tsx +++ b/src/components/pages/gallery/CollectionSortOptions.tsx @@ -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'; diff --git a/src/components/pages/gallery/Collections.tsx b/src/components/pages/gallery/Collections.tsx index b67e05ccc..e0380c7f1 100644 --- a/src/components/pages/gallery/Collections.tsx +++ b/src/components/pages/gallery/Collections.tsx @@ -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[]; diff --git a/src/components/pages/gallery/PlanSelector.tsx b/src/components/pages/gallery/PlanSelector.tsx index 4894b44f4..48156e7f5 100644 --- a/src/components/pages/gallery/PlanSelector.tsx +++ b/src/components/pages/gallery/PlanSelector.tsx @@ -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; diff --git a/src/components/pages/gallery/PreviewCard.tsx b/src/components/pages/gallery/PreviewCard.tsx index ddb4f7de0..54b09410e 100644 --- a/src/components/pages/gallery/PreviewCard.tsx +++ b/src/components/pages/gallery/PreviewCard.tsx @@ -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 = (e) => { if (isRangeSelectActive) { onRangeSelect?.(); + } else { + onSelect(e.target.checked); } - onSelect?.(e.target.checked); }; const longPressCallback = () => { onSelect(!selected); }; const handleHover = () => { - if (selectOnClick) { + if (isRangeSelectActive) { onHover(); } }; diff --git a/src/components/pages/gallery/SelectedFileOptions.tsx b/src/components/pages/gallery/SelectedFileOptions.tsx index 7985e5a1c..c661f589f 100644 --- a/src/components/pages/gallery/SelectedFileOptions.tsx +++ b/src/components/pages/gallery/SelectedFileOptions.tsx @@ -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; diff --git a/src/components/pages/gallery/Upload.tsx b/src/components/pages/gallery/Upload.tsx index df694188b..11a2e41ee 100644 --- a/src/components/pages/gallery/Upload.tsx +++ b/src/components/pages/gallery/Upload.tsx @@ -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; 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>; - setFileCounter: React.Dispatch< - React.SetStateAction<{ - finished: number; - total: number; - }> - >; - setUploadStage: React.Dispatch>; - setFileProgress: React.Dispatch>>; - setUploadResult: React.Dispatch>>; -} 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(); + const collectionWiseFiles = new Map(); 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(); + let collectionWiseFiles = new Map(); 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, + }); } }; diff --git a/src/components/pages/gallery/UploadProgress.tsx b/src/components/pages/gallery/UploadProgress.tsx index d92e9685f..38052ff16 100644 --- a/src/components/pages/gallery/UploadProgress.tsx +++ b/src/components/pages/gallery/UploadProgress.tsx @@ -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; diff --git a/src/constants/collection/index.ts b/src/constants/collection/index.ts new file mode 100644 index 000000000..afb10f0e4 --- /dev/null +++ b/src/constants/collection/index.ts @@ -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, +} diff --git a/src/constants/crypto/index.ts b/src/constants/crypto/index.ts new file mode 100644 index 000000000..9226ed874 --- /dev/null +++ b/src/constants/crypto/index.ts @@ -0,0 +1 @@ +export const ENCRYPTION_CHUNK_SIZE = 4 * 1024 * 1024; diff --git a/src/constants/export/index.ts b/src/constants/export/index.ts new file mode 100644 index 000000000..4dcdc8f71 --- /dev/null +++ b/src/constants/export/index.ts @@ -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, +} diff --git a/src/constants/file/index.ts b/src/constants/file/index.ts new file mode 100644 index 000000000..36ed503b8 --- /dev/null +++ b/src/constants/file/index.ts @@ -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, +} diff --git a/src/constants/gallery/index.ts b/src/constants/gallery/index.ts new file mode 100644 index 000000000..be5c385f4 --- /dev/null +++ b/src/constants/gallery/index.ts @@ -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; diff --git a/src/constants/pages/index.ts b/src/constants/pages/index.ts new file mode 100644 index 000000000..aad2a4831 --- /dev/null +++ b/src/constants/pages/index.ts @@ -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 = '/', +} diff --git a/src/constants/sentry/index.ts b/src/constants/sentry/index.ts new file mode 100644 index 000000000..5ccfcebe1 --- /dev/null +++ b/src/constants/sentry/index.ts @@ -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'; diff --git a/src/constants/upload/index.ts b/src/constants/upload/index.ts new file mode 100644 index 000000000..6a35e8203 --- /dev/null +++ b/src/constants/upload/index.ts @@ -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, +} diff --git a/src/constants/user/index.ts b/src/constants/user/index.ts new file mode 100644 index 000000000..12221f4e6 --- /dev/null +++ b/src/constants/user/index.ts @@ -0,0 +1 @@ +export const FIX_CREATION_TIME_VISIBLE_TO_USER_IDS = [1, 125, 243, 341]; diff --git a/src/pages/change-email/index.tsx b/src/pages/change-email/index.tsx index dabe9f773..f81b971a6 100644 --- a/src/pages/change-email/index.tsx +++ b/src/pages/change-email/index.tsx @@ -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(''); diff --git a/src/pages/change-password/index.tsx b/src/pages/change-password/index.tsx index 1b851342b..a9137edfe 100644 --- a/src/pages/change-password/index.tsx +++ b/src/pages/change-password/index.tsx @@ -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(); diff --git a/src/pages/credentials/index.tsx b/src/pages/credentials/index.tsx index fff9be33f..86d44ce5f 100644 --- a/src/pages/credentials/index.tsx +++ b/src/pages/credentials/index.tsx @@ -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(); diff --git a/src/pages/gallery/index.tsx b/src/pages/gallery/index.tsx index df8bf8982..cfbfdfc1a 100644 --- a/src/pages/gallery/index.tsx +++ b/src/pages/gallery/index.tsx @@ -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>; -export type SetCollections = React.Dispatch>; -export type SetLoading = React.Dispatch>; -export type setSearchStats = React.Dispatch>; - -export type Search = { - date?: DateValue; - location?: Bbox; - fileIndex?: number; -}; -export interface SearchStats { - resultCount: number; - timeTaken: number; -} - -type GalleryContextType = { - thumbs: Map; - files: Map; - showPlanSelectorModal: () => void; - setActiveCollection: (collection: number) => void; - syncWithRemote: (force?: boolean, silent?: boolean) => Promise; -}; - const defaultGalleryContext: GalleryContextType = { thumbs: new Map(), files: new Map(), @@ -159,7 +132,7 @@ export default function Gallery() { const [collections, setCollections] = useState([]); const [collectionsAndTheirLatestFile, setCollectionsAndTheirLatestFile] = useState([]); - const [files, setFiles] = useState(null); + const [files, setFiles] = useState(null); const [favItemIds, setFavItemIds] = useState>(); const [bannerMessage, setBannerMessage] = useState( 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); diff --git a/src/pages/generate/index.tsx b/src/pages/generate/index.tsx index 62d6aa6aa..659fc2da9 100644 --- a/src/pages/generate/index.tsx +++ b/src/pages/generate/index.tsx @@ -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(); diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 20e1c7161..4d80828c5 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -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; diff --git a/src/pages/login/index.tsx b/src/pages/login/index.tsx index 3cc3b17ac..ef056f426 100644 --- a/src/pages/login/index.tsx +++ b/src/pages/login/index.tsx @@ -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(); diff --git a/src/pages/recover/index.tsx b/src/pages/recover/index.tsx index fdfe7fbb8..a6a7deee9 100644 --- a/src/pages/recover/index.tsx +++ b/src/pages/recover/index.tsx @@ -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'); diff --git a/src/pages/signup/index.tsx b/src/pages/signup/index.tsx index 3f88c1955..78d2e5e27 100644 --- a/src/pages/signup/index.tsx +++ b/src/pages/signup/index.tsx @@ -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(); diff --git a/src/pages/two-factor/recover/index.tsx b/src/pages/two-factor/recover/index.tsx index 30f599eeb..867f4983b 100644 --- a/src/pages/two-factor/recover/index.tsx +++ b/src/pages/two-factor/recover/index.tsx @@ -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'); diff --git a/src/pages/two-factor/setup/index.tsx b/src/pages/two-factor/setup/index.tsx index 85a9df614..8fe4d432e 100644 --- a/src/pages/two-factor/setup/index.tsx +++ b/src/pages/two-factor/setup/index.tsx @@ -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, diff --git a/src/pages/two-factor/verify/index.tsx b/src/pages/two-factor/verify/index.tsx index 4c70ea593..3ccdb14ee 100644 --- a/src/pages/two-factor/verify/index.tsx +++ b/src/pages/two-factor/verify/index.tsx @@ -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'; diff --git a/src/pages/verify/index.tsx b/src/pages/verify/index.tsx index 3a8324f79..d12563012 100644 --- a/src/pages/verify/index.tsx +++ b/src/pages/verify/index.tsx @@ -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; diff --git a/src/services/billingService.ts b/src/services/billingService.ts index 7c09d5fba..93f8e7b81 100644 --- a/src/services/billingService.ts +++ b/src/services/billingService.ts @@ -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 { try { diff --git a/src/services/collectionService.ts b/src/services/collectionService.ts index 3a44d8936..cd63c2e56 100644 --- a/src/services/collectionService.ts +++ b/src/services/collectionService.ts @@ -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 => { 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(); + const latestFile = new Map(); files.forEach((file) => { if (!latestFile.has(file.collectionID)) { @@ -263,7 +210,9 @@ export const getCollectionsAndTheirLatestFile = ( return collectionsAndTheirLatestFile; }; -export const getFavItemIds = async (files: File[]): Promise> => { +export const getFavItemIds = async ( + files: EnteFile[] +): Promise> => { 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 => { 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(); for (const file of files) { diff --git a/src/services/downloadManager.ts b/src/services/downloadManager.ts index 5e4c4b856..e49ca0ff7 100644 --- a/src/services/downloadManager.ts +++ b/src/services/downloadManager.ts @@ -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>(); private thumbnailObjectUrlPromise = new Map>(); - 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) { diff --git a/src/services/exportService.ts b/src/services/exportService.ts index e6e8a1425..3e413576a 100644 --- a/src/services/exportService.ts +++ b/src/services/exportService.ts @@ -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; -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, - 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 ) { for (let file of files) { diff --git a/src/services/ffmpegService.ts b/src/services/ffmpegService.ts index 1184593b4..3914cfea2 100644 --- a/src/services/ffmpegService.ts +++ b/src/services/ffmpegService.ts @@ -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(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; diff --git a/src/services/fileService.ts b/src/services/fileService.ts index 0c0b2861d..8c9f0ece3 100644 --- a/src/services/fileService.ts +++ b/src/services/fileService.ts @@ -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; -} - -export interface EncryptedMagicMetadataCore - extends Omit { - data: string; -} - -export interface MagicMetadataProps { - visibility?: VISIBILITY_STATE; -} - -export interface MagicMetadata extends Omit { - data: MagicMetadataProps; -} - -export interface PublicMagicMetadataProps { - editedTime?: number; - editedName?: string; -} - -export interface PublicMagicMetadata extends Omit { - 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 = - (await localForage.getItem(FILES_TABLE)) || []; + const files: Array = + (await localForage.getItem(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(); + const latestVersionFiles = new Map(); 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 => { + files: EnteFile[], + setFiles: (files: EnteFile[]) => void +): Promise => { 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[] + }) as Promise[] )) ); @@ -249,7 +133,7 @@ export const getFiles = async ( const removeDeletedCollectionFiles = async ( collections: Collection[], - files: File[] + files: EnteFile[] ) => { const syncedCollectionIds = new Set(); 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, diff --git a/src/services/migrateThumbnailService.ts b/src/services/migrateThumbnailService.ts index 4245a4538..a5c693e97 100644 --- a/src/services/migrateThumbnailService.ts +++ b/src/services/migrateThumbnailService.ts @@ -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 ); diff --git a/src/services/upload/queueProcessor.ts b/src/services/queueProcessor.ts similarity index 91% rename from src/services/upload/queueProcessor.ts rename to src/services/queueProcessor.ts index 6d9e86fc6..f6fd3df5b 100644 --- a/src/services/upload/queueProcessor.ts +++ b/src/services/queueProcessor.ts @@ -1,4 +1,4 @@ -import { CustomError } from 'utils/common/errorUtil'; +import { CustomError } from 'utils/error'; interface RequestQueueItem { request: (canceller?: RequestCanceller) => Promise; @@ -43,7 +43,7 @@ export default class QueueProcessor { 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 { } } - 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) { diff --git a/src/services/searchService.ts b/src/services/searchService.ts index 7d41fed8a..b69c88e75 100644 --- a/src/services/searchService.ts +++ b/src/services/searchService.ts @@ -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, diff --git a/src/services/trashService.ts b/src/services/trashService.ts index 11cc3933a..36ea04756 100644 --- a/src/services/trashService.ts +++ b/src/services/trashService.ts @@ -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)) || []; return trash; @@ -52,7 +45,7 @@ async function getLastSyncTime() { export async function syncTrash( collections: Collection[], setFiles: SetFiles, - files: File[] + files: EnteFile[] ): Promise { const trash = await getLocalTrash(); collections = [...collections, ...(await getLocalDeletedCollections())]; @@ -79,7 +72,7 @@ export const updateTrash = async ( collections: Map, sinceTime: number, setFiles: SetFiles, - files: File[], + files: EnteFile[], currentTrash: Trash ): Promise => { try { diff --git a/src/services/updateCreationTimeWithExif.ts b/src/services/updateCreationTimeWithExif.ts index b88004f54..e34f889c0 100644 --- a/src/services/updateCreationTimeWithExif.ts +++ b/src/services/updateCreationTimeWithExif.ts @@ -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( diff --git a/src/services/upload/encryptionService.ts b/src/services/upload/encryptionService.ts index 3ea654cf0..edbc4864b 100644 --- a/src/services/upload/encryptionService.ts +++ b/src/services/upload/encryptionService.ts @@ -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; diff --git a/src/services/upload/exifService.ts b/src/services/upload/exifService.ts index 5792267c8..e463598ed 100644 --- a/src/services/upload/exifService.ts +++ b/src/services/upload/exifService.ts @@ -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 { 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((resolve) => { - const reader = new FileReader(); reader.onload = () => resolve(reader.result as string); reader.readAsDataURL(blob); }); diff --git a/src/services/upload/metadataService.ts b/src/services/upload/metadataService.ts index 305b0b202..cc5a6909f 100644 --- a/src/services/upload/metadataService.ts +++ b/src/services/upload/metadataService.ts @@ -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 diff --git a/src/services/upload/multiPartUploadService.ts b/src/services/upload/multiPartUploadService.ts index be082ba7a..ddecde550 100644 --- a/src/services/upload/multiPartUploadService.ts +++ b/src/services/upload/multiPartUploadService.ts @@ -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 diff --git a/src/services/upload/readFileService.ts b/src/services/upload/readFileService.ts index b7742b67b..b40ccfe38 100644 --- a/src/services/upload/readFileService.ts +++ b/src/services/upload/readFileService.ts @@ -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 { 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({ 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; } diff --git a/src/services/upload/thumbnailService.ts b/src/services/upload/thumbnailService.ts index 8872150df..7d908899d 100644 --- a/src/services/upload/thumbnailService.ts +++ b/src/services/upload/thumbnailService.ts @@ -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'); diff --git a/src/services/upload/uiService.ts b/src/services/upload/uiService.ts index d85f8f743..7857bb8b6 100644 --- a/src/services/upload/uiService.ts +++ b/src/services/upload/uiService.ts @@ -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; diff --git a/src/services/upload/uploadHttpClient.ts b/src/services/upload/uploadHttpClient.ts index a3bc006e5..0e4d803af 100644 --- a/src/services/upload/uploadHttpClient.ts +++ b/src/services/upload/uploadHttpClient.ts @@ -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 { + async uploadFile(uploadFile: UploadFile): Promise { try { const token = getToken(); if (!token) { diff --git a/src/services/upload/uploadManager.ts b/src/services/upload/uploadManager.ts index fe9db1016..1da78b92a 100644 --- a/src/services/upload/uploadManager.ts +++ b/src/services/upload/uploadManager.ts @@ -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; - class UploadManager { private cryptoWorkers = new Array(MAX_CONCURRENT_UPLOADS); private metadataMap: MetadataMap; private filesToBeUploaded: FileWithCollection[]; private failedFiles: FileWithCollection[]; - private existingFilesCollectionWise: Map; - private existingFiles: File[]; + private existingFilesCollectionWise: Map; + private existingFiles: EnteFile[]; private setFiles: SetFiles; private collections: Map; 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(); + this.metadataMap = new Map(); 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 ); diff --git a/src/services/upload/uploadService.ts b/src/services/upload/uploadService.ts index a14682772..a624d3c93 100644 --- a/src/services/upload/uploadService.ts +++ b/src/services/upload/uploadService.ts @@ -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; - 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 { - 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 {} - -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; + private metadataMap: Map; 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 { 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 { + ): Promise { 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 ); diff --git a/src/services/upload/uploader.ts b/src/services/upload/uploader.ts index aef8bd962..aee78c2b9 100644 --- a/src/services/upload/uploader.ts +++ b/src/services/upload/uploader.ts @@ -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 { 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; } diff --git a/src/services/userService.ts b/src/services/userService.ts index 5960b076f..5cdd08ca1 100644 --- a/src/services/userService.ts +++ b/src/services/userService.ts @@ -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, diff --git a/src/types.ts b/src/types.ts deleted file mode 100644 index e7bf5e2af..000000000 --- a/src/types.ts +++ /dev/null @@ -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 = '/', -} diff --git a/src/types/billing/index.ts b/src/types/billing/index.ts new file mode 100644 index 000000000..59a19b055 --- /dev/null +++ b/src/types/billing/index.ts @@ -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; +} diff --git a/src/types/collection/index.ts b/src/types/collection/index.ts new file mode 100644 index 000000000..b6e35693a --- /dev/null +++ b/src/types/collection/index.ts @@ -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[]; +} diff --git a/src/types/export/index.ts b/src/types/export/index.ts new file mode 100644 index 000000000..baf210f4c --- /dev/null +++ b/src/types/export/index.ts @@ -0,0 +1,25 @@ +import { ExportStage } from 'constants/export'; + +export type CollectionIDPathMap = Map; +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; +} diff --git a/src/types/file/index.ts b/src/types/file/index.ts new file mode 100644 index 000000000..ab0b1a566 --- /dev/null +++ b/src/types/file/index.ts @@ -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; +} + +export interface EncryptedMagicMetadataCore + extends Omit { + data: string; +} + +export interface MagicMetadataProps { + visibility?: VISIBILITY_STATE; +} + +export interface MagicMetadata extends Omit { + data: MagicMetadataProps; +} + +export interface PublicMagicMetadataProps { + editedTime?: number; + editedName?: string; +} + +export interface PublicMagicMetadata extends Omit { + 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; +} diff --git a/src/types/gallery/index.ts b/src/types/gallery/index.ts new file mode 100644 index 000000000..e7c10e3ce --- /dev/null +++ b/src/types/gallery/index.ts @@ -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>; +export type SetCollections = React.Dispatch>; +export type SetLoading = React.Dispatch>; +export type setSearchStats = React.Dispatch>; + +export type Search = { + date?: DateValue; + location?: Bbox; + fileIndex?: number; +}; +export interface SearchStats { + resultCount: number; + timeTaken: number; +} + +export type GalleryContextType = { + thumbs: Map; + files: Map; + showPlanSelectorModal: () => void; + setActiveCollection: (collection: number) => void; + syncWithRemote: (force?: boolean, silent?: boolean) => Promise; +}; diff --git a/src/types/search/index.ts b/src/types/search/index.ts new file mode 100644 index 000000000..0f3a7bfff --- /dev/null +++ b/src/types/search/index.ts @@ -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; +} diff --git a/src/types/trash/index.ts b/src/types/trash/index.ts new file mode 100644 index 000000000..9a9492ce6 --- /dev/null +++ b/src/types/trash/index.ts @@ -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[]; diff --git a/src/types/upload/index.ts b/src/types/upload/index.ts new file mode 100644 index 000000000..6e58177bc --- /dev/null +++ b/src/types/upload/index.ts @@ -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; + 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>; + setFileCounter: React.Dispatch< + React.SetStateAction<{ + finished: number; + total: number; + }> + >; + setUploadStage: React.Dispatch>; + setFileProgress: React.Dispatch>>; + setUploadResult: React.Dispatch>>; +} + +export interface FileWithCollection { + file: File; + collectionID?: number; + collection?: Collection; +} + +export type MetadataMap = Map; + +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 { + 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 {} + +export interface UploadFile extends BackupedFile { + collectionID: number; + encryptedKey: string; + keyDecryptionNonce: string; +} diff --git a/src/types/user/index.ts b/src/types/user/index.ts new file mode 100644 index 000000000..72ad9ba8f --- /dev/null +++ b/src/types/user/index.ts @@ -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; +} diff --git a/src/utils/billingUtil.ts b/src/utils/billing/index.ts similarity index 95% rename from src/utils/billingUtil.ts rename to src/utils/billing/index.ts index 43e1e1924..e8043d61d 100644 --- a/src/utils/billingUtil.ts +++ b/src/utils/billing/index.ts @@ -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 ); } diff --git a/src/utils/collection/index.ts b/src/utils/collection/index.ts index 46aae90e9..e63500b60 100644 --- a/src/utils/collection/index.ts +++ b/src/utils/collection/index.ts @@ -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 ) { diff --git a/src/utils/common/key.ts b/src/utils/common/key.ts index 17d14b8ad..d3130d1fe 100644 --- a/src/utils/common/key.ts +++ b/src/utils/common/key.ts @@ -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 { diff --git a/src/utils/crypto/index.ts b/src/utils/crypto/index.ts index 2f6e237c3..9e1e39bd5 100644 --- a/src/utils/crypto/index.ts +++ b/src/utils/crypto/index.ts @@ -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'; diff --git a/src/utils/crypto/libsodium.ts b/src/utils/crypto/libsodium.ts index 3b45c121d..e4678a2c7 100644 --- a/src/utils/crypto/libsodium.ts +++ b/src/utils/crypto/libsodium.ts @@ -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, diff --git a/src/utils/common/errorUtil.ts b/src/utils/error/index.ts similarity index 100% rename from src/utils/common/errorUtil.ts rename to src/utils/error/index.ts diff --git a/src/utils/export/index.ts b/src/utils/export/index.ts index ffad98a7f..ed58f4420 100644 --- a/src/utils/export/index.ts +++ b/src/utils/export/index.ts @@ -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 diff --git a/src/utils/file/index.ts b/src/utils/file/index.ts index ea911e08a..929c319ea 100644 --- a/src/utils/file/index.ts +++ b/src/utils/file/index.ts @@ -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(); +export function sortFilesIntoCollections(files: EnteFile[]) { + const collectionWiseFiles = new Map(); 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(); 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(); 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 || diff --git a/src/utils/search/index.ts b/src/utils/search/index.ts index 60f6211e1..ce9da7f1c 100644 --- a/src/utils/search/index.ts +++ b/src/utils/search/index.ts @@ -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); diff --git a/src/utils/sentry/index.ts b/src/utils/sentry/index.ts index b3d4c87ca..a37919d34 100644 --- a/src/utils/sentry/index.ts +++ b/src/utils/sentry/index.ts @@ -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 = ( diff --git a/src/utils/upload/index.ts b/src/utils/upload/index.ts index ded536051..ebf11f0a3 100644 --- a/src/utils/upload/index.ts +++ b/src/utils/upload/index.ts @@ -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 && diff --git a/src/worker/crypto.worker.js b/src/worker/crypto.worker.js index e4da90107..cf0137831 100644 --- a/src/worker/crypto.worker.js +++ b/src/worker/crypto.worker.js @@ -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); } diff --git a/tsconfig.json b/tsconfig.json index 46e7172e8..4e0195e39 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -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" - ] -} \ No newline at end of file + "exclude": ["node_modules"] +}