Initial cast commit

This commit is contained in:
httpjamesm 2023-11-12 15:05:53 -05:00
parent 775fb7f66e
commit 2415fe4cab
No known key found for this signature in database
75 changed files with 10285 additions and 6 deletions

13
apps/cast/.eslintrc.js Normal file
View file

@ -0,0 +1,13 @@
module.exports = {
// When root is set to true, ESLint will stop looking for configuration files in parent directories.
// This is required here to ensure desktop picks the right eslint config, where this app is
// packaged as a submodule.
root: true,
extends: ['@ente/eslint-config'],
parser: '@typescript-eslint/parser',
parserOptions: {
tsconfigRootDir: __dirname,
project: './tsconfig.json',
},
ignorePatterns: ['.eslintrc.js'],
};

36
apps/cast/.gitignore vendored Normal file
View file

@ -0,0 +1,36 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

40
apps/cast/README.md Normal file
View file

@ -0,0 +1,40 @@
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file.
[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`.
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.

6
apps/cast/next.config.js Normal file
View file

@ -0,0 +1,6 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
};
module.exports = nextConfig;

24
apps/cast/package.json Normal file
View file

@ -0,0 +1,24 @@
{
"name": "cast",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"react": "^18",
"react-dom": "^18",
"next": "14.0.2"
},
"devDependencies": {
"typescript": "^5",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"eslint": "^8",
"eslint-config-next": "14.0.2"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>

After

Width:  |  Height:  |  Size: 629 B

View file

@ -0,0 +1,53 @@
import * as Sentry from '@sentry/nextjs';
import { getSentryTunnelURL } from 'utils/common/apiUtil';
import { getSentryUserID } from 'utils/user';
import { runningInBrowser } from 'utils/common';
import { getHasOptedOutOfCrashReports } from 'utils/storage/index';
import {
getSentryDSN,
getSentryENV,
getSentryRelease,
getIsSentryEnabled,
} from 'constants/sentry';
const HAS_OPTED_OUT_OF_CRASH_REPORTING =
runningInBrowser() && getHasOptedOutOfCrashReports();
if (!HAS_OPTED_OUT_OF_CRASH_REPORTING) {
const SENTRY_DSN = getSentryDSN();
const SENTRY_ENV = getSentryENV();
const SENTRY_RELEASE = getSentryRelease();
const IS_ENABLED = getIsSentryEnabled();
Sentry.init({
dsn: SENTRY_DSN,
enabled: IS_ENABLED,
environment: SENTRY_ENV,
release: SENTRY_RELEASE,
attachStacktrace: true,
autoSessionTracking: false,
tunnel: getSentryTunnelURL(),
beforeSend(event) {
event.request = event.request || {};
const currentURL = new URL(document.location.href);
currentURL.hash = '';
event.request.url = currentURL;
return event;
},
integrations: function (i) {
return i.filter(function (i) {
return i.name !== 'Breadcrumbs';
});
},
// ...
// Note: if you want to override the automatic release value, do not set a
// `release` value here - use the environment variable `SENTRY_RELEASE`, so
// that it will also get attached to your source maps
});
const main = async () => {
Sentry.setUser({ id: await getSentryUserID() });
};
main();
}

View file

@ -0,0 +1,3 @@
defaults.url=https://sentry.ente.io/
defaults.org=ente
defaults.project=photos-web

View file

@ -0,0 +1,28 @@
import * as Sentry from '@sentry/nextjs';
import {
getSentryDSN,
getSentryENV,
getSentryRelease,
getIsSentryEnabled,
} from 'constants/sentry';
import { getSentryUserID } from 'utils/user';
const SENTRY_DSN = getSentryDSN();
const SENTRY_ENV = getSentryENV();
const SENTRY_RELEASE = getSentryRelease();
const IS_ENABLED = getIsSentryEnabled();
Sentry.init({
dsn: SENTRY_DSN,
enabled: IS_ENABLED,
environment: SENTRY_ENV,
release: SENTRY_RELEASE,
autoSessionTracking: false,
});
const main = async () => {
Sentry.setUser({ id: await getSentryUserID() });
};
main();

View file

@ -0,0 +1,11 @@
const ENV_DEVELOPMENT = 'development';
module.exports.getIsSentryEnabled = () => {
if (process.env.NEXT_PUBLIC_SENTRY_ENV === ENV_DEVELOPMENT) {
return false;
} else if (process.env.NEXT_PUBLIC_DISABLE_SENTRY === 'true') {
return false;
} else {
return true;
}
};

View file

@ -0,0 +1 @@
export const REQUEST_BATCH_SIZE = 1000;

View file

@ -0,0 +1,59 @@
import { PAGES } from 'constants/pages';
import { runningInBrowser } from 'utils/common';
import { getAlbumsURL, getAuthURL } from 'utils/common/apiUtil';
export enum APPS {
PHOTOS = 'PHOTOS',
AUTH = 'AUTH',
ALBUMS = 'ALBUMS',
}
export const ALLOWED_APP_PAGES = new Map([
[APPS.ALBUMS, [PAGES.SHARED_ALBUMS, PAGES.ROOT]],
[
APPS.AUTH,
[
PAGES.ROOT,
PAGES.LOGIN,
PAGES.SIGNUP,
PAGES.VERIFY,
PAGES.CREDENTIALS,
PAGES.RECOVER,
PAGES.CHANGE_PASSWORD,
PAGES.GENERATE,
PAGES.AUTH,
PAGES.TWO_FACTOR_VERIFY,
PAGES.TWO_FACTOR_RECOVER,
],
],
]);
export const CLIENT_PACKAGE_NAMES = new Map([
[APPS.ALBUMS, 'io.ente.albums.web'],
[APPS.PHOTOS, 'io.ente.photos.web'],
[APPS.AUTH, 'io.ente.auth.web'],
]);
export const getAppNameAndTitle = () => {
if (!runningInBrowser()) {
return {};
}
const currentURL = new URL(window.location.href);
const albumsURL = new URL(getAlbumsURL());
const authURL = new URL(getAuthURL());
if (currentURL.origin === albumsURL.origin) {
return { name: APPS.ALBUMS, title: 'ente Photos' };
} else if (currentURL.origin === authURL.origin) {
return { name: APPS.AUTH, title: 'ente Auth' };
} else {
return { name: APPS.PHOTOS, title: 'ente Photos' };
}
};
export const getAppTitle = () => {
return getAppNameAndTitle().title;
};
export const getAppName = () => {
return getAppNameAndTitle().name;
};

View file

@ -0,0 +1,5 @@
export enum CACHES {
THUMBS = 'thumbs',
FACE_CROPS = 'face-crops',
FILES = 'files',
}

View file

@ -0,0 +1,100 @@
export const ARCHIVE_SECTION = -1;
export const TRASH_SECTION = -2;
export const DUMMY_UNCATEGORIZED_COLLECTION = -3;
export const HIDDEN_ITEMS_SECTION = -4;
export const ALL_SECTION = 0;
export const DEFAULT_HIDDEN_COLLECTION_USER_FACING_NAME = 'Hidden';
export enum CollectionType {
folder = 'folder',
favorites = 'favorites',
album = 'album',
uncategorized = 'uncategorized',
}
export enum CollectionSummaryType {
folder = 'folder',
favorites = 'favorites',
album = 'album',
archive = 'archive',
trash = 'trash',
uncategorized = 'uncategorized',
all = 'all',
outgoingShare = 'outgoingShare',
incomingShareViewer = 'incomingShareViewer',
incomingShareCollaborator = 'incomingShareCollaborator',
sharedOnlyViaLink = 'sharedOnlyViaLink',
archived = 'archived',
defaultHidden = 'defaultHidden',
hiddenItems = 'hiddenItems',
pinned = 'pinned',
}
export enum COLLECTION_LIST_SORT_BY {
NAME,
CREATION_TIME_ASCENDING,
UPDATION_TIME_DESCENDING,
}
export const COLLECTION_SHARE_DEFAULT_VALID_DURATION =
10 * 24 * 60 * 60 * 1000 * 1000;
export const COLLECTION_SHARE_DEFAULT_DEVICE_LIMIT = 4;
export const COLLECTION_SORT_ORDER = new Map([
[CollectionSummaryType.all, 0],
[CollectionSummaryType.hiddenItems, 0],
[CollectionSummaryType.uncategorized, 1],
[CollectionSummaryType.favorites, 2],
[CollectionSummaryType.pinned, 3],
[CollectionSummaryType.album, 4],
[CollectionSummaryType.folder, 4],
[CollectionSummaryType.incomingShareViewer, 4],
[CollectionSummaryType.incomingShareCollaborator, 4],
[CollectionSummaryType.outgoingShare, 4],
[CollectionSummaryType.sharedOnlyViaLink, 4],
[CollectionSummaryType.archived, 4],
[CollectionSummaryType.archive, 5],
[CollectionSummaryType.trash, 6],
[CollectionSummaryType.defaultHidden, 7],
]);
export const SYSTEM_COLLECTION_TYPES = new Set([
CollectionSummaryType.all,
CollectionSummaryType.archive,
CollectionSummaryType.trash,
CollectionSummaryType.uncategorized,
CollectionSummaryType.hiddenItems,
CollectionSummaryType.defaultHidden,
]);
export const ADD_TO_NOT_ALLOWED_COLLECTION = new Set([
CollectionSummaryType.all,
CollectionSummaryType.archive,
CollectionSummaryType.incomingShareViewer,
CollectionSummaryType.trash,
CollectionSummaryType.uncategorized,
CollectionSummaryType.defaultHidden,
CollectionSummaryType.hiddenItems,
]);
export const MOVE_TO_NOT_ALLOWED_COLLECTION = new Set([
CollectionSummaryType.all,
CollectionSummaryType.archive,
CollectionSummaryType.incomingShareViewer,
CollectionSummaryType.incomingShareCollaborator,
CollectionSummaryType.trash,
CollectionSummaryType.uncategorized,
CollectionSummaryType.defaultHidden,
CollectionSummaryType.hiddenItems,
]);
export const OPTIONS_NOT_HAVING_COLLECTION_TYPES = new Set([
CollectionSummaryType.all,
CollectionSummaryType.archive,
]);
export const HIDE_FROM_COLLECTION_BAR_TYPES = new Set([
CollectionSummaryType.trash,
CollectionSummaryType.archive,
CollectionSummaryType.uncategorized,
CollectionSummaryType.defaultHidden,
]);

View file

@ -0,0 +1,7 @@
export const ENCRYPTION_CHUNK_SIZE = 4 * 1024 * 1024;
export enum PasswordStrength {
WEAK = 'WEAK',
MODERATE = 'MODERATE',
STRONG = 'STRONG',
}

View file

@ -0,0 +1,43 @@
export const MIN_EDITED_CREATION_TIME = new Date(1800, 0, 1);
export const MAX_EDITED_CREATION_TIME = new Date();
export const MAX_EDITED_FILE_NAME_LENGTH = 100;
export const MAX_CAPTION_SIZE = 5000;
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 const RAW_FORMATS = [
'heic',
'rw2',
'tiff',
'arw',
'cr3',
'cr2',
'raf',
'nef',
'psd',
'dng',
'tif',
];
export const SUPPORTED_RAW_FORMATS = [
'heic',
'rw2',
'tiff',
'arw',
'cr3',
'cr2',
'nef',
'psd',
'dng',
'tif',
];

View file

@ -0,0 +1,15 @@
export const GAP_BTW_TILES = 4;
export const DATE_CONTAINER_HEIGHT = 48;
export const SIZE_AND_COUNT_CONTAINER_HEIGHT = 72;
export const IMAGE_CONTAINER_MAX_HEIGHT = 180;
export const IMAGE_CONTAINER_MAX_WIDTH = 180;
export const MIN_COLUMNS = 4;
export const SPACE_BTW_DATES = 44;
export const SPACE_BTW_DATES_TO_IMAGE_CONTAINER_WIDTH_RATIO = 0.244;
export enum PLAN_PERIOD {
MONTH = 'month',
YEAR = 'year',
}
export const SYNC_INTERVAL_IN_MICROSECONDS = 1000 * 60 * 5; // 5 minutes

View file

@ -0,0 +1,8 @@
/** Enums of supported locale */
export enum Language {
en = 'en',
fr = 'fr',
zh = 'zh',
nl = 'nl',
es = 'es',
}

View file

@ -0,0 +1,20 @@
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 = '/',
SHARED_ALBUMS = '/shared-albums',
// ML_DEBUG = '/ml-debug',
DEDUPLICATE = '/deduplicate',
// AUTH page is used to show (auth)enticator codes
AUTH = '/auth',
}

View file

@ -0,0 +1,15 @@
export const ENV_DEVELOPMENT = 'development';
export const ENV_PRODUCTION = 'production';
export const getSentryDSN = () =>
process.env.NEXT_PUBLIC_SENTRY_DSN ??
'https://bd3656fc40d74d5e8f278132817963a3@sentry.ente.io/2';
export const getSentryENV = () =>
process.env.NEXT_PUBLIC_SENTRY_ENV ?? ENV_PRODUCTION;
export const getSentryRelease = () => process.env.SENTRY_RELEASE;
export { getIsSentryEnabled } from '../../sentryConfigUtil';
export const isDEVSentryENV = () => getSentryENV() === ENV_DEVELOPMENT;

View file

@ -0,0 +1,142 @@
import { ENCRYPTION_CHUNK_SIZE } from 'constants/crypto';
import { FILE_TYPE } from 'constants/file';
import {
FileTypeInfo,
ImportSuggestion,
Location,
ParsedExtractedMetadata,
} from 'types/upload';
// list of format that were missed by type-detection for some files.
export const WHITELISTED_FILE_FORMATS: FileTypeInfo[] = [
{ fileType: FILE_TYPE.IMAGE, exactType: 'jpeg', mimeType: 'image/jpeg' },
{ fileType: FILE_TYPE.IMAGE, exactType: 'jpg', mimeType: 'image/jpeg' },
{ fileType: FILE_TYPE.VIDEO, exactType: 'webm', mimeType: 'video/webm' },
{ fileType: FILE_TYPE.VIDEO, exactType: 'mod', mimeType: 'video/mpeg' },
{ fileType: FILE_TYPE.VIDEO, exactType: 'mp4', mimeType: 'video/mp4' },
{ fileType: FILE_TYPE.IMAGE, exactType: 'gif', mimeType: 'image/gif' },
{ fileType: FILE_TYPE.VIDEO, exactType: 'dv', mimeType: 'video/x-dv' },
{
fileType: FILE_TYPE.VIDEO,
exactType: 'wmv',
mimeType: 'video/x-ms-asf',
},
{
fileType: FILE_TYPE.VIDEO,
exactType: 'hevc',
mimeType: 'video/hevc',
},
{
fileType: FILE_TYPE.IMAGE,
exactType: 'raf',
mimeType: 'image/x-fuji-raf',
},
{
fileType: FILE_TYPE.IMAGE,
exactType: 'orf',
mimeType: 'image/x-olympus-orf',
},
{
fileType: FILE_TYPE.IMAGE,
exactType: 'crw',
mimeType: 'image/x-canon-crw',
},
];
export const KNOWN_NON_MEDIA_FORMATS = ['xmp', 'html', 'txt'];
export const EXIFLESS_FORMATS = ['gif', 'bmp'];
// 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,
EXTRACTING_METADATA,
UPLOADING,
CANCELLING,
FINISH,
}
export enum UPLOAD_STRATEGY {
SINGLE_COLLECTION,
COLLECTION_PER_FOLDER,
}
export enum UPLOAD_RESULT {
FAILED,
ALREADY_UPLOADED,
UNSUPPORTED,
BLOCKED,
TOO_LARGE,
LARGER_THAN_AVAILABLE_STORAGE,
UPLOADED,
UPLOADED_WITH_STATIC_THUMBNAIL,
ADDED_SYMLINK,
}
export enum PICKED_UPLOAD_TYPE {
FILES = 'files',
FOLDERS = 'folders',
ZIPS = 'zips',
}
export const MAX_FILE_SIZE_SUPPORTED = 4 * 1024 * 1024 * 1024; // 4 GB
export const LIVE_PHOTO_ASSET_SIZE_LIMIT = 20 * 1024 * 1024; // 20MB
export const NULL_EXTRACTED_METADATA: ParsedExtractedMetadata = {
location: NULL_LOCATION,
creationTime: null,
width: null,
height: null,
};
export const A_SEC_IN_MICROSECONDS = 1e6;
export const DEFAULT_IMPORT_SUGGESTION: ImportSuggestion = {
rootFolderName: '',
hasNestedFolders: false,
hasRootLevelFileWithFolder: false,
};
export const BLACK_THUMBNAIL_BASE64 =
'/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAEBAQEBAQEB' +
'AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQ' +
'EBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAARC' +
'ACWASwDAREAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUF' +
'BAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk' +
'6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztL' +
'W2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAA' +
'AAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVY' +
'nLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImK' +
'kpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oAD' +
'AMBAAIRAxEAPwD/AD/6ACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKA' +
'CgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACg' +
'AoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKAC' +
'gAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAo' +
'AKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACg' +
'AoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACg' +
'AoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKA' +
'CgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKA' +
'CgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoA' +
'KACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACg' +
'AoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAo' +
'AKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKA' +
'CgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAK' +
'ACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoA' +
'KACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAo' +
'AKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAo' +
'AKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgD/9k=';

View file

@ -0,0 +1,19 @@
export const ENTE_WEBSITE_LINK = 'https://ente.io';
export const ML_BLOG_LINK = 'https://ente.io/blog/desktop-ml-beta';
export const FACE_SEARCH_PRIVACY_POLICY_LINK =
'https://ente.io/privacy#8-biometric-information-privacy-policy';
export const SUPPORT_EMAIL = 'support@ente.io';
export const APP_DOWNLOAD_URL = 'https://ente.io/download/desktop';
export const FEEDBACK_EMAIL = 'feedback@ente.io';
export const DELETE_ACCOUNT_EMAIL = 'account-deletion@ente.io';
export const WEB_ROADMAP_URL = 'https://github.com/ente-io/photos-web/issues';
export const DESKTOP_ROADMAP_URL =
'https://github.com/ente-io/photos-desktop/issues';

View file

@ -0,0 +1,6 @@
import '/styles/globals.css';
import type { AppProps } from 'next/app';
export default function App({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />;
}

View file

@ -0,0 +1,13 @@
import { Html, Head, Main, NextScript } from 'next/document';
export default function Document() {
return (
<Html lang="en">
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
);
}

View file

@ -0,0 +1,118 @@
import Head from 'next/head';
import Image from 'next/image';
import { Inter } from 'next/font/google';
import styles from '/styles/Home.module.css';
const inter = Inter({ subsets: ['latin'] });
export default function Home() {
return (
<>
<Head>
<title>Create Next App</title>
<meta
name="description"
content="Generated by create next app"
/>
<meta
name="viewport"
content="width=device-width, initial-scale=1"
/>
<link rel="icon" href="/favicon.ico" />
</Head>
<main className={`${styles.main} ${inter.className}`}>
<div className={styles.description}>
<p>
Get started by editing&nbsp;
<code className={styles.code}>src/pages/index.tsx</code>
</p>
<div>
<a
href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer">
By{' '}
<Image
src="/vercel.svg"
alt="Vercel Logo"
className={styles.vercelLogo}
width={100}
height={24}
priority
/>
</a>
</div>
</div>
<div className={styles.center}>
<Image
className={styles.logo}
src="/next.svg"
alt="Next.js Logo"
width={180}
height={37}
priority
/>
</div>
<div className={styles.grid}>
<a
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
className={styles.card}
target="_blank"
rel="noopener noreferrer">
<h2>
Docs <span>-&gt;</span>
</h2>
<p>
Find in-depth information about Next.js features
and&nbsp;API.
</p>
</a>
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
className={styles.card}
target="_blank"
rel="noopener noreferrer">
<h2>
Learn <span>-&gt;</span>
</h2>
<p>
Learn about Next.js in an interactive course
with&nbsp;quizzes!
</p>
</a>
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
className={styles.card}
target="_blank"
rel="noopener noreferrer">
<h2>
Templates <span>-&gt;</span>
</h2>
<p>
Discover and deploy boilerplate example
Next.js&nbsp;projects.
</p>
</a>
<a
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
className={styles.card}
target="_blank"
rel="noopener noreferrer">
<h2>
Deploy <span>-&gt;</span>
</h2>
<p>
Instantly deploy your Next.js site to a shareable
URL with&nbsp;Vercel.
</p>
</a>
</div>
</main>
</>
);
}

View file

@ -0,0 +1,241 @@
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
import { addLogLine } from 'utils/logging';
import { logError } from 'utils/sentry';
import { ApiError, CustomError, isApiErrorResponse } from 'utils/error';
interface IHTTPHeaders {
[headerKey: string]: any;
}
interface IQueryPrams {
[paramName: string]: any;
}
/**
* Service to manage all HTTP calls.
*/
class HTTPService {
constructor() {
axios.interceptors.response.use(
(response) => Promise.resolve(response),
(error) => {
const config = error.config as AxiosRequestConfig;
if (error.response) {
const response = error.response as AxiosResponse;
let apiError: ApiError;
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
if (isApiErrorResponse(response.data)) {
const responseData = response.data;
logError(error, 'HTTP Service Error', {
url: config.url,
method: config.method,
xRequestId: response.headers['x-request-id'],
httpStatus: response.status,
errMessage: responseData.message,
errCode: responseData.code,
});
apiError = new ApiError(
responseData.message,
responseData.code,
response.status
);
} else {
if (response.status >= 400 && response.status < 500) {
apiError = new ApiError(
CustomError.CLIENT_ERROR,
'',
response.status
);
} else {
apiError = new ApiError(
CustomError.ServerError,
'',
response.status
);
}
}
logError(apiError, 'HTTP Service Error', {
url: config.url,
method: config.method,
cfRay: response.headers['cf-ray'],
xRequestId: response.headers['x-request-id'],
httpStatus: response.status,
});
throw apiError;
} else if (error.request) {
// The request was made but no response was received
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
// http.ClientRequest in node.js
addLogLine(
'request failed- no response',
`url: ${config.url}`,
`method: ${config.method}`
);
return Promise.reject(error);
} else {
// Something happened in setting up the request that triggered an Error
addLogLine(
'request failed- axios error',
`url: ${config.url}`,
`method: ${config.method}`
);
return Promise.reject(error);
}
}
);
}
/**
* header object to be append to all api calls.
*/
private headers: IHTTPHeaders = {
'content-type': 'application/json',
};
/**
* Sets the headers to the given object.
*/
public setHeaders(headers: IHTTPHeaders) {
this.headers = {
...this.headers,
...headers,
};
}
/**
* Adds a header to list of headers.
*/
public appendHeader(key: string, value: string) {
this.headers = {
...this.headers,
[key]: value,
};
}
/**
* Removes the given header.
*/
public removeHeader(key: string) {
this.headers[key] = undefined;
}
/**
* Returns axios interceptors.
*/
// eslint-disable-next-line class-methods-use-this
public getInterceptors() {
return axios.interceptors;
}
/**
* Generic HTTP request.
* This is done so that developer can use any functionality
* provided by axios. Here, only the set headers are spread
* over what was sent in config.
*/
public async request(config: AxiosRequestConfig, customConfig?: any) {
// eslint-disable-next-line no-param-reassign
config.headers = {
...this.headers,
...config.headers,
};
if (customConfig?.cancel) {
config.cancelToken = new axios.CancelToken(
(c) => (customConfig.cancel.exec = c)
);
}
return await axios({ ...config, ...customConfig });
}
/**
* Get request.
*/
public get(
url: string,
params?: IQueryPrams,
headers?: IHTTPHeaders,
customConfig?: any
) {
return this.request(
{
headers,
method: 'GET',
params,
url,
},
customConfig
);
}
/**
* Post request
*/
public post(
url: string,
data?: any,
params?: IQueryPrams,
headers?: IHTTPHeaders,
customConfig?: any
) {
return this.request(
{
data,
headers,
method: 'POST',
params,
url,
},
customConfig
);
}
/**
* Put request
*/
public put(
url: string,
data: any,
params?: IQueryPrams,
headers?: IHTTPHeaders,
customConfig?: any
) {
return this.request(
{
data,
headers,
method: 'PUT',
params,
url,
},
customConfig
);
}
/**
* Delete request
*/
public delete(
url: string,
data: any,
params?: IQueryPrams,
headers?: IHTTPHeaders,
customConfig?: any
) {
return this.request(
{
data,
headers,
method: 'DELETE',
params,
url,
},
customConfig
);
}
}
// Creates a Singleton Service.
// This will help me maintain common headers / functionality
// at a central place.
export default new HTTPService();

View file

@ -0,0 +1,32 @@
export enum MS_KEYS {
OPT_OUT_OF_CRASH_REPORTS = 'optOutOfCrashReports',
SRP_CONFIGURE_IN_PROGRESS = 'srpConfigureInProgress',
REDIRECT_URL = 'redirectUrl',
}
type StoreType = Map<Partial<MS_KEYS>, any>;
class InMemoryStore {
private store: StoreType = new Map();
get(key: MS_KEYS) {
return this.store.get(key);
}
set(key: MS_KEYS, value: any) {
this.store.set(key, value);
}
delete(key: MS_KEYS) {
this.store.delete(key);
}
has(key: MS_KEYS) {
return this.store.has(key);
}
clear() {
this.store.clear();
}
}
export default new InMemoryStore();

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,12 @@
import { EventEmitter } from 'eventemitter3';
// When registering event handlers,
// handle errors to avoid unhandled rejection or propagation to emit call
export enum Events {
LOGOUT = 'logout',
FILE_UPLOADED = 'fileUploaded',
LOCAL_FILES_UPDATED = 'localFilesUpdated',
}
export const eventBus = new EventEmitter<Events>();

View file

@ -0,0 +1,311 @@
import { getEndpoint } from 'utils/common/apiUtil';
import localForage from 'utils/storage/localForage';
import { getToken } from 'utils/common/key';
import { Collection } from 'types/collection';
import HTTPService from './HTTPService';
import { logError } from 'utils/sentry';
import {
decryptFile,
getLatestVersionFiles,
mergeMetadata,
sortFiles,
} from 'utils/file';
import { eventBus, Events } from './events';
import {
EnteFile,
EncryptedEnteFile,
TrashRequest,
FileWithUpdatedMagicMetadata,
FileWithUpdatedPublicMagicMetadata,
} from 'types/file';
import { SetFiles } from 'types/gallery';
import { BulkUpdateMagicMetadataRequest } from 'types/magicMetadata';
import { addLogLine } from 'utils/logging';
import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker';
import {
getCollectionLastSyncTime,
setCollectionLastSyncTime,
} from './collectionService';
import { REQUEST_BATCH_SIZE } from 'constants/api';
import { batch } from 'utils/common';
const ENDPOINT = getEndpoint();
const FILES_TABLE = 'files';
const HIDDEN_FILES_TABLE = 'hidden-files';
export const getLocalFiles = async (type: 'normal' | 'hidden' = 'normal') => {
const tableName = type === 'normal' ? FILES_TABLE : HIDDEN_FILES_TABLE;
const files: Array<EnteFile> =
(await localForage.getItem<EnteFile[]>(tableName)) || [];
return files;
};
const setLocalFiles = async (type: 'normal' | 'hidden', files: EnteFile[]) => {
try {
const tableName = type === 'normal' ? FILES_TABLE : HIDDEN_FILES_TABLE;
await localForage.setItem(tableName, files);
try {
eventBus.emit(Events.LOCAL_FILES_UPDATED);
} catch (e) {
logError(e, 'Error in localFileUpdated handlers');
}
} catch (e1) {
try {
const storageEstimate = await navigator.storage.estimate();
logError(e1, 'failed to save files to indexedDB', {
storageEstimate,
});
addLogLine(`storage estimate ${JSON.stringify(storageEstimate)}`);
} catch (e2) {
logError(e1, 'failed to save files to indexedDB');
logError(e2, 'failed to get storage stats');
}
throw e1;
}
};
export const getAllLocalFiles = async () => {
const normalFiles = await getLocalFiles('normal');
const hiddenFiles = await getLocalFiles('hidden');
return [...normalFiles, ...hiddenFiles];
};
export const syncFiles = async (
type: 'normal' | 'hidden',
collections: Collection[],
setFiles: SetFiles
) => {
const localFiles = await getLocalFiles(type);
let files = await removeDeletedCollectionFiles(collections, localFiles);
if (files.length !== localFiles.length) {
await setLocalFiles(type, files);
setFiles(sortFiles(mergeMetadata(files)));
}
for (const collection of collections) {
if (!getToken()) {
continue;
}
const lastSyncTime = await getCollectionLastSyncTime(collection);
if (collection.updationTime === lastSyncTime) {
continue;
}
const newFiles = await getFiles(collection, lastSyncTime, setFiles);
files = getLatestVersionFiles([...files, ...newFiles]);
await setLocalFiles(type, files);
setCollectionLastSyncTime(collection, collection.updationTime);
}
return files;
};
export const getFiles = async (
collection: Collection,
sinceTime: number,
setFiles: SetFiles
): Promise<EnteFile[]> => {
try {
let decryptedFiles: EnteFile[] = [];
let time = sinceTime;
let resp;
do {
const token = getToken();
if (!token) {
break;
}
resp = await HTTPService.get(
`${ENDPOINT}/collections/v2/diff`,
{
collectionID: collection.id,
sinceTime: time,
},
{
'X-Auth-Token': token,
}
);
const newDecryptedFilesBatch = await Promise.all(
resp.data.diff.map(async (file: EncryptedEnteFile) => {
if (!file.isDeleted) {
return await decryptFile(file, collection.key);
} else {
return file;
}
}) as Promise<EnteFile>[]
);
decryptedFiles = [...decryptedFiles, ...newDecryptedFilesBatch];
setFiles((files) =>
sortFiles(
mergeMetadata(
getLatestVersionFiles([
...(files || []),
...decryptedFiles,
])
)
)
);
if (resp.data.diff.length) {
time = resp.data.diff.slice(-1)[0].updationTime;
}
} while (resp.data.hasMore);
return decryptedFiles;
} catch (e) {
logError(e, 'Get files failed');
throw e;
}
};
const removeDeletedCollectionFiles = async (
collections: Collection[],
files: EnteFile[]
) => {
const syncedCollectionIds = new Set<number>();
for (const collection of collections) {
syncedCollectionIds.add(collection.id);
}
files = files.filter((file) => syncedCollectionIds.has(file.collectionID));
return files;
};
export const trashFiles = async (filesToTrash: EnteFile[]) => {
try {
const token = getToken();
if (!token) {
return;
}
const batchedFilesToTrash = batch(filesToTrash, REQUEST_BATCH_SIZE);
for (const batch of batchedFilesToTrash) {
const trashRequest: TrashRequest = {
items: batch.map((file) => ({
fileID: file.id,
collectionID: file.collectionID,
})),
};
await HTTPService.post(
`${ENDPOINT}/files/trash`,
trashRequest,
null,
{
'X-Auth-Token': token,
}
);
}
} catch (e) {
logError(e, 'trash file failed');
throw e;
}
};
export const deleteFromTrash = async (filesToDelete: number[]) => {
try {
const token = getToken();
if (!token) {
return;
}
const batchedFilesToDelete = batch(filesToDelete, REQUEST_BATCH_SIZE);
for (const batch of batchedFilesToDelete) {
await HTTPService.post(
`${ENDPOINT}/trash/delete`,
{ fileIDs: batch },
null,
{
'X-Auth-Token': token,
}
);
}
} catch (e) {
logError(e, 'deleteFromTrash failed');
throw e;
}
};
export const updateFileMagicMetadata = async (
fileWithUpdatedMagicMetadataList: FileWithUpdatedMagicMetadata[]
) => {
const token = getToken();
if (!token) {
return;
}
const reqBody: BulkUpdateMagicMetadataRequest = { metadataList: [] };
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
for (const {
file,
updatedMagicMetadata,
} of fileWithUpdatedMagicMetadataList) {
const { file: encryptedMagicMetadata } =
await cryptoWorker.encryptMetadata(
updatedMagicMetadata.data,
file.key
);
reqBody.metadataList.push({
id: file.id,
magicMetadata: {
version: updatedMagicMetadata.version,
count: updatedMagicMetadata.count,
data: encryptedMagicMetadata.encryptedData,
header: encryptedMagicMetadata.decryptionHeader,
},
});
}
await HTTPService.put(`${ENDPOINT}/files/magic-metadata`, reqBody, null, {
'X-Auth-Token': token,
});
return fileWithUpdatedMagicMetadataList.map(
({ file, updatedMagicMetadata }): EnteFile => ({
...file,
magicMetadata: {
...updatedMagicMetadata,
version: updatedMagicMetadata.version + 1,
},
})
);
};
export const updateFilePublicMagicMetadata = async (
fileWithUpdatedPublicMagicMetadataList: FileWithUpdatedPublicMagicMetadata[]
): Promise<EnteFile[]> => {
const token = getToken();
if (!token) {
return;
}
const reqBody: BulkUpdateMagicMetadataRequest = { metadataList: [] };
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
for (const {
file,
updatedPublicMagicMetadata: updatePublicMagicMetadata,
} of fileWithUpdatedPublicMagicMetadataList) {
const { file: encryptedPubMagicMetadata } =
await cryptoWorker.encryptMetadata(
updatePublicMagicMetadata.data,
file.key
);
reqBody.metadataList.push({
id: file.id,
magicMetadata: {
version: updatePublicMagicMetadata.version,
count: updatePublicMagicMetadata.count,
data: encryptedPubMagicMetadata.encryptedData,
header: encryptedPubMagicMetadata.decryptionHeader,
},
});
}
await HTTPService.put(
`${ENDPOINT}/files/public-magic-metadata`,
reqBody,
null,
{
'X-Auth-Token': token,
}
);
return fileWithUpdatedPublicMagicMetadataList.map(
({ file, updatedPublicMagicMetadata }): EnteFile => ({
...file,
pubMagicMetadata: {
...updatedPublicMagicMetadata,
version: updatedPublicMagicMetadata.version + 1,
},
})
);
};

View file

@ -0,0 +1,757 @@
import { PAGES } from 'constants/pages';
import {
getEndpoint,
getFamilyPortalURL,
isDevDeployment,
} from 'utils/common/apiUtil';
import { clearKeys } from 'utils/storage/sessionStorage';
import router from 'next/router';
import { clearData, getData, LS_KEYS } from 'utils/storage/localStorage';
import localForage from 'utils/storage/localForage';
import { getToken } from 'utils/common/key';
import HTTPService from './HTTPService';
import {
computeVerifierHelper,
generateLoginSubKey,
generateSRPClient,
getRecoveryKey,
} from 'utils/crypto';
import { logError } from 'utils/sentry';
import { eventBus, Events } from './events';
import {
KeyAttributes,
RecoveryKey,
TwoFactorSecret,
TwoFactorVerificationResponse,
TwoFactorRecoveryResponse,
UserDetails,
DeleteChallengeResponse,
GetRemoteStoreValueResponse,
SetupSRPRequest,
CreateSRPSessionResponse,
UserVerificationResponse,
GetFeatureFlagResponse,
SetupSRPResponse,
CompleteSRPSetupRequest,
CompleteSRPSetupResponse,
SRPSetupAttributes,
SRPAttributes,
UpdateSRPAndKeysRequest,
UpdateSRPAndKeysResponse,
GetSRPAttributesResponse,
} from 'types/user';
import { ApiError, CustomError } from 'utils/error';
import isElectron from 'is-electron';
// import safeStorageService from './electron/safeStorage';
// import { deleteAllCache } from 'utils/storage/cache';
import { B64EncryptionResult } from 'types/crypto';
import { getLocalFamilyData, isPartOfFamily } from 'utils/user/family';
import { AxiosResponse, HttpStatusCode } from 'axios';
import { APPS, getAppName } from 'constants/apps';
import { addLocalLog } from 'utils/logging';
import { convertBase64ToBuffer, convertBufferToBase64 } from 'utils/user';
import { setLocalMapEnabled } from 'utils/storage';
import InMemoryStore, { MS_KEYS } from './InMemoryStore';
const ENDPOINT = getEndpoint();
const HAS_SET_KEYS = 'hasSetKeys';
export const sendOtt = (email: string) => {
const appName = getAppName();
return HTTPService.post(`${ENDPOINT}/users/ott`, {
email,
client: appName === APPS.AUTH ? 'totp' : 'web',
});
};
export const getPublicKey = async (email: string) => {
const token = getToken();
const resp = await HTTPService.get(
`${ENDPOINT}/users/public-key`,
{ email },
{
'X-Auth-Token': token,
}
);
return resp.data.publicKey;
};
export const getPaymentToken = async () => {
const token = getToken();
const resp = await HTTPService.get(
`${ENDPOINT}/users/payment-token`,
null,
{
'X-Auth-Token': token,
}
);
return resp.data['paymentToken'];
};
export const getFamiliesToken = async () => {
try {
const token = getToken();
const resp = await HTTPService.get(
`${ENDPOINT}/users/families-token`,
null,
{
'X-Auth-Token': token,
}
);
return resp.data['familiesToken'];
} catch (e) {
logError(e, 'failed to get family token');
throw e;
}
};
export const getRoadmapRedirectURL = async () => {
try {
const token = getToken();
const resp = await HTTPService.get(
`${ENDPOINT}/users/roadmap/v2`,
null,
{
'X-Auth-Token': token,
}
);
return resp.data['url'];
} catch (e) {
logError(e, 'failed to get roadmap url');
throw e;
}
};
export const verifyOtt = (email: string, ott: string) =>
HTTPService.post(`${ENDPOINT}/users/verify-email`, { email, ott });
export const putAttributes = (token: string, keyAttributes: KeyAttributes) =>
HTTPService.put(`${ENDPOINT}/users/attributes`, { keyAttributes }, null, {
'X-Auth-Token': token,
});
export const setRecoveryKey = (token: string, recoveryKey: RecoveryKey) =>
HTTPService.put(`${ENDPOINT}/users/recovery-key`, recoveryKey, null, {
'X-Auth-Token': token,
});
export const logoutUser = async () => {
try {
try {
// ignore server logout result as logoutUser can be triggered before sign up or on token expiry
await _logout();
} catch (e) {
//ignore
}
try {
InMemoryStore.clear();
} catch (e) {
logError(e, 'clear InMemoryStore failed');
}
try {
clearKeys();
} catch (e) {
logError(e, 'clearKeys failed');
}
try {
clearData();
} catch (e) {
logError(e, 'clearData failed');
}
try {
// await deleteAllCache();
} catch (e) {
logError(e, 'deleteAllCache failed');
}
try {
await clearFiles();
} catch (e) {
logError(e, 'clearFiles failed');
}
if (isElectron()) {
try {
// safeStorageService.clearElectronStore();
} catch (e) {
logError(e, 'clearElectronStore failed');
}
}
try {
eventBus.emit(Events.LOGOUT);
} catch (e) {
logError(e, 'Error in logout handlers');
}
router.push(PAGES.ROOT);
} catch (e) {
logError(e, 'logoutUser failed');
}
};
export const clearFiles = async () => {
await localForage.clear();
};
export const isTokenValid = async (token: string) => {
try {
const resp = await HTTPService.get(
`${ENDPOINT}/users/session-validity/v2`,
null,
{
'X-Auth-Token': token,
}
);
try {
if (resp.data[HAS_SET_KEYS] === undefined) {
throw Error('resp.data.hasSetKey undefined');
}
if (!resp.data['hasSetKeys']) {
try {
await putAttributes(
token,
getData(LS_KEYS.ORIGINAL_KEY_ATTRIBUTES)
);
} catch (e) {
logError(e, 'put attribute failed');
}
}
} catch (e) {
logError(e, 'hasSetKeys not set in session validity response');
}
return true;
} catch (e) {
logError(e, 'session-validity api call failed');
if (
e instanceof ApiError &&
e.httpStatusCode === HttpStatusCode.Unauthorized
) {
return false;
} else {
return true;
}
}
};
export const setupTwoFactor = async () => {
const resp = await HTTPService.post(
`${ENDPOINT}/users/two-factor/setup`,
null,
null,
{
'X-Auth-Token': getToken(),
}
);
return resp.data as TwoFactorSecret;
};
export const enableTwoFactor = async (
code: string,
recoveryEncryptedTwoFactorSecret: B64EncryptionResult
) => {
await HTTPService.post(
`${ENDPOINT}/users/two-factor/enable`,
{
code,
encryptedTwoFactorSecret:
recoveryEncryptedTwoFactorSecret.encryptedData,
twoFactorSecretDecryptionNonce:
recoveryEncryptedTwoFactorSecret.nonce,
},
null,
{
'X-Auth-Token': getToken(),
}
);
};
export const verifyTwoFactor = async (code: string, sessionID: string) => {
const resp = await HTTPService.post(
`${ENDPOINT}/users/two-factor/verify`,
{
code,
sessionID,
},
null
);
return resp.data as TwoFactorVerificationResponse;
};
export const recoverTwoFactor = async (sessionID: string) => {
const resp = await HTTPService.get(`${ENDPOINT}/users/two-factor/recover`, {
sessionID,
});
return resp.data as TwoFactorRecoveryResponse;
};
export const removeTwoFactor = async (sessionID: string, secret: string) => {
const resp = await HTTPService.post(`${ENDPOINT}/users/two-factor/remove`, {
sessionID,
secret,
});
return resp.data as TwoFactorVerificationResponse;
};
export const disableTwoFactor = async () => {
await HTTPService.post(`${ENDPOINT}/users/two-factor/disable`, null, null, {
'X-Auth-Token': getToken(),
});
};
export const getTwoFactorStatus = async () => {
const resp = await HTTPService.get(
`${ENDPOINT}/users/two-factor/status`,
null,
{
'X-Auth-Token': getToken(),
}
);
return resp.data['status'];
};
export const _logout = async () => {
if (!getToken()) return true;
try {
await HTTPService.post(`${ENDPOINT}/users/logout`, null, null, {
'X-Auth-Token': getToken(),
});
return true;
} catch (e) {
logError(e, '/users/logout failed');
return false;
}
};
export const sendOTTForEmailChange = async (email: string) => {
if (!getToken()) {
return null;
}
await HTTPService.post(`${ENDPOINT}/users/ott`, {
email,
client: 'web',
purpose: 'change',
});
};
export const changeEmail = async (email: string, ott: string) => {
if (!getToken()) {
return null;
}
await HTTPService.post(
`${ENDPOINT}/users/change-email`,
{
email,
ott,
},
null,
{
'X-Auth-Token': getToken(),
}
);
};
export const getUserDetailsV2 = async (): Promise<UserDetails> => {
try {
const token = getToken();
const resp = await HTTPService.get(
`${ENDPOINT}/users/details/v2`,
null,
{
'X-Auth-Token': token,
}
);
return resp.data;
} catch (e) {
logError(e, 'failed to get user details v2');
throw e;
}
};
export const getFamilyPortalRedirectURL = async () => {
try {
const jwtToken = await getFamiliesToken();
const isFamilyCreated = isPartOfFamily(getLocalFamilyData());
return `${getFamilyPortalURL()}?token=${jwtToken}&isFamilyCreated=${isFamilyCreated}&redirectURL=${
window.location.origin
}/gallery`;
} catch (e) {
logError(e, 'unable to generate to family portal URL');
throw e;
}
};
export const getAccountDeleteChallenge = async () => {
try {
const token = getToken();
const resp = await HTTPService.get(
`${ENDPOINT}/users/delete-challenge`,
null,
{
'X-Auth-Token': token,
}
);
return resp.data as DeleteChallengeResponse;
} catch (e) {
logError(e, 'failed to get account delete challenge');
throw e;
}
};
export const deleteAccount = async (
challenge: string,
reason: string,
feedback: string
) => {
try {
const token = getToken();
if (!token) {
return;
}
await HTTPService.delete(
`${ENDPOINT}/users/delete`,
{ challenge, reason, feedback },
null,
{
'X-Auth-Token': token,
}
);
} catch (e) {
logError(e, 'deleteAccount api call failed');
throw e;
}
};
// Ensure that the keys in local storage are not malformed by verifying that the
// recoveryKey can be decrypted with the masterKey.
// Note: This is not bullet-proof.
export const validateKey = async () => {
try {
await getRecoveryKey();
return true;
} catch (e) {
await logoutUser();
return false;
}
};
export const getFaceSearchEnabledStatus = async () => {
try {
const token = getToken();
const resp: AxiosResponse<GetRemoteStoreValueResponse> =
await HTTPService.get(
`${ENDPOINT}/remote-store`,
{
key: 'faceSearchEnabled',
defaultValue: false,
},
{
'X-Auth-Token': token,
}
);
return resp.data.value === 'true';
} catch (e) {
logError(e, 'failed to get face search enabled status');
throw e;
}
};
export const updateFaceSearchEnabledStatus = async (newStatus: boolean) => {
try {
const token = getToken();
await HTTPService.post(
`${ENDPOINT}/remote-store/update`,
{
key: 'faceSearchEnabled',
value: newStatus.toString(),
},
null,
{
'X-Auth-Token': token,
}
);
} catch (e) {
logError(e, 'failed to update face search enabled status');
throw e;
}
};
export const syncMapEnabled = async () => {
try {
const status = await getMapEnabledStatus();
setLocalMapEnabled(status);
} catch (e) {
logError(e, 'failed to sync map enabled status');
throw e;
}
};
export const getMapEnabledStatus = async () => {
try {
const token = getToken();
const resp: AxiosResponse<GetRemoteStoreValueResponse> =
await HTTPService.get(
`${ENDPOINT}/remote-store`,
{
key: 'mapEnabled',
defaultValue: false,
},
{
'X-Auth-Token': token,
}
);
return resp.data.value === 'true';
} catch (e) {
logError(e, 'failed to get map enabled status');
throw e;
}
};
export const updateMapEnabledStatus = async (newStatus: boolean) => {
try {
const token = getToken();
await HTTPService.post(
`${ENDPOINT}/remote-store/update`,
{
key: 'mapEnabled',
value: newStatus.toString(),
},
null,
{
'X-Auth-Token': token,
}
);
} catch (e) {
logError(e, 'failed to update map enabled status');
throw e;
}
};
export async function getDisableCFUploadProxyFlag(): Promise<boolean> {
try {
const disableCFUploadProxy =
process.env.NEXT_PUBLIC_DISABLE_CF_UPLOAD_PROXY;
if (isDevDeployment() && typeof disableCFUploadProxy !== 'undefined') {
return disableCFUploadProxy === 'true';
}
const featureFlags = (
await fetch('https://static.ente.io/feature_flags.json')
).json() as GetFeatureFlagResponse;
return featureFlags.disableCFUploadProxy;
} catch (e) {
logError(e, 'failed to get feature flags');
return false;
}
}
export const getSRPAttributes = async (
email: string
): Promise<SRPAttributes | null> => {
try {
const resp = await HTTPService.get(`${ENDPOINT}/users/srp/attributes`, {
email,
});
return (resp.data as GetSRPAttributesResponse).attributes;
} catch (e) {
logError(e, 'failed to get SRP attributes');
return null;
}
};
export const configureSRP = async ({
srpSalt,
srpUserID,
srpVerifier,
loginSubKey,
}: SRPSetupAttributes) => {
try {
const srpConfigureInProgress = InMemoryStore.get(
MS_KEYS.SRP_CONFIGURE_IN_PROGRESS
);
if (srpConfigureInProgress) {
throw Error('SRP configure already in progress');
}
InMemoryStore.set(MS_KEYS.SRP_CONFIGURE_IN_PROGRESS, true);
const srpClient = await generateSRPClient(
srpSalt,
srpUserID,
loginSubKey
);
const srpA = convertBufferToBase64(srpClient.computeA());
addLocalLog(() => `srp a: ${srpA}`);
const token = getToken();
const { setupID, srpB } = await startSRPSetup(token, {
srpA,
srpUserID,
srpSalt,
srpVerifier,
});
srpClient.setB(convertBase64ToBuffer(srpB));
const srpM1 = convertBufferToBase64(srpClient.computeM1());
const { srpM2 } = await completeSRPSetup(token, {
srpM1,
setupID,
});
srpClient.checkM2(convertBase64ToBuffer(srpM2));
} catch (e) {
logError(e, 'srp configure failed');
throw e;
} finally {
InMemoryStore.set(MS_KEYS.SRP_CONFIGURE_IN_PROGRESS, false);
}
};
export const startSRPSetup = async (
token: string,
setupSRPRequest: SetupSRPRequest
): Promise<SetupSRPResponse> => {
try {
const resp = await HTTPService.post(
`${ENDPOINT}/users/srp/setup`,
setupSRPRequest,
null,
{
'X-Auth-Token': token,
}
);
return resp.data as SetupSRPResponse;
} catch (e) {
logError(e, 'failed to post SRP attributes');
throw e;
}
};
export const completeSRPSetup = async (
token: string,
completeSRPSetupRequest: CompleteSRPSetupRequest
) => {
try {
const resp = await HTTPService.post(
`${ENDPOINT}/users/srp/complete`,
completeSRPSetupRequest,
null,
{
'X-Auth-Token': token,
}
);
return resp.data as CompleteSRPSetupResponse;
} catch (e) {
logError(e, 'failed to complete SRP setup');
throw e;
}
};
export const loginViaSRP = async (
srpAttributes: SRPAttributes,
kek: string
): Promise<UserVerificationResponse> => {
try {
const loginSubKey = await generateLoginSubKey(kek);
const srpClient = await generateSRPClient(
srpAttributes.srpSalt,
srpAttributes.srpUserID,
loginSubKey
);
const srpVerifier = computeVerifierHelper(
srpAttributes.srpSalt,
srpAttributes.srpUserID,
loginSubKey
);
addLocalLog(() => `srp verifier: ${srpVerifier}`);
const srpA = srpClient.computeA();
const { srpB, sessionID } = await createSRPSession(
srpAttributes.srpUserID,
convertBufferToBase64(srpA)
);
srpClient.setB(convertBase64ToBuffer(srpB));
const m1 = srpClient.computeM1();
addLocalLog(() => `srp m1: ${convertBufferToBase64(m1)}`);
const { srpM2, ...rest } = await verifySRPSession(
sessionID,
srpAttributes.srpUserID,
convertBufferToBase64(m1)
);
addLocalLog(() => `srp verify session successful,srpM2: ${srpM2}`);
srpClient.checkM2(convertBase64ToBuffer(srpM2));
addLocalLog(() => `srp server verify successful`);
return rest;
} catch (e) {
logError(e, 'srp verify failed');
throw e;
}
};
export const createSRPSession = async (srpUserID: string, srpA: string) => {
try {
const resp = await HTTPService.post(
`${ENDPOINT}/users/srp/create-session`,
{
srpUserID,
srpA,
}
);
return resp.data as CreateSRPSessionResponse;
} catch (e) {
logError(e, 'createSRPSession failed');
throw e;
}
};
export const verifySRPSession = async (
sessionID: string,
srpUserID: string,
srpM1: string
) => {
try {
const resp = await HTTPService.post(
`${ENDPOINT}/users/srp/verify-session`,
{
sessionID,
srpUserID,
srpM1,
},
null
);
return resp.data as UserVerificationResponse;
} catch (e) {
logError(e, 'verifySRPSession failed');
if (
e instanceof ApiError &&
e.httpStatusCode === HttpStatusCode.Forbidden
) {
throw Error(CustomError.INCORRECT_PASSWORD);
} else {
throw e;
}
}
};
export const updateSRPAndKeys = async (
token: string,
updateSRPAndKeyRequest: UpdateSRPAndKeysRequest
): Promise<UpdateSRPAndKeysResponse> => {
const resp = await HTTPService.post(
`${ENDPOINT}/users/srp/update`,
updateSRPAndKeyRequest,
null,
{
'X-Auth-Token': token,
}
);
return resp.data as UpdateSRPAndKeysResponse;
};

View file

@ -0,0 +1,229 @@
.main {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
padding: 6rem;
min-height: 100vh;
}
.description {
display: inherit;
justify-content: inherit;
align-items: inherit;
font-size: 0.85rem;
max-width: var(--max-width);
width: 100%;
z-index: 2;
font-family: var(--font-mono);
}
.description a {
display: flex;
justify-content: center;
align-items: center;
gap: 0.5rem;
}
.description p {
position: relative;
margin: 0;
padding: 1rem;
background-color: rgba(var(--callout-rgb), 0.5);
border: 1px solid rgba(var(--callout-border-rgb), 0.3);
border-radius: var(--border-radius);
}
.code {
font-weight: 700;
font-family: var(--font-mono);
}
.grid {
display: grid;
grid-template-columns: repeat(4, minmax(25%, auto));
max-width: 100%;
width: var(--max-width);
}
.card {
padding: 1rem 1.2rem;
border-radius: var(--border-radius);
background: rgba(var(--card-rgb), 0);
border: 1px solid rgba(var(--card-border-rgb), 0);
transition: background 200ms, border 200ms;
}
.card span {
display: inline-block;
transition: transform 200ms;
}
.card h2 {
font-weight: 600;
margin-bottom: 0.7rem;
}
.card p {
margin: 0;
opacity: 0.6;
font-size: 0.9rem;
line-height: 1.5;
max-width: 30ch;
}
.center {
display: flex;
justify-content: center;
align-items: center;
position: relative;
padding: 4rem 0;
}
.center::before {
background: var(--secondary-glow);
border-radius: 50%;
width: 480px;
height: 360px;
margin-left: -400px;
}
.center::after {
background: var(--primary-glow);
width: 240px;
height: 180px;
z-index: -1;
}
.center::before,
.center::after {
content: '';
left: 50%;
position: absolute;
filter: blur(45px);
transform: translateZ(0);
}
.logo {
position: relative;
}
/* Enable hover only on non-touch devices */
@media (hover: hover) and (pointer: fine) {
.card:hover {
background: rgba(var(--card-rgb), 0.1);
border: 1px solid rgba(var(--card-border-rgb), 0.15);
}
.card:hover span {
transform: translateX(4px);
}
}
@media (prefers-reduced-motion) {
.card:hover span {
transform: none;
}
}
/* Mobile */
@media (max-width: 700px) {
.content {
padding: 4rem;
}
.grid {
grid-template-columns: 1fr;
margin-bottom: 120px;
max-width: 320px;
text-align: center;
}
.card {
padding: 1rem 2.5rem;
}
.card h2 {
margin-bottom: 0.5rem;
}
.center {
padding: 8rem 0 6rem;
}
.center::before {
transform: none;
height: 300px;
}
.description {
font-size: 0.8rem;
}
.description a {
padding: 1rem;
}
.description p,
.description div {
display: flex;
justify-content: center;
position: fixed;
width: 100%;
}
.description p {
align-items: center;
inset: 0 0 auto;
padding: 2rem 1rem 1.4rem;
border-radius: 0;
border: none;
border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25);
background: linear-gradient(
to bottom,
rgba(var(--background-start-rgb), 1),
rgba(var(--callout-rgb), 0.5)
);
background-clip: padding-box;
backdrop-filter: blur(24px);
}
.description div {
align-items: flex-end;
pointer-events: none;
inset: auto 0 0;
padding: 2rem;
height: 200px;
background: linear-gradient(
to bottom,
transparent 0%,
rgb(var(--background-end-rgb)) 40%
);
z-index: 1;
}
}
/* Tablet and Smaller Desktop */
@media (min-width: 701px) and (max-width: 1120px) {
.grid {
grid-template-columns: repeat(2, 50%);
}
}
@media (prefers-color-scheme: dark) {
.vercelLogo {
filter: invert(1);
}
.logo {
filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70);
}
}
@keyframes rotate {
from {
transform: rotate(360deg);
}
to {
transform: rotate(0deg);
}
}

View file

@ -0,0 +1,110 @@
:root {
--max-width: 1100px;
--border-radius: 12px;
--font-mono: ui-monospace, Menlo, Monaco, 'Cascadia Mono', 'Segoe UI Mono',
'Roboto Mono', 'Oxygen Mono', 'Ubuntu Monospace', 'Source Code Pro',
'Fira Mono', 'Droid Sans Mono', 'Courier New', monospace;
--foreground-rgb: 0, 0, 0;
--background-start-rgb: 214, 219, 220;
--background-end-rgb: 255, 255, 255;
--primary-glow: conic-gradient(
from 180deg at 50% 50%,
#16abff33 0deg,
#0885ff33 55deg,
#54d6ff33 120deg,
#0071ff33 160deg,
transparent 360deg
);
--secondary-glow: radial-gradient(
rgba(255, 255, 255, 1),
rgba(255, 255, 255, 0)
);
--tile-start-rgb: 239, 245, 249;
--tile-end-rgb: 228, 232, 233;
--tile-border: conic-gradient(
#00000080,
#00000040,
#00000030,
#00000020,
#00000010,
#00000010,
#00000080
);
--callout-rgb: 238, 240, 241;
--callout-border-rgb: 172, 175, 176;
--card-rgb: 180, 185, 188;
--card-border-rgb: 131, 134, 135;
}
@media (prefers-color-scheme: dark) {
:root {
--foreground-rgb: 255, 255, 255;
--background-start-rgb: 0, 0, 0;
--background-end-rgb: 0, 0, 0;
--primary-glow: radial-gradient(
rgba(1, 65, 255, 0.4),
rgba(1, 65, 255, 0)
);
--secondary-glow: linear-gradient(
to bottom right,
rgba(1, 65, 255, 0),
rgba(1, 65, 255, 0),
rgba(1, 65, 255, 0.3)
);
--tile-start-rgb: 2, 13, 46;
--tile-end-rgb: 2, 5, 19;
--tile-border: conic-gradient(
#ffffff80,
#ffffff40,
#ffffff30,
#ffffff20,
#ffffff10,
#ffffff10,
#ffffff80
);
--callout-rgb: 20, 20, 20;
--callout-border-rgb: 108, 108, 108;
--card-rgb: 100, 100, 100;
--card-border-rgb: 200, 200, 200;
}
}
* {
box-sizing: border-box;
padding: 0;
margin: 0;
}
html,
body {
max-width: 100vw;
overflow-x: hidden;
}
body {
color: rgb(var(--foreground-rgb));
background: linear-gradient(
to bottom,
transparent,
rgb(var(--background-end-rgb))
)
rgb(var(--background-start-rgb));
}
a {
color: inherit;
text-decoration: none;
}
@media (prefers-color-scheme: dark) {
html {
color-scheme: dark;
}
}

View file

@ -0,0 +1,25 @@
import { PLAN_PERIOD } from 'constants/gallery';
export interface Subscription {
id: number;
userID: number;
productID: string;
storage: number;
originalTransactionID: string;
expiryTime: number;
paymentProvider: string;
attributes: {
isCancelled: boolean;
};
price: string;
period: PLAN_PERIOD;
}
export interface Plan {
id: string;
androidID: string;
iosID: string;
storage: number;
price: string;
period: PLAN_PERIOD;
stripeID: string;
}

20
apps/cast/src/types/cache/index.ts vendored Normal file
View file

@ -0,0 +1,20 @@
export interface LimitedCacheStorage {
open: (cacheName: string) => Promise<LimitedCache>;
delete: (cacheName: string) => Promise<boolean>;
}
export interface LimitedCache {
match: (key: string) => Promise<Response>;
put: (key: string, data: Response) => Promise<void>;
delete: (key: string) => Promise<boolean>;
}
export interface ProxiedLimitedCacheStorage {
open: (cacheName: string) => Promise<ProxiedWorkerLimitedCache>;
delete: (cacheName: string) => Promise<boolean>;
}
export interface ProxiedWorkerLimitedCache {
match: (key: string) => Promise<ArrayBuffer>;
put: (key: string, data: ArrayBuffer) => Promise<void>;
delete: (key: string) => Promise<boolean>;
}

View file

@ -0,0 +1,156 @@
import { EnteFile } from 'types/file';
import { CollectionSummaryType, CollectionType } from 'constants/collection';
import {
EncryptedMagicMetadata,
MagicMetadataCore,
SUB_TYPE,
VISIBILITY_STATE,
} from 'types/magicMetadata';
export enum COLLECTION_ROLE {
VIEWER = 'VIEWER',
OWNER = 'OWNER',
COLLABORATOR = 'COLLABORATOR',
UNKNOWN = 'UNKNOWN',
}
export interface CollectionUser {
id: number;
email: string;
role: COLLECTION_ROLE;
}
export interface EncryptedCollection {
id: number;
owner: CollectionUser;
// collection name was unencrypted in the past, so we need to keep it as optional
name?: string;
encryptedKey: string;
keyDecryptionNonce: string;
encryptedName: string;
nameDecryptionNonce: string;
type: CollectionType;
attributes: collectionAttributes;
sharees: CollectionUser[];
publicURLs?: PublicURL[];
updationTime: number;
isDeleted: boolean;
magicMetadata: EncryptedMagicMetadata;
pubMagicMetadata: EncryptedMagicMetadata;
sharedMagicMetadata: EncryptedMagicMetadata;
}
export interface Collection
extends Omit<
EncryptedCollection,
| 'encryptedKey'
| 'keyDecryptionNonce'
| 'encryptedName'
| 'nameDecryptionNonce'
| 'magicMetadata'
| 'pubMagicMetadata'
| 'sharedMagicMetadata'
> {
key: string;
name: string;
magicMetadata: CollectionMagicMetadata;
pubMagicMetadata: CollectionPublicMagicMetadata;
sharedMagicMetadata: CollectionShareeMagicMetadata;
}
export interface PublicURL {
url: string;
deviceLimit: number;
validTill: number;
enableDownload: boolean;
enableCollect: boolean;
passwordEnabled: boolean;
nonce?: string;
opsLimit?: number;
memLimit?: number;
}
export interface UpdatePublicURL {
collectionID: number;
disablePassword?: boolean;
enableDownload?: boolean;
enableCollect?: boolean;
validTill?: number;
deviceLimit?: number;
passHash?: string;
nonce?: string;
opsLimit?: number;
memLimit?: number;
}
export interface CreatePublicAccessTokenRequest {
collectionID: number;
validTill?: number;
deviceLimit?: number;
}
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 type CollectionToFileMap = Map<number, EnteFile>;
export interface RemoveFromCollectionRequest {
collectionID: number;
fileIDs: number[];
}
export interface CollectionMagicMetadataProps {
visibility?: VISIBILITY_STATE;
subType?: SUB_TYPE;
order?: number;
}
export type CollectionMagicMetadata =
MagicMetadataCore<CollectionMagicMetadataProps>;
export interface CollectionShareeMetadataProps {
visibility?: VISIBILITY_STATE;
}
export type CollectionShareeMagicMetadata =
MagicMetadataCore<CollectionShareeMetadataProps>;
export interface CollectionPublicMagicMetadataProps {
asc?: boolean;
coverID?: number;
}
export type CollectionPublicMagicMetadata =
MagicMetadataCore<CollectionPublicMagicMetadataProps>;
export interface CollectionSummary {
id: number;
name: string;
type: CollectionSummaryType;
coverFile: EnteFile;
latestFile: EnteFile;
fileCount: number;
updationTime: number;
order?: number;
}
export type CollectionSummaries = Map<number, CollectionSummary>;
export type CollectionFilesCount = Map<number, number>;

View file

@ -0,0 +1,19 @@
import { DataStream } from 'types/upload';
export interface LocalFileAttributes<
T extends string | Uint8Array | DataStream
> {
encryptedData: T;
decryptionHeader: string;
}
export interface EncryptionResult<T extends string | Uint8Array | DataStream> {
file: LocalFileAttributes<T>;
key: string;
}
export interface B64EncryptionResult {
encryptedData: string;
key: string;
nonce: string;
}

View file

@ -0,0 +1,422 @@
import sodium, { StateAddress } from 'libsodium-wrappers';
import { ENCRYPTION_CHUNK_SIZE } from 'constants/crypto';
import { B64EncryptionResult } from 'types/crypto';
import { CustomError } from 'utils/error';
export async function decryptChaChaOneShot(
data: Uint8Array,
header: Uint8Array,
key: string
) {
await sodium.ready;
const pullState = sodium.crypto_secretstream_xchacha20poly1305_init_pull(
header,
await fromB64(key)
);
const pullResult = sodium.crypto_secretstream_xchacha20poly1305_pull(
pullState,
data,
null
);
return pullResult.message;
}
export async function decryptChaCha(
data: Uint8Array,
header: Uint8Array,
key: string
) {
await sodium.ready;
const pullState = sodium.crypto_secretstream_xchacha20poly1305_init_pull(
header,
await fromB64(key)
);
const decryptionChunkSize =
ENCRYPTION_CHUNK_SIZE +
sodium.crypto_secretstream_xchacha20poly1305_ABYTES;
let bytesRead = 0;
const decryptedData = [];
let tag = sodium.crypto_secretstream_xchacha20poly1305_TAG_MESSAGE;
while (tag !== sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL) {
let chunkSize = decryptionChunkSize;
if (bytesRead + chunkSize > data.length) {
chunkSize = data.length - bytesRead;
}
const buffer = data.slice(bytesRead, bytesRead + chunkSize);
const pullResult = sodium.crypto_secretstream_xchacha20poly1305_pull(
pullState,
buffer
);
if (!pullResult.message) {
throw new Error(CustomError.PROCESSING_FAILED);
}
for (let index = 0; index < pullResult.message.length; index++) {
decryptedData.push(pullResult.message[index]);
}
tag = pullResult.tag;
bytesRead += chunkSize;
}
return Uint8Array.from(decryptedData);
}
export async function initChunkDecryption(header: Uint8Array, key: Uint8Array) {
await sodium.ready;
const pullState = sodium.crypto_secretstream_xchacha20poly1305_init_pull(
header,
key
);
const decryptionChunkSize =
ENCRYPTION_CHUNK_SIZE +
sodium.crypto_secretstream_xchacha20poly1305_ABYTES;
const tag = sodium.crypto_secretstream_xchacha20poly1305_TAG_MESSAGE;
return { pullState, decryptionChunkSize, tag };
}
export async function decryptFileChunk(
data: Uint8Array,
pullState: StateAddress
) {
await sodium.ready;
const pullResult = sodium.crypto_secretstream_xchacha20poly1305_pull(
pullState,
data
);
if (!pullResult.message) {
throw new Error(CustomError.PROCESSING_FAILED);
}
const newTag = pullResult.tag;
return { decryptedData: pullResult.message, newTag };
}
export async function encryptChaChaOneShot(data: Uint8Array, key: string) {
await sodium.ready;
const uintkey: Uint8Array = await fromB64(key);
const initPushResult =
sodium.crypto_secretstream_xchacha20poly1305_init_push(uintkey);
const [pushState, header] = [initPushResult.state, initPushResult.header];
const pushResult = sodium.crypto_secretstream_xchacha20poly1305_push(
pushState,
data,
null,
sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL
);
return {
key: await toB64(uintkey),
file: {
encryptedData: pushResult,
decryptionHeader: await toB64(header),
},
};
}
export async function encryptChaCha(data: Uint8Array) {
await sodium.ready;
const uintkey: Uint8Array =
sodium.crypto_secretstream_xchacha20poly1305_keygen();
const initPushResult =
sodium.crypto_secretstream_xchacha20poly1305_init_push(uintkey);
const [pushState, header] = [initPushResult.state, initPushResult.header];
let bytesRead = 0;
let tag = sodium.crypto_secretstream_xchacha20poly1305_TAG_MESSAGE;
const encryptedData = [];
while (tag !== sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL) {
let chunkSize = ENCRYPTION_CHUNK_SIZE;
if (bytesRead + chunkSize >= data.length) {
chunkSize = data.length - bytesRead;
tag = sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL;
}
const buffer = data.slice(bytesRead, bytesRead + chunkSize);
bytesRead += chunkSize;
const pushResult = sodium.crypto_secretstream_xchacha20poly1305_push(
pushState,
buffer,
null,
tag
);
for (let index = 0; index < pushResult.length; index++) {
encryptedData.push(pushResult[index]);
}
}
return {
key: await toB64(uintkey),
file: {
encryptedData: new Uint8Array(encryptedData),
decryptionHeader: await toB64(header),
},
};
}
export async function initChunkEncryption() {
await sodium.ready;
const key = sodium.crypto_secretstream_xchacha20poly1305_keygen();
const initPushResult =
sodium.crypto_secretstream_xchacha20poly1305_init_push(key);
const [pushState, header] = [initPushResult.state, initPushResult.header];
return {
key: await toB64(key),
decryptionHeader: await toB64(header),
pushState,
};
}
export async function encryptFileChunk(
data: Uint8Array,
pushState: sodium.StateAddress,
isFinalChunk: boolean
) {
await sodium.ready;
const tag = isFinalChunk
? sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL
: sodium.crypto_secretstream_xchacha20poly1305_TAG_MESSAGE;
const pushResult = sodium.crypto_secretstream_xchacha20poly1305_push(
pushState,
data,
null,
tag
);
return pushResult;
}
export async function encryptToB64(data: string, key: string) {
await sodium.ready;
const encrypted = await encrypt(await fromB64(data), await fromB64(key));
return {
encryptedData: await toB64(encrypted.encryptedData),
key: await toB64(encrypted.key),
nonce: await toB64(encrypted.nonce),
} as B64EncryptionResult;
}
export async function generateKeyAndEncryptToB64(data: string) {
await sodium.ready;
const key = sodium.crypto_secretbox_keygen();
return await encryptToB64(data, await toB64(key));
}
export async function encryptUTF8(data: string, key: string) {
const b64Data = await toB64(await fromUTF8(data));
return await encryptToB64(b64Data, key);
}
export async function decryptB64(data: string, nonce: string, key: string) {
await sodium.ready;
const decrypted = await decrypt(
await fromB64(data),
await fromB64(nonce),
await fromB64(key)
);
return await toB64(decrypted);
}
export async function decryptToUTF8(data: string, nonce: string, key: string) {
await sodium.ready;
const decrypted = await decrypt(
await fromB64(data),
await fromB64(nonce),
await fromB64(key)
);
return sodium.to_string(decrypted);
}
async function encrypt(data: Uint8Array, key: Uint8Array) {
await sodium.ready;
const nonce = sodium.randombytes_buf(sodium.crypto_secretbox_NONCEBYTES);
const encryptedData = sodium.crypto_secretbox_easy(data, nonce, key);
return {
encryptedData,
key,
nonce,
};
}
async function decrypt(data: Uint8Array, nonce: Uint8Array, key: Uint8Array) {
await sodium.ready;
return sodium.crypto_secretbox_open_easy(data, nonce, key);
}
export async function initChunkHashing() {
await sodium.ready;
const hashState = sodium.crypto_generichash_init(
null,
sodium.crypto_generichash_BYTES_MAX
);
return hashState;
}
export async function hashFileChunk(
hashState: sodium.StateAddress,
chunk: Uint8Array
) {
await sodium.ready;
sodium.crypto_generichash_update(hashState, chunk);
}
export async function completeChunkHashing(hashState: sodium.StateAddress) {
await sodium.ready;
const hash = sodium.crypto_generichash_final(
hashState,
sodium.crypto_generichash_BYTES_MAX
);
const hashString = toB64(hash);
return hashString;
}
export async function deriveKey(
passphrase: string,
salt: string,
opsLimit: number,
memLimit: number
) {
await sodium.ready;
return await toB64(
sodium.crypto_pwhash(
sodium.crypto_secretbox_KEYBYTES,
await fromUTF8(passphrase),
await fromB64(salt),
opsLimit,
memLimit,
sodium.crypto_pwhash_ALG_ARGON2ID13
)
);
}
export async function deriveSensitiveKey(passphrase: string, salt: string) {
await sodium.ready;
const minMemLimit = sodium.crypto_pwhash_MEMLIMIT_MIN;
let opsLimit = sodium.crypto_pwhash_OPSLIMIT_SENSITIVE;
let memLimit = sodium.crypto_pwhash_MEMLIMIT_SENSITIVE;
while (memLimit > minMemLimit) {
try {
const key = await deriveKey(passphrase, salt, opsLimit, memLimit);
return {
key,
opsLimit,
memLimit,
};
} catch (e) {
opsLimit *= 2;
memLimit /= 2;
}
}
}
export async function deriveInteractiveKey(passphrase: string, salt: string) {
await sodium.ready;
const key = await toB64(
sodium.crypto_pwhash(
sodium.crypto_secretbox_KEYBYTES,
await fromUTF8(passphrase),
await fromB64(salt),
sodium.crypto_pwhash_OPSLIMIT_INTERACTIVE,
sodium.crypto_pwhash_MEMLIMIT_INTERACTIVE,
sodium.crypto_pwhash_ALG_ARGON2ID13
)
);
return {
key,
opsLimit: sodium.crypto_pwhash_OPSLIMIT_INTERACTIVE,
memLimit: sodium.crypto_pwhash_MEMLIMIT_INTERACTIVE,
};
}
export async function generateEncryptionKey() {
await sodium.ready;
return await toB64(sodium.crypto_kdf_keygen());
}
export async function generateSaltToDeriveKey() {
await sodium.ready;
return await toB64(sodium.randombytes_buf(sodium.crypto_pwhash_SALTBYTES));
}
export async function generateKeyPair() {
await sodium.ready;
const keyPair: sodium.KeyPair = sodium.crypto_box_keypair();
return {
privateKey: await toB64(keyPair.privateKey),
publicKey: await toB64(keyPair.publicKey),
};
}
export async function boxSealOpen(
input: string,
publicKey: string,
secretKey: string
) {
await sodium.ready;
return await toB64(
sodium.crypto_box_seal_open(
await fromB64(input),
await fromB64(publicKey),
await fromB64(secretKey)
)
);
}
export async function boxSeal(input: string, publicKey: string) {
await sodium.ready;
return await toB64(
sodium.crypto_box_seal(await fromB64(input), await fromB64(publicKey))
);
}
export async function generateSubKey(
key: string,
subKeyLength: number,
subKeyID: number,
context: string
) {
await sodium.ready;
return await toB64(
sodium.crypto_kdf_derive_from_key(
subKeyLength,
subKeyID,
context,
await fromB64(key)
)
);
}
export async function fromB64(input: string) {
await sodium.ready;
return sodium.from_base64(input, sodium.base64_variants.ORIGINAL);
}
export async function toB64(input: Uint8Array) {
await sodium.ready;
return sodium.to_base64(input, sodium.base64_variants.ORIGINAL);
}
export async function toURLSafeB64(input: Uint8Array) {
await sodium.ready;
return sodium.to_base64(input, sodium.base64_variants.URLSAFE);
}
export async function fromUTF8(input: string) {
await sodium.ready;
return sodium.from_string(input);
}
export async function toUTF8(input: string) {
await sodium.ready;
return sodium.to_string(await fromB64(input));
}
export async function toHex(input: string) {
await sodium.ready;
return sodium.to_hex(await fromB64(input));
}
export async function fromHex(input: string) {
await sodium.ready;
return await toB64(sodium.from_hex(input));
}

View file

@ -0,0 +1,103 @@
import {
EncryptedMagicMetadata,
MagicMetadataCore,
VISIBILITY_STATE,
} from 'types/magicMetadata';
import { Metadata } from 'types/upload';
export interface MetadataFileAttributes {
encryptedData: string;
decryptionHeader: string;
}
export interface S3FileAttributes {
objectKey: string;
decryptionHeader: string;
}
export interface FileInfo {
fileSize: number;
thumbSize: number;
}
export interface EncryptedEnteFile {
id: number;
collectionID: number;
ownerID: number;
file: S3FileAttributes;
thumbnail: S3FileAttributes;
metadata: MetadataFileAttributes;
info: FileInfo;
magicMetadata: EncryptedMagicMetadata;
pubMagicMetadata: EncryptedMagicMetadata;
encryptedKey: string;
keyDecryptionNonce: string;
isDeleted: boolean;
updationTime: number;
}
export interface EnteFile
extends Omit<
EncryptedEnteFile,
| 'metadata'
| 'pubMagicMetadata'
| 'magicMetadata'
| 'encryptedKey'
| 'keyDecryptionNonce'
> {
metadata: Metadata;
magicMetadata: FileMagicMetadata;
pubMagicMetadata: FilePublicMagicMetadata;
isTrashed?: boolean;
key: string;
src?: string;
msrc?: string;
html?: string;
w?: number;
h?: number;
title?: string;
deleteBy?: number;
isSourceLoaded?: boolean;
originalVideoURL?: string;
originalImageURL?: string;
dataIndex?: number;
conversionFailed?: boolean;
isConverted?: boolean;
}
export interface TrashRequest {
items: TrashRequestItems[];
}
export interface TrashRequestItems {
fileID: number;
collectionID: number;
}
export interface FileWithUpdatedMagicMetadata {
file: EnteFile;
updatedMagicMetadata: FileMagicMetadata;
}
export interface FileWithUpdatedPublicMagicMetadata {
file: EnteFile;
updatedPublicMagicMetadata: FilePublicMagicMetadata;
}
export interface FileMagicMetadataProps {
visibility?: VISIBILITY_STATE;
filePaths?: string[];
}
export type FileMagicMetadata = MagicMetadataCore<FileMagicMetadataProps>;
export interface FilePublicMagicMetadataProps {
editedTime?: number;
editedName?: string;
caption?: string;
uploaderName?: string;
w?: number;
h?: number;
}
export type FilePublicMagicMetadata =
MagicMetadataCore<FilePublicMagicMetadataProps>;

View file

@ -0,0 +1,57 @@
// import { CollectionDownloadProgressAttributes } from 'components/Collections/CollectionDownloadProgress';
// import { CollectionSelectorAttributes } from 'components/Collections/CollectionSelector';
// import { TimeStampListItem } from 'components/PhotoList';
import { Collection } from 'types/collection';
import { EnteFile } from 'types/file';
import { User } from 'types/user';
export type SelectedState = {
[k: number]: boolean;
ownCount: number;
count: number;
collectionID: number;
};
export type SetFiles = React.Dispatch<React.SetStateAction<EnteFile[]>>;
export type SetCollections = React.Dispatch<React.SetStateAction<Collection[]>>;
export type SetLoading = React.Dispatch<React.SetStateAction<boolean>>;
// export type SetCollectionSelectorAttributes = React.Dispatch<
// React.SetStateAction<CollectionSelectorAttributes>
// >;
// export type SetCollectionDownloadProgressAttributes = React.Dispatch<
// React.SetStateAction<CollectionDownloadProgressAttributes>
// >;
export type MergedSourceURL = {
original: string;
converted: string;
};
export enum UploadTypeSelectorIntent {
normalUpload,
import,
collectPhotos,
}
export type GalleryContextType = {
thumbs: Map<number, string>;
files: Map<number, MergedSourceURL>;
showPlanSelectorModal: () => void;
setActiveCollectionID: (collectionID: number) => void;
syncWithRemote: (force?: boolean, silent?: boolean) => Promise<void>;
setBlockingLoad: (value: boolean) => void;
setIsInSearchMode: (value: boolean) => void;
// photoListHeader: TimeStampListItem;
openExportModal: () => void;
authenticateUser: (callback: () => void) => void;
user: User;
userIDToEmailMap: Map<number, string>;
emailList: string[];
openHiddenSection: (callback?: () => void) => void;
isClipSearchResult: boolean;
};
export enum CollectionSelectorIntent {
upload,
add,
move,
restore,
unhide,
}

View file

@ -0,0 +1,29 @@
export interface MagicMetadataCore<T> {
version: number;
count: number;
header: string;
data: T;
}
export type EncryptedMagicMetadata = MagicMetadataCore<string>;
export enum VISIBILITY_STATE {
VISIBLE = 0,
ARCHIVED = 1,
HIDDEN = 2,
}
export enum SUB_TYPE {
DEFAULT = 0,
DEFAULT_HIDDEN = 1,
QUICK_LINK_COLLECTION = 2,
}
export interface BulkUpdateMagicMetadataRequest {
metadataList: UpdateMagicMetadataRequest[];
}
export interface UpdateMagicMetadataRequest {
id: number;
magicMetadata: EncryptedMagicMetadata;
}

View file

@ -0,0 +1,167 @@
import { FILE_TYPE } from 'constants/file';
import { Collection } from 'types/collection';
import { B64EncryptionResult, LocalFileAttributes } from 'types/crypto';
import {
MetadataFileAttributes,
S3FileAttributes,
FilePublicMagicMetadata,
FilePublicMagicMetadataProps,
} from 'types/file';
import { EncryptedMagicMetadata } from 'types/magicMetadata';
export interface DataStream {
stream: ReadableStream<Uint8Array>;
chunkCount: number;
}
export function isDataStream(object: any): object is DataStream {
return 'stream' in object;
}
export type Logger = (message: string) => void;
export interface Metadata {
title: string;
creationTime: number;
modificationTime: number;
latitude: number;
longitude: number;
fileType: FILE_TYPE;
hasStaticThumbnail?: boolean;
hash?: string;
imageHash?: string;
videoHash?: string;
localID?: number;
version?: number;
deviceFolder?: string;
}
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;
mimeType?: string;
imageType?: string;
videoType?: string;
}
/*
* ElectronFile is a custom interface that is used to represent
* any file on disk as a File-like object in the Electron desktop app.
*
* This was added to support the auto-resuming of failed uploads
* which needed absolute paths to the files which the
* normal File interface does not provide.
*/
export interface ElectronFile {
name: string;
path: string;
size: number;
lastModified: number;
stream: () => Promise<ReadableStream<Uint8Array>>;
blob: () => Promise<Blob>;
arrayBuffer: () => Promise<Uint8Array>;
}
export interface UploadAsset {
isLivePhoto?: boolean;
file?: File | ElectronFile;
livePhotoAssets?: LivePhotoAssets;
isElectron?: boolean;
}
export interface LivePhotoAssets {
image: globalThis.File | ElectronFile;
video: globalThis.File | ElectronFile;
}
export interface FileWithCollection extends UploadAsset {
localID: number;
collection?: Collection;
collectionID?: number;
}
export type ParsedMetadataJSONMap = Map<string, ParsedMetadataJSON>;
export interface UploadURL {
url: string;
objectKey: string;
}
export interface FileInMemory {
filedata: Uint8Array | DataStream;
thumbnail: Uint8Array;
hasStaticThumbnail: boolean;
}
export interface FileWithMetadata
extends Omit<FileInMemory, 'hasStaticThumbnail'> {
metadata: Metadata;
localID: number;
pubMagicMetadata: FilePublicMagicMetadata;
}
export interface EncryptedFile {
file: ProcessedFile;
fileKey: B64EncryptionResult;
}
export interface ProcessedFile {
file: LocalFileAttributes<Uint8Array | DataStream>;
thumbnail: LocalFileAttributes<Uint8Array>;
metadata: LocalFileAttributes<string>;
pubMagicMetadata: EncryptedMagicMetadata;
localID: number;
}
export interface BackupedFile {
file: S3FileAttributes;
thumbnail: S3FileAttributes;
metadata: MetadataFileAttributes;
pubMagicMetadata: EncryptedMagicMetadata;
}
export interface UploadFile extends BackupedFile {
collectionID: number;
encryptedKey: string;
keyDecryptionNonce: string;
}
export interface ParsedExtractedMetadata {
location: Location;
creationTime: number;
width: number;
height: number;
}
// This is used to prompt the user the make upload strategy choice
export interface ImportSuggestion {
rootFolderName: string;
hasNestedFolders: boolean;
hasRootLevelFileWithFolder: boolean;
}
export interface PublicUploadProps {
token: string;
passwordToken: string;
accessedThroughSharedURL: boolean;
}
export interface ExtractMetadataResult {
metadata: Metadata;
publicMagicMetadata: FilePublicMagicMetadataProps;
}

View file

@ -0,0 +1,43 @@
import { UPLOAD_RESULT, UPLOAD_STAGES } from 'constants/upload';
export type FileID = number;
export type FileName = string;
export type PercentageUploaded = number;
export type UploadFileNames = Map<FileID, FileName>;
export interface UploadCounter {
finished: number;
total: number;
}
export interface InProgressUpload {
localFileID: FileID;
progress: PercentageUploaded;
}
export interface FinishedUpload {
localFileID: FileID;
result: UPLOAD_RESULT;
}
export type InProgressUploads = Map<FileID, PercentageUploaded>;
export type FinishedUploads = Map<FileID, UPLOAD_RESULT>;
export type SegregatedFinishedUploads = Map<UPLOAD_RESULT, FileID[]>;
export interface ProgressUpdater {
setPercentComplete: React.Dispatch<React.SetStateAction<number>>;
setUploadCounter: React.Dispatch<React.SetStateAction<UploadCounter>>;
setUploadStage: React.Dispatch<React.SetStateAction<UPLOAD_STAGES>>;
setInProgressUploads: React.Dispatch<
React.SetStateAction<InProgressUpload[]>
>;
setFinishedUploads: React.Dispatch<
React.SetStateAction<SegregatedFinishedUploads>
>;
setUploadFilenames: React.Dispatch<React.SetStateAction<UploadFileNames>>;
setHasLivePhotos: React.Dispatch<React.SetStateAction<boolean>>;
setUploadProgressView: React.Dispatch<React.SetStateAction<boolean>>;
}

View file

@ -0,0 +1,168 @@
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 UpdateSRPAndKeysRequest {
srpM1: string;
setupID: string;
updatedKeyAttr: UpdatedKey;
}
export interface UpdateSRPAndKeysResponse {
srpM2: string;
setupID: string;
}
export interface RecoveryKey {
masterKeyEncryptedWithRecoveryKey: string;
masterKeyDecryptionNonce: string;
recoveryKeyEncryptedWithMasterKey: string;
recoveryKeyDecryptionNonce: string;
}
export interface User {
id: number;
email: string;
token: string;
encryptedToken: string;
isTwoFactorEnabled: boolean;
twoFactorSessionID: string;
}
export interface UserVerificationResponse {
id: number;
keyAttributes?: KeyAttributes;
encryptedToken?: string;
token?: string;
twoFactorSessionID: string;
srpM2?: 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 FamilyMember {
email: string;
usage: number;
id: string;
isAdmin: boolean;
}
export interface FamilyData {
storage: number;
expiry: number;
members: FamilyMember[];
}
export interface UserDetails {
email: string;
usage: number;
fileCount: number;
sharedCollectionCount: number;
subscription: Subscription;
familyData?: FamilyData;
storageBonus?: number;
}
export interface DeleteChallengeResponse {
allowDelete: boolean;
encryptedChallenge: string;
}
export interface GetRemoteStoreValueResponse {
value: string;
}
export interface UpdateRemoteStoreValueRequest {
key: string;
value: string;
}
export interface SRPAttributes {
srpUserID: string;
srpSalt: string;
memLimit: number;
opsLimit: number;
kekSalt: string;
isEmailMFAEnabled: boolean;
}
export interface GetSRPAttributesResponse {
attributes: SRPAttributes;
}
export interface SRPSetupAttributes {
srpSalt: string;
srpVerifier: string;
srpUserID: string;
loginSubKey: string;
}
export interface SetupSRPRequest {
srpUserID: string;
srpSalt: string;
srpVerifier: string;
srpA: string;
}
export interface SetupSRPResponse {
setupID: string;
srpB: string;
}
export interface CompleteSRPSetupRequest {
setupID: string;
srpM1: string;
}
export interface CompleteSRPSetupResponse {
setupID: string;
srpM2: string;
}
export interface CreateSRPSessionResponse {
sessionID: string;
srpB: string;
}
export interface GetFeatureFlagResponse {
disableCFUploadProxy?: boolean;
}

View file

@ -0,0 +1,617 @@
import {
addToCollection,
createAlbum,
getAllLocalCollections,
getLocalCollections,
getNonEmptyCollections,
moveToCollection,
removeFromCollection,
restoreToCollection,
unhideToCollection,
updateCollectionMagicMetadata,
updatePublicCollectionMagicMetadata,
updateSharedCollectionMagicMetadata,
} from 'services/collectionService';
import { downloadFiles, downloadFilesDesktop } from 'utils/file';
import { getAllLocalFiles, getLocalFiles } from 'services/fileService';
import { EnteFile } from 'types/file';
import { CustomError } from 'utils/error';
import { User } from 'types/user';
import { getData, LS_KEYS } from 'utils/storage/localStorage';
import { logError } from 'utils/sentry';
import {
COLLECTION_ROLE,
Collection,
CollectionMagicMetadataProps,
CollectionPublicMagicMetadataProps,
CollectionSummaries,
} from 'types/collection';
import {
CollectionSummaryType,
CollectionType,
HIDE_FROM_COLLECTION_BAR_TYPES,
OPTIONS_NOT_HAVING_COLLECTION_TYPES,
SYSTEM_COLLECTION_TYPES,
MOVE_TO_NOT_ALLOWED_COLLECTION,
ADD_TO_NOT_ALLOWED_COLLECTION,
HIDDEN_ITEMS_SECTION,
DEFAULT_HIDDEN_COLLECTION_USER_FACING_NAME,
} from 'constants/collection';
import { getUnixTimeInMicroSecondsWithDelta } from 'utils/time';
import { SUB_TYPE, VISIBILITY_STATE } from 'types/magicMetadata';
import { isArchivedCollection, updateMagicMetadata } from 'utils/magicMetadata';
import { getAlbumsURL } from 'utils/common/apiUtil';
import bs58 from 'bs58';
import { t } from 'i18next';
import isElectron from 'is-electron';
import { SetCollectionDownloadProgressAttributes } from 'types/gallery';
import ElectronService from 'services/electron/common';
import {
getCollectionExportPath,
getUniqueCollectionExportName,
} from 'utils/export';
import exportService from 'services/export';
import { CollectionDownloadProgressAttributes } from 'components/Collections/CollectionDownloadProgress';
export enum COLLECTION_OPS_TYPE {
ADD,
MOVE,
REMOVE,
RESTORE,
UNHIDE,
}
export async function handleCollectionOps(
type: COLLECTION_OPS_TYPE,
collection: Collection,
selectedFiles: EnteFile[],
selectedCollectionID: number
) {
switch (type) {
case COLLECTION_OPS_TYPE.ADD:
await addToCollection(collection, selectedFiles);
break;
case COLLECTION_OPS_TYPE.MOVE:
await moveToCollection(
selectedCollectionID,
collection,
selectedFiles
);
break;
case COLLECTION_OPS_TYPE.REMOVE:
await removeFromCollection(collection.id, selectedFiles);
break;
case COLLECTION_OPS_TYPE.RESTORE:
await restoreToCollection(collection, selectedFiles);
break;
case COLLECTION_OPS_TYPE.UNHIDE:
await unhideToCollection(collection, selectedFiles);
break;
default:
throw Error(CustomError.INVALID_COLLECTION_OPERATION);
}
}
export function getSelectedCollection(
collectionID: number,
collections: Collection[]
) {
return collections.find((collection) => collection.id === collectionID);
}
export async function downloadCollectionHelper(
collectionID: number,
setCollectionDownloadProgressAttributes: SetCollectionDownloadProgressAttributes
) {
try {
const allFiles = await getAllLocalFiles();
const collectionFiles = allFiles.filter(
(file) => file.collectionID === collectionID
);
const allCollections = await getAllLocalCollections();
const collection = allCollections.find(
(collection) => collection.id === collectionID
);
if (!collection) {
throw Error('collection not found');
}
await downloadCollectionFiles(
collection.name,
collection.id,
isHiddenCollection(collection),
collectionFiles,
setCollectionDownloadProgressAttributes
);
} catch (e) {
logError(e, 'download collection failed ');
}
}
export async function downloadDefaultHiddenCollectionHelper(
setCollectionDownloadProgressAttributes: SetCollectionDownloadProgressAttributes
) {
try {
const hiddenCollections = await getLocalCollections('hidden');
const defaultHiddenCollectionsIds =
getDefaultHiddenCollectionIDs(hiddenCollections);
const hiddenFiles = await getLocalFiles('hidden');
const defaultHiddenCollectionFiles = hiddenFiles.filter((file) =>
defaultHiddenCollectionsIds.has(file.collectionID)
);
await downloadCollectionFiles(
DEFAULT_HIDDEN_COLLECTION_USER_FACING_NAME,
HIDDEN_ITEMS_SECTION,
true,
defaultHiddenCollectionFiles,
setCollectionDownloadProgressAttributes
);
} catch (e) {
logError(e, 'download hidden files failed ');
}
}
async function downloadCollectionFiles(
collectionName: string,
collectionID: number,
isHidden: boolean,
collectionFiles: EnteFile[],
setCollectionDownloadProgressAttributes: SetCollectionDownloadProgressAttributes
) {
if (!collectionFiles.length) {
return;
}
const canceller = new AbortController();
const increaseSuccess = () => {
if (canceller.signal.aborted) return;
setCollectionDownloadProgressAttributes((prev) => ({
...prev,
success: prev.success + 1,
}));
};
const increaseFailed = () => {
if (canceller.signal.aborted) return;
setCollectionDownloadProgressAttributes((prev) => ({
...prev,
failed: prev.failed + 1,
}));
};
const isCancelled = () => canceller.signal.aborted;
const initialProgressAttributes: CollectionDownloadProgressAttributes = {
collectionName,
collectionID,
isHidden,
canceller,
total: collectionFiles.length,
success: 0,
failed: 0,
downloadDirPath: null,
};
if (isElectron()) {
const selectedDir = await ElectronService.selectDirectory();
if (!selectedDir) {
return;
}
const downloadDirPath = await createCollectionDownloadFolder(
selectedDir,
collectionName
);
setCollectionDownloadProgressAttributes({
...initialProgressAttributes,
downloadDirPath,
});
await downloadFilesDesktop(
collectionFiles,
{ increaseSuccess, increaseFailed, isCancelled },
downloadDirPath
);
} else {
setCollectionDownloadProgressAttributes(initialProgressAttributes);
await downloadFiles(collectionFiles, {
increaseSuccess,
increaseFailed,
isCancelled,
});
}
}
async function createCollectionDownloadFolder(
downloadDirPath: string,
collectionName: string
) {
const collectionDownloadName = getUniqueCollectionExportName(
downloadDirPath,
collectionName
);
const collectionDownloadPath = getCollectionExportPath(
downloadDirPath,
collectionDownloadName
);
await exportService.checkExistsAndCreateDir(collectionDownloadPath);
return collectionDownloadPath;
}
export function appendCollectionKeyToShareURL(
url: string,
collectionKey: string
) {
if (!url) {
return null;
}
const sharableURL = new URL(url);
const albumsURL = new URL(getAlbumsURL());
sharableURL.protocol = albumsURL.protocol;
sharableURL.host = albumsURL.host;
sharableURL.pathname = albumsURL.pathname;
const bytes = Buffer.from(collectionKey, 'base64');
sharableURL.hash = bs58.encode(bytes);
return sharableURL.href;
}
const _intSelectOption = (i: number) => {
const label = i === 0 ? t('NO_DEVICE_LIMIT') : i.toString();
return { label, value: i };
};
export function getDeviceLimitOptions() {
return [0, 2, 5, 10, 25, 50].map((i) => _intSelectOption(i));
}
export const shareExpiryOptions = () => [
{ label: t('NEVER'), value: () => 0 },
{
label: t('AFTER_TIME.HOUR'),
value: () => getUnixTimeInMicroSecondsWithDelta({ hours: 1 }),
},
{
label: t('AFTER_TIME.DAY'),
value: () => getUnixTimeInMicroSecondsWithDelta({ days: 1 }),
},
{
label: t('AFTER_TIME.WEEK'),
value: () => getUnixTimeInMicroSecondsWithDelta({ days: 7 }),
},
{
label: t('AFTER_TIME.MONTH'),
value: () => getUnixTimeInMicroSecondsWithDelta({ months: 1 }),
},
{
label: t('AFTER_TIME.YEAR'),
value: () => getUnixTimeInMicroSecondsWithDelta({ years: 1 }),
},
];
export const changeCollectionVisibility = async (
collection: Collection,
visibility: VISIBILITY_STATE
) => {
try {
const updatedMagicMetadataProps: CollectionMagicMetadataProps = {
visibility,
};
const user: User = getData(LS_KEYS.USER);
if (collection.owner.id === user.id) {
const updatedMagicMetadata = await updateMagicMetadata(
updatedMagicMetadataProps,
collection.magicMetadata,
collection.key
);
await updateCollectionMagicMetadata(
collection,
updatedMagicMetadata
);
} else {
const updatedMagicMetadata = await updateMagicMetadata(
updatedMagicMetadataProps,
collection.sharedMagicMetadata,
collection.key
);
await updateSharedCollectionMagicMetadata(
collection,
updatedMagicMetadata
);
}
} catch (e) {
logError(e, 'change collection visibility failed');
throw e;
}
};
export const changeCollectionSortOrder = async (
collection: Collection,
asc: boolean
) => {
try {
const updatedPublicMagicMetadataProps: CollectionPublicMagicMetadataProps =
{
asc,
};
const updatedPubMagicMetadata = await updateMagicMetadata(
updatedPublicMagicMetadataProps,
collection.pubMagicMetadata,
collection.key
);
await updatePublicCollectionMagicMetadata(
collection,
updatedPubMagicMetadata
);
} catch (e) {
logError(e, 'change collection sort order failed');
}
};
export const changeCollectionOrder = async (
collection: Collection,
order: number
) => {
try {
const updatedMagicMetadataProps: CollectionMagicMetadataProps = {
order,
};
const updatedMagicMetadata = await updateMagicMetadata(
updatedMagicMetadataProps,
collection.magicMetadata,
collection.key
);
await updateCollectionMagicMetadata(collection, updatedMagicMetadata);
} catch (e) {
logError(e, 'change collection order failed');
}
};
export const changeCollectionSubType = async (
collection: Collection,
subType: SUB_TYPE
) => {
try {
const updatedMagicMetadataProps: CollectionMagicMetadataProps = {
subType: subType,
};
const updatedMagicMetadata = await updateMagicMetadata(
updatedMagicMetadataProps,
collection.magicMetadata,
collection.key
);
await updateCollectionMagicMetadata(collection, updatedMagicMetadata);
} catch (e) {
logError(e, 'change collection subType failed');
throw e;
}
};
export const getArchivedCollections = (collections: Collection[]) => {
return new Set<number>(
collections
.filter(isArchivedCollection)
.map((collection) => collection.id)
);
};
export const getDefaultHiddenCollectionIDs = (collections: Collection[]) => {
return new Set<number>(
collections
.filter(isDefaultHiddenCollection)
.map((collection) => collection.id)
);
};
export const hasNonSystemCollections = (
collectionSummaries: CollectionSummaries
) => {
for (const collectionSummary of collectionSummaries.values()) {
if (!isSystemCollection(collectionSummary.type)) return true;
}
return false;
};
export const isMoveToAllowedCollection = (type: CollectionSummaryType) => {
return !MOVE_TO_NOT_ALLOWED_COLLECTION.has(type);
};
export const isAddToAllowedCollection = (type: CollectionSummaryType) => {
return !ADD_TO_NOT_ALLOWED_COLLECTION.has(type);
};
export const isSystemCollection = (type: CollectionSummaryType) => {
return SYSTEM_COLLECTION_TYPES.has(type);
};
export const shouldShowOptions = (type: CollectionSummaryType) => {
return !OPTIONS_NOT_HAVING_COLLECTION_TYPES.has(type);
};
export const showEmptyTrashQuickOption = (type: CollectionSummaryType) => {
return type === CollectionSummaryType.trash;
};
export const showDownloadQuickOption = (type: CollectionSummaryType) => {
return (
type === CollectionSummaryType.folder ||
type === CollectionSummaryType.favorites ||
type === CollectionSummaryType.album ||
type === CollectionSummaryType.uncategorized ||
type === CollectionSummaryType.hiddenItems ||
type === CollectionSummaryType.incomingShareViewer ||
type === CollectionSummaryType.incomingShareCollaborator ||
type === CollectionSummaryType.outgoingShare ||
type === CollectionSummaryType.sharedOnlyViaLink ||
type === CollectionSummaryType.archived ||
type === CollectionSummaryType.pinned
);
};
export const showShareQuickOption = (type: CollectionSummaryType) => {
return (
type === CollectionSummaryType.folder ||
type === CollectionSummaryType.album ||
type === CollectionSummaryType.outgoingShare ||
type === CollectionSummaryType.sharedOnlyViaLink ||
type === CollectionSummaryType.archived ||
type === CollectionSummaryType.incomingShareViewer ||
type === CollectionSummaryType.incomingShareCollaborator ||
type === CollectionSummaryType.pinned
);
};
export const shouldBeShownOnCollectionBar = (type: CollectionSummaryType) => {
return !HIDE_FROM_COLLECTION_BAR_TYPES.has(type);
};
export const getUserOwnedCollections = (collections: Collection[]) => {
const user: User = getData(LS_KEYS.USER);
if (!user?.id) {
throw Error('user missing');
}
return collections.filter((collection) => collection.owner.id === user.id);
};
export const isDefaultHiddenCollection = (collection: Collection) =>
collection.magicMetadata?.data.subType === SUB_TYPE.DEFAULT_HIDDEN;
export const isHiddenCollection = (collection: Collection) =>
collection.magicMetadata?.data.visibility === VISIBILITY_STATE.HIDDEN;
export const isQuickLinkCollection = (collection: Collection) =>
collection.magicMetadata?.data.subType === SUB_TYPE.QUICK_LINK_COLLECTION;
export function isOutgoingShare(collection: Collection, user: User): boolean {
return collection.owner.id === user.id && collection.sharees?.length > 0;
}
export function isIncomingShare(collection: Collection, user: User) {
return collection.owner.id !== user.id;
}
export function isIncomingViewerShare(collection: Collection, user: User) {
const sharee = collection.sharees?.find((sharee) => sharee.id === user.id);
return sharee?.role === COLLECTION_ROLE.VIEWER;
}
export function isIncomingCollabShare(collection: Collection, user: User) {
const sharee = collection.sharees?.find((sharee) => sharee.id === user.id);
return sharee?.role === COLLECTION_ROLE.COLLABORATOR;
}
export function isSharedOnlyViaLink(collection: Collection) {
return collection.publicURLs?.length && !collection.sharees?.length;
}
export function isValidMoveTarget(
sourceCollectionID: number,
targetCollection: Collection,
user: User
) {
return (
sourceCollectionID !== targetCollection.id &&
!isHiddenCollection(targetCollection) &&
!isQuickLinkCollection(targetCollection) &&
!isIncomingShare(targetCollection, user)
);
}
export function isValidReplacementAlbum(
collection: Collection,
user: User,
wantedCollectionName: string
) {
return (
collection.name === wantedCollectionName &&
(collection.type === CollectionType.album ||
collection.type === CollectionType.folder) &&
!isHiddenCollection(collection) &&
!isQuickLinkCollection(collection) &&
!isIncomingShare(collection, user)
);
}
export function getCollectionNameMap(
collections: Collection[]
): Map<number, string> {
return new Map<number, string>(
collections.map((collection) => [collection.id, collection.name])
);
}
export function getNonEmptyPersonalCollections(
collections: Collection[],
personalFiles: EnteFile[],
user: User
): Collection[] {
if (!user?.id) {
throw Error('user missing');
}
const nonEmptyCollections = getNonEmptyCollections(
collections,
personalFiles
);
const personalCollections = nonEmptyCollections.filter(
(collection) => collection.owner.id === user?.id
);
return personalCollections;
}
export function getNonHiddenCollections(
collections: Collection[]
): Collection[] {
return collections.filter((collection) => !isHiddenCollection(collection));
}
export function getHiddenCollections(collections: Collection[]): Collection[] {
return collections.filter((collection) => isHiddenCollection(collection));
}
export async function splitNormalAndHiddenCollections(
collections: Collection[]
): Promise<{
normalCollections: Collection[];
hiddenCollections: Collection[];
}> {
const normalCollections = [];
const hiddenCollections = [];
for (const collection of collections) {
if (isHiddenCollection(collection)) {
hiddenCollections.push(collection);
} else {
normalCollections.push(collection);
}
}
return { normalCollections, hiddenCollections };
}
export function constructCollectionNameMap(
collections: Collection[]
): Map<number, string> {
return new Map<number, string>(
(collections ?? []).map((collection) => [
collection.id,
getCollectionUserFacingName(collection),
])
);
}
export const getCollectionUserFacingName = (collection: Collection) => {
if (isDefaultHiddenCollection(collection)) {
return DEFAULT_HIDDEN_COLLECTION_USER_FACING_NAME;
}
return collection.name;
};
export const getOrCreateAlbum = async (
albumName: string,
existingCollections: Collection[]
) => {
const user: User = getData(LS_KEYS.USER);
if (!user?.id) {
throw Error('user missing');
}
for (const collection of existingCollections) {
if (isValidReplacementAlbum(collection, user, albumName)) {
return collection;
}
}
return createAlbum(albumName);
};

View file

@ -0,0 +1,25 @@
import { Remote } from 'comlink';
import { DedicatedCryptoWorker } from 'worker/crypto.worker';
import { ComlinkWorker } from './comlinkWorker';
class ComlinkCryptoWorker {
private comlinkWorkerInstance: Promise<Remote<DedicatedCryptoWorker>>;
async getInstance() {
if (!this.comlinkWorkerInstance) {
const comlinkWorker = getDedicatedCryptoWorker();
this.comlinkWorkerInstance = comlinkWorker.remote;
}
return this.comlinkWorkerInstance;
}
}
export const getDedicatedCryptoWorker = () => {
const cryptoComlinkWorker = new ComlinkWorker<typeof DedicatedCryptoWorker>(
'ente-crypto-worker',
new Worker(new URL('worker/crypto.worker.ts', import.meta.url))
);
return cryptoComlinkWorker;
};
export default new ComlinkCryptoWorker();

View file

@ -0,0 +1,27 @@
import { Remote, wrap } from 'comlink';
// import { WorkerElectronCacheStorageClient } from 'services/workerElectronCache/client';
import { addLocalLog } from 'utils/logging';
export class ComlinkWorker<T extends new () => InstanceType<T>> {
public remote: Promise<Remote<InstanceType<T>>>;
private worker: Worker;
private name: string;
constructor(name: string, worker: Worker) {
this.name = name;
this.worker = worker;
this.worker.onerror = (errorEvent) => {
console.error('Got error event from worker', errorEvent);
};
addLocalLog(() => `Initiated ${this.name}`);
const comlink = wrap<T>(this.worker);
this.remote = new comlink() as Promise<Remote<InstanceType<T>>>;
// expose(WorkerElectronCacheStorageClient, this.worker);
}
public terminate() {
this.worker.terminate();
addLocalLog(() => `Terminated ${this.name}`);
}
}

View file

@ -0,0 +1,113 @@
import { getData, LS_KEYS } from 'utils/storage/localStorage';
export const getEndpoint = () => {
let endpoint = getData(LS_KEYS.API_ENDPOINT);
if (endpoint) {
return endpoint;
}
endpoint = process.env.NEXT_PUBLIC_ENTE_ENDPOINT;
if (isDevDeployment() && endpoint) {
return endpoint;
}
return 'https://api.ente.io';
};
export const getFileURL = (id: number) => {
const endpoint = process.env.NEXT_PUBLIC_ENTE_ENDPOINT;
if (isDevDeployment() && endpoint) {
return `${endpoint}/files/download/${id}`;
}
return `https://files.ente.io/?fileID=${id}`;
};
export const getPublicCollectionFileURL = (id: number) => {
const endpoint = process.env.NEXT_PUBLIC_ENTE_ENDPOINT;
if (isDevDeployment() && endpoint) {
return `${endpoint}/public-collection/files/download/${id}`;
}
return `https://public-albums.ente.io/download/?fileID=${id}`;
};
export const getThumbnailURL = (id: number) => {
const endpoint = process.env.NEXT_PUBLIC_ENTE_ENDPOINT;
if (isDevDeployment() && endpoint) {
return `${endpoint}/files/preview/${id}`;
}
return `https://thumbnails.ente.io/?fileID=${id}`;
};
export const getPublicCollectionThumbnailURL = (id: number) => {
const endpoint = process.env.NEXT_PUBLIC_ENTE_ENDPOINT;
if (isDevDeployment() && endpoint) {
return `${endpoint}/public-collection/files/preview/${id}`;
}
return `https://public-albums.ente.io/preview/?fileID=${id}`;
};
export const getUploadEndpoint = () => {
const endpoint = process.env.NEXT_PUBLIC_ENTE_UPLOAD_ENDPOINT;
if (isDevDeployment() && endpoint) {
return endpoint;
}
return `https://uploader.ente.io`;
};
export const getPaymentsURL = () => {
const paymentsURL = process.env.NEXT_PUBLIC_ENTE_PAYMENT_ENDPOINT;
if (isDevDeployment() && paymentsURL) {
return paymentsURL;
}
return `https://payments.ente.io`;
};
export const getAlbumsURL = () => {
const albumsURL = process.env.NEXT_PUBLIC_ENTE_ALBUM_ENDPOINT;
if (isDevDeployment() && albumsURL) {
return albumsURL;
}
return `https://albums.ente.io`;
};
// getFamilyPortalURL returns the endpoint for the family dashboard which can be used to
// create or manage family.
export const getFamilyPortalURL = () => {
const familyURL = process.env.NEXT_PUBLIC_ENTE_FAMILY_PORTAL_ENDPOINT;
if (isDevDeployment() && familyURL) {
return familyURL;
}
return `https://family.ente.io`;
};
// getAuthenticatorURL returns the endpoint for the authenticator which can be used to
// view authenticator codes.
export const getAuthURL = () => {
const authURL = process.env.NEXT_PUBLIC_ENTE_AUTH_ENDPOINT;
if (isDevDeployment() && authURL) {
return authURL;
}
return `https://auth.ente.io`;
};
export const getSentryTunnelURL = () => {
return `https://sentry-reporter.ente.io`;
};
/*
It's a dev deployment (and should use the environment override for endpoints ) in three cases:
1. when the URL opened is that of the staging web app, or
2. when the URL opened is that of the staging album app, or
3. if the app is running locally (hence node_env is development)
4. if the app is running in test mode
*/
export const isDevDeployment = () => {
if (globalThis?.location) {
return (
process.env.NEXT_PUBLIC_ENTE_WEB_ENDPOINT ===
globalThis.location.origin ||
process.env.NEXT_PUBLIC_ENTE_ALBUM_ENDPOINT ===
globalThis.location.origin ||
process.env.NEXT_PUBLIC_IS_TEST_APP === 'true' ||
process.env.NODE_ENV === 'development'
);
}
};

View file

@ -0,0 +1,149 @@
import { CustomError } from 'utils/error';
import isElectron from 'is-electron';
import { APP_DOWNLOAD_URL } from 'constants/urls';
export function checkConnectivity() {
if (navigator.onLine) {
return true;
}
throw new Error(CustomError.NO_INTERNET_CONNECTION);
}
export function runningInBrowser() {
return typeof window !== 'undefined';
}
export function runningInWorker() {
return typeof importScripts === 'function';
}
export function runningInElectron() {
return isElectron();
}
export function runningInChrome(includeMobile: boolean) {
try {
const userAgentData = navigator['userAgentData'];
const chromeBrand = userAgentData?.brands?.filter(
(b) => b.brand === 'Google Chrome' || b.brand === 'Chromium'
)?.[0];
return chromeBrand && (includeMobile || userAgentData.mobile === false);
} catch (error) {
console.error('Error in runningInChrome: ', error);
return false;
}
}
export function offscreenCanvasSupported() {
return !(typeof OffscreenCanvas === 'undefined');
}
export function webglSupported() {
try {
const canvas = document.createElement('canvas');
const gl = canvas.getContext('webgl');
return gl && gl instanceof WebGLRenderingContext;
} catch (error) {
console.error('Error in webglSupported: ', error);
return false;
}
}
export async function sleep(time: number) {
await new Promise((resolve) => {
setTimeout(() => resolve(null), time);
});
}
export function downloadApp() {
openLink(APP_DOWNLOAD_URL, true);
}
export function reverseString(title: string) {
return title
?.split(' ')
.reduce((reversedString, currWord) => `${currWord} ${reversedString}`);
}
export function initiateEmail(email: string) {
const a = document.createElement('a');
a.href = 'mailto:' + email;
a.rel = 'noreferrer noopener';
a.click();
}
export const promiseWithTimeout = async <T>(
request: Promise<T>,
timeout: number
): Promise<T> => {
const timeoutRef = { current: null };
const rejectOnTimeout = new Promise<null>((_, reject) => {
timeoutRef.current = setTimeout(
() => reject(Error(CustomError.WAIT_TIME_EXCEEDED)),
timeout
);
});
const requestWithTimeOutCancellation = async () => {
const resp = await request;
clearTimeout(timeoutRef.current);
return resp;
};
return await Promise.race([
requestWithTimeOutCancellation(),
rejectOnTimeout,
]);
};
export const preloadImage = (imgBasePath: string) => {
const srcSet = [];
for (let i = 1; i <= 3; i++) {
srcSet.push(`${imgBasePath}/${i}x.png ${i}x`);
}
new Image().srcset = srcSet.join(',');
};
export function openLink(href: string, newTab?: boolean) {
const a = document.createElement('a');
a.href = href;
if (newTab) {
a.target = '_blank';
}
a.rel = 'noreferrer noopener';
a.click();
}
export async function waitAndRun(
waitPromise: Promise<void>,
task: () => Promise<void>
) {
if (waitPromise && isPromise(waitPromise)) {
await waitPromise;
}
await task();
}
function isPromise(p: any) {
if (typeof p === 'object' && typeof p.then === 'function') {
return true;
}
return false;
}
export function isClipboardItemPresent() {
return typeof ClipboardItem !== 'undefined';
}
export function batch<T>(arr: T[], batchSize: number): T[][] {
const batches: T[][] = [];
for (let i = 0; i < arr.length; i += batchSize) {
batches.push(arr.slice(i, i + batchSize));
}
return batches;
}
export const mergeMaps = <K, V>(map1: Map<K, V>, map2: Map<K, V>) => {
const mergedMap = new Map<K, V>(map1);
map2.forEach((value, key) => {
mergedMap.set(key, value);
});
return mergedMap;
};

View file

@ -0,0 +1,26 @@
import { B64EncryptionResult } from 'types/crypto';
import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker';
import { getData, LS_KEYS } from 'utils/storage/localStorage';
import { getKey, SESSION_KEYS } from 'utils/storage/sessionStorage';
import { CustomError } from '../error';
export const getActualKey = async () => {
try {
const encryptionKeyAttributes: B64EncryptionResult = getKey(
SESSION_KEYS.ENCRYPTION_KEY
);
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
const key = await cryptoWorker.decryptB64(
encryptionKeyAttributes.encryptedData,
encryptionKeyAttributes.nonce,
encryptionKeyAttributes.key
);
return key;
} catch (e) {
throw new Error(CustomError.KEY_MISSING);
}
};
export const getToken = () => getData(LS_KEYS.USER)?.token;
export const getUserID = () => getData(LS_KEYS.USER)?.id;

View file

@ -0,0 +1,365 @@
import { KeyAttributes, SRPSetupAttributes } from 'types/user';
import { SESSION_KEYS, setKey } from 'utils/storage/sessionStorage';
import { getData, LS_KEYS, setData } from 'utils/storage/localStorage';
import { getActualKey, getToken } from 'utils/common/key';
import { setRecoveryKey } from 'services/userService';
import { logError } from 'utils/sentry';
import isElectron from 'is-electron';
// import safeStorageService from 'services/electron/safeStorage';
import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker';
import { PasswordStrength } from 'constants/crypto';
import zxcvbn from 'zxcvbn';
import { SRP, SrpClient } from 'fast-srp-hap';
import { convertBase64ToBuffer, convertBufferToBase64 } from 'utils/user';
import { v4 as uuidv4 } from 'uuid';
import { addLocalLog } from 'utils/logging';
const SRP_PARAMS = SRP.params['4096'];
const LOGIN_SUB_KEY_LENGTH = 32;
const LOGIN_SUB_KEY_ID = 1;
const LOGIN_SUB_KEY_CONTEXT = 'loginctx';
const LOGIN_SUB_KEY_BYTE_LENGTH = 16;
export async function generateKeyAndSRPAttributes(passphrase: string): Promise<{
keyAttributes: KeyAttributes;
masterKey: string;
srpSetupAttributes: SRPSetupAttributes;
}> {
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
const masterKey = await cryptoWorker.generateEncryptionKey();
const recoveryKey = await cryptoWorker.generateEncryptionKey();
const kekSalt = await cryptoWorker.generateSaltToDeriveKey();
const kek = await cryptoWorker.deriveSensitiveKey(passphrase, kekSalt);
const masterKeyEncryptedWithKek = await cryptoWorker.encryptToB64(
masterKey,
kek.key
);
const masterKeyEncryptedWithRecoveryKey = await cryptoWorker.encryptToB64(
masterKey,
recoveryKey
);
const recoveryKeyEncryptedWithMasterKey = await cryptoWorker.encryptToB64(
recoveryKey,
masterKey
);
const keyPair = await cryptoWorker.generateKeyPair();
const encryptedKeyPairAttributes = await cryptoWorker.encryptToB64(
keyPair.privateKey,
masterKey
);
const loginSubKey = await generateLoginSubKey(kek.key);
const srpSetupAttributes = await generateSRPSetupAttributes(loginSubKey);
const keyAttributes: KeyAttributes = {
kekSalt,
encryptedKey: masterKeyEncryptedWithKek.encryptedData,
keyDecryptionNonce: masterKeyEncryptedWithKek.nonce,
publicKey: keyPair.publicKey,
encryptedSecretKey: encryptedKeyPairAttributes.encryptedData,
secretKeyDecryptionNonce: encryptedKeyPairAttributes.nonce,
opsLimit: kek.opsLimit,
memLimit: kek.memLimit,
masterKeyEncryptedWithRecoveryKey:
masterKeyEncryptedWithRecoveryKey.encryptedData,
masterKeyDecryptionNonce: masterKeyEncryptedWithRecoveryKey.nonce,
recoveryKeyEncryptedWithMasterKey:
recoveryKeyEncryptedWithMasterKey.encryptedData,
recoveryKeyDecryptionNonce: recoveryKeyEncryptedWithMasterKey.nonce,
};
return {
keyAttributes,
masterKey,
srpSetupAttributes,
};
}
// We encrypt the masterKey, with an intermediate key derived from the
// passphrase (with Interactive mem and ops limits) to avoid saving it to local
// storage in plain text. This means that on the web user will always have to
// enter their passphrase to access their masterKey.
export async function generateAndSaveIntermediateKeyAttributes(
passphrase: string,
existingKeyAttributes: KeyAttributes,
key: string
): Promise<KeyAttributes> {
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
const intermediateKekSalt = await cryptoWorker.generateSaltToDeriveKey();
const intermediateKek = await cryptoWorker.deriveInteractiveKey(
passphrase,
intermediateKekSalt
);
const encryptedKeyAttributes = await cryptoWorker.encryptToB64(
key,
intermediateKek.key
);
const intermediateKeyAttributes = Object.assign(existingKeyAttributes, {
kekSalt: intermediateKekSalt,
encryptedKey: encryptedKeyAttributes.encryptedData,
keyDecryptionNonce: encryptedKeyAttributes.nonce,
opsLimit: intermediateKek.opsLimit,
memLimit: intermediateKek.memLimit,
});
setData(LS_KEYS.KEY_ATTRIBUTES, intermediateKeyAttributes);
return intermediateKeyAttributes;
}
export const saveKeyInSessionStore = async (
keyType: SESSION_KEYS,
key: string,
fromDesktop?: boolean
) => {
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
const sessionKeyAttributes = await cryptoWorker.generateKeyAndEncryptToB64(
key
);
setKey(keyType, sessionKeyAttributes);
if (
isElectron() &&
!fromDesktop &&
keyType === SESSION_KEYS.ENCRYPTION_KEY
) {
// safeStorageService.setEncryptionKey(key);
}
};
export const getRecoveryKey = async () => {
let recoveryKey: string = null;
try {
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
const keyAttributes: KeyAttributes = getData(LS_KEYS.KEY_ATTRIBUTES);
const {
recoveryKeyEncryptedWithMasterKey,
recoveryKeyDecryptionNonce,
} = keyAttributes;
const masterKey = await getActualKey();
if (recoveryKeyEncryptedWithMasterKey) {
recoveryKey = await cryptoWorker.decryptB64(
recoveryKeyEncryptedWithMasterKey,
recoveryKeyDecryptionNonce,
masterKey
);
} else {
recoveryKey = await createNewRecoveryKey();
}
recoveryKey = await cryptoWorker.toHex(recoveryKey);
return recoveryKey;
} catch (e) {
logError(e, 'getRecoveryKey failed');
throw e;
}
};
// Used only for legacy users for whom we did not generate recovery keys during
// sign up
async function createNewRecoveryKey() {
const masterKey = await getActualKey();
const existingAttributes = getData(LS_KEYS.KEY_ATTRIBUTES);
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
const recoveryKey = await cryptoWorker.generateEncryptionKey();
const encryptedMasterKey = await cryptoWorker.encryptToB64(
masterKey,
recoveryKey
);
const encryptedRecoveryKey = await cryptoWorker.encryptToB64(
recoveryKey,
masterKey
);
const recoveryKeyAttributes = {
masterKeyEncryptedWithRecoveryKey: encryptedMasterKey.encryptedData,
masterKeyDecryptionNonce: encryptedMasterKey.nonce,
recoveryKeyEncryptedWithMasterKey: encryptedRecoveryKey.encryptedData,
recoveryKeyDecryptionNonce: encryptedRecoveryKey.nonce,
};
await setRecoveryKey(getToken(), recoveryKeyAttributes);
const updatedKeyAttributes = Object.assign(
existingAttributes,
recoveryKeyAttributes
);
setData(LS_KEYS.KEY_ATTRIBUTES, updatedKeyAttributes);
return recoveryKey;
}
export async function decryptAndStoreToken(
keyAttributes: KeyAttributes,
masterKey: string
) {
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
const user = getData(LS_KEYS.USER);
let decryptedToken = null;
const { encryptedToken } = user;
if (encryptedToken && encryptedToken.length > 0) {
const secretKey = await cryptoWorker.decryptB64(
keyAttributes.encryptedSecretKey,
keyAttributes.secretKeyDecryptionNonce,
masterKey
);
const urlUnsafeB64DecryptedToken = await cryptoWorker.boxSealOpen(
encryptedToken,
keyAttributes.publicKey,
secretKey
);
const decryptedTokenBytes = await cryptoWorker.fromB64(
urlUnsafeB64DecryptedToken
);
decryptedToken = await cryptoWorker.toURLSafeB64(decryptedTokenBytes);
setData(LS_KEYS.USER, {
...user,
token: decryptedToken,
encryptedToken: null,
});
}
}
export async function encryptWithRecoveryKey(key: string) {
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
const hexRecoveryKey = await getRecoveryKey();
const recoveryKey = await cryptoWorker.fromHex(hexRecoveryKey);
const encryptedKey = await cryptoWorker.encryptToB64(key, recoveryKey);
return encryptedKey;
}
export async function decryptDeleteAccountChallenge(
encryptedChallenge: string
) {
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
const masterKey = await getActualKey();
const keyAttributes = getData(LS_KEYS.KEY_ATTRIBUTES);
const secretKey = await cryptoWorker.decryptB64(
keyAttributes.encryptedSecretKey,
keyAttributes.secretKeyDecryptionNonce,
masterKey
);
const b64DecryptedChallenge = await cryptoWorker.boxSealOpen(
encryptedChallenge,
keyAttributes.publicKey,
secretKey
);
const utf8DecryptedChallenge = atob(b64DecryptedChallenge);
return utf8DecryptedChallenge;
}
export function estimatePasswordStrength(password: string): PasswordStrength {
if (!password) {
return PasswordStrength.WEAK;
}
const zxcvbnResult = zxcvbn(password);
if (zxcvbnResult.score < 2) {
return PasswordStrength.WEAK;
} else if (zxcvbnResult.score < 3) {
return PasswordStrength.MODERATE;
} else {
return PasswordStrength.STRONG;
}
}
export const isWeakPassword = (password: string) => {
return estimatePasswordStrength(password) === PasswordStrength.WEAK;
};
export const generateLoginSubKey = async (kek: string) => {
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
const kekSubKeyString = await cryptoWorker.generateSubKey(
kek,
LOGIN_SUB_KEY_LENGTH,
LOGIN_SUB_KEY_ID,
LOGIN_SUB_KEY_CONTEXT
);
const kekSubKey = await cryptoWorker.fromB64(kekSubKeyString);
// use first 16 bytes of generated kekSubKey as loginSubKey
const loginSubKey = await cryptoWorker.toB64(
kekSubKey.slice(0, LOGIN_SUB_KEY_BYTE_LENGTH)
);
return loginSubKey;
};
export const generateSRPSetupAttributes = async (
loginSubKey: string
): Promise<SRPSetupAttributes> => {
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
const srpSalt = await cryptoWorker.generateSaltToDeriveKey();
const srpUserID = uuidv4();
const srpVerifierBuffer = SRP.computeVerifier(
SRP_PARAMS,
convertBase64ToBuffer(srpSalt),
Buffer.from(srpUserID),
convertBase64ToBuffer(loginSubKey)
);
const srpVerifier = convertBufferToBase64(srpVerifierBuffer);
addLocalLog(
() => `SRP setup attributes generated',
${JSON.stringify({
srpSalt,
srpUserID,
srpVerifier,
loginSubKey,
})}`
);
return {
srpUserID,
srpSalt,
srpVerifier,
loginSubKey,
};
};
export const computeVerifierHelper = (
srpSalt: string,
srpUserID: string,
loginSubKey: string
) => {
const srpVerifierBuffer = SRP.computeVerifier(
SRP_PARAMS,
convertBase64ToBuffer(srpSalt),
Buffer.from(srpUserID),
convertBase64ToBuffer(loginSubKey)
);
return convertBufferToBase64(srpVerifierBuffer);
};
export const generateSRPClient = async (
srpSalt: string,
srpUserID: string,
loginSubKey: string
) => {
return new Promise<SrpClient>((resolve, reject) => {
SRP.genKey(function (err, secret1) {
try {
if (err) {
reject(err);
}
const srpClient = new SrpClient(
SRP_PARAMS,
convertBase64ToBuffer(srpSalt),
Buffer.from(srpUserID),
convertBase64ToBuffer(loginSubKey),
secret1,
false
);
resolve(srpClient);
} catch (e) {
reject(e);
}
});
});
};

View file

@ -0,0 +1,422 @@
import sodium, { StateAddress } from 'libsodium-wrappers';
import { ENCRYPTION_CHUNK_SIZE } from 'constants/crypto';
import { B64EncryptionResult } from 'types/crypto';
import { CustomError } from 'utils/error';
export async function decryptChaChaOneShot(
data: Uint8Array,
header: Uint8Array,
key: string
) {
await sodium.ready;
const pullState = sodium.crypto_secretstream_xchacha20poly1305_init_pull(
header,
await fromB64(key)
);
const pullResult = sodium.crypto_secretstream_xchacha20poly1305_pull(
pullState,
data,
null
);
return pullResult.message;
}
export async function decryptChaCha(
data: Uint8Array,
header: Uint8Array,
key: string
) {
await sodium.ready;
const pullState = sodium.crypto_secretstream_xchacha20poly1305_init_pull(
header,
await fromB64(key)
);
const decryptionChunkSize =
ENCRYPTION_CHUNK_SIZE +
sodium.crypto_secretstream_xchacha20poly1305_ABYTES;
let bytesRead = 0;
const decryptedData = [];
let tag = sodium.crypto_secretstream_xchacha20poly1305_TAG_MESSAGE;
while (tag !== sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL) {
let chunkSize = decryptionChunkSize;
if (bytesRead + chunkSize > data.length) {
chunkSize = data.length - bytesRead;
}
const buffer = data.slice(bytesRead, bytesRead + chunkSize);
const pullResult = sodium.crypto_secretstream_xchacha20poly1305_pull(
pullState,
buffer
);
if (!pullResult.message) {
throw new Error(CustomError.PROCESSING_FAILED);
}
for (let index = 0; index < pullResult.message.length; index++) {
decryptedData.push(pullResult.message[index]);
}
tag = pullResult.tag;
bytesRead += chunkSize;
}
return Uint8Array.from(decryptedData);
}
export async function initChunkDecryption(header: Uint8Array, key: Uint8Array) {
await sodium.ready;
const pullState = sodium.crypto_secretstream_xchacha20poly1305_init_pull(
header,
key
);
const decryptionChunkSize =
ENCRYPTION_CHUNK_SIZE +
sodium.crypto_secretstream_xchacha20poly1305_ABYTES;
const tag = sodium.crypto_secretstream_xchacha20poly1305_TAG_MESSAGE;
return { pullState, decryptionChunkSize, tag };
}
export async function decryptFileChunk(
data: Uint8Array,
pullState: StateAddress
) {
await sodium.ready;
const pullResult = sodium.crypto_secretstream_xchacha20poly1305_pull(
pullState,
data
);
if (!pullResult.message) {
throw new Error(CustomError.PROCESSING_FAILED);
}
const newTag = pullResult.tag;
return { decryptedData: pullResult.message, newTag };
}
export async function encryptChaChaOneShot(data: Uint8Array, key: string) {
await sodium.ready;
const uintkey: Uint8Array = await fromB64(key);
const initPushResult =
sodium.crypto_secretstream_xchacha20poly1305_init_push(uintkey);
const [pushState, header] = [initPushResult.state, initPushResult.header];
const pushResult = sodium.crypto_secretstream_xchacha20poly1305_push(
pushState,
data,
null,
sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL
);
return {
key: await toB64(uintkey),
file: {
encryptedData: pushResult,
decryptionHeader: await toB64(header),
},
};
}
export async function encryptChaCha(data: Uint8Array) {
await sodium.ready;
const uintkey: Uint8Array =
sodium.crypto_secretstream_xchacha20poly1305_keygen();
const initPushResult =
sodium.crypto_secretstream_xchacha20poly1305_init_push(uintkey);
const [pushState, header] = [initPushResult.state, initPushResult.header];
let bytesRead = 0;
let tag = sodium.crypto_secretstream_xchacha20poly1305_TAG_MESSAGE;
const encryptedData = [];
while (tag !== sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL) {
let chunkSize = ENCRYPTION_CHUNK_SIZE;
if (bytesRead + chunkSize >= data.length) {
chunkSize = data.length - bytesRead;
tag = sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL;
}
const buffer = data.slice(bytesRead, bytesRead + chunkSize);
bytesRead += chunkSize;
const pushResult = sodium.crypto_secretstream_xchacha20poly1305_push(
pushState,
buffer,
null,
tag
);
for (let index = 0; index < pushResult.length; index++) {
encryptedData.push(pushResult[index]);
}
}
return {
key: await toB64(uintkey),
file: {
encryptedData: new Uint8Array(encryptedData),
decryptionHeader: await toB64(header),
},
};
}
export async function initChunkEncryption() {
await sodium.ready;
const key = sodium.crypto_secretstream_xchacha20poly1305_keygen();
const initPushResult =
sodium.crypto_secretstream_xchacha20poly1305_init_push(key);
const [pushState, header] = [initPushResult.state, initPushResult.header];
return {
key: await toB64(key),
decryptionHeader: await toB64(header),
pushState,
};
}
export async function encryptFileChunk(
data: Uint8Array,
pushState: sodium.StateAddress,
isFinalChunk: boolean
) {
await sodium.ready;
const tag = isFinalChunk
? sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL
: sodium.crypto_secretstream_xchacha20poly1305_TAG_MESSAGE;
const pushResult = sodium.crypto_secretstream_xchacha20poly1305_push(
pushState,
data,
null,
tag
);
return pushResult;
}
export async function encryptToB64(data: string, key: string) {
await sodium.ready;
const encrypted = await encrypt(await fromB64(data), await fromB64(key));
return {
encryptedData: await toB64(encrypted.encryptedData),
key: await toB64(encrypted.key),
nonce: await toB64(encrypted.nonce),
} as B64EncryptionResult;
}
export async function generateKeyAndEncryptToB64(data: string) {
await sodium.ready;
const key = sodium.crypto_secretbox_keygen();
return await encryptToB64(data, await toB64(key));
}
export async function encryptUTF8(data: string, key: string) {
const b64Data = await toB64(await fromUTF8(data));
return await encryptToB64(b64Data, key);
}
export async function decryptB64(data: string, nonce: string, key: string) {
await sodium.ready;
const decrypted = await decrypt(
await fromB64(data),
await fromB64(nonce),
await fromB64(key)
);
return await toB64(decrypted);
}
export async function decryptToUTF8(data: string, nonce: string, key: string) {
await sodium.ready;
const decrypted = await decrypt(
await fromB64(data),
await fromB64(nonce),
await fromB64(key)
);
return sodium.to_string(decrypted);
}
async function encrypt(data: Uint8Array, key: Uint8Array) {
await sodium.ready;
const nonce = sodium.randombytes_buf(sodium.crypto_secretbox_NONCEBYTES);
const encryptedData = sodium.crypto_secretbox_easy(data, nonce, key);
return {
encryptedData,
key,
nonce,
};
}
async function decrypt(data: Uint8Array, nonce: Uint8Array, key: Uint8Array) {
await sodium.ready;
return sodium.crypto_secretbox_open_easy(data, nonce, key);
}
export async function initChunkHashing() {
await sodium.ready;
const hashState = sodium.crypto_generichash_init(
null,
sodium.crypto_generichash_BYTES_MAX
);
return hashState;
}
export async function hashFileChunk(
hashState: sodium.StateAddress,
chunk: Uint8Array
) {
await sodium.ready;
sodium.crypto_generichash_update(hashState, chunk);
}
export async function completeChunkHashing(hashState: sodium.StateAddress) {
await sodium.ready;
const hash = sodium.crypto_generichash_final(
hashState,
sodium.crypto_generichash_BYTES_MAX
);
const hashString = toB64(hash);
return hashString;
}
export async function deriveKey(
passphrase: string,
salt: string,
opsLimit: number,
memLimit: number
) {
await sodium.ready;
return await toB64(
sodium.crypto_pwhash(
sodium.crypto_secretbox_KEYBYTES,
await fromUTF8(passphrase),
await fromB64(salt),
opsLimit,
memLimit,
sodium.crypto_pwhash_ALG_ARGON2ID13
)
);
}
export async function deriveSensitiveKey(passphrase: string, salt: string) {
await sodium.ready;
const minMemLimit = sodium.crypto_pwhash_MEMLIMIT_MIN;
let opsLimit = sodium.crypto_pwhash_OPSLIMIT_SENSITIVE;
let memLimit = sodium.crypto_pwhash_MEMLIMIT_SENSITIVE;
while (memLimit > minMemLimit) {
try {
const key = await deriveKey(passphrase, salt, opsLimit, memLimit);
return {
key,
opsLimit,
memLimit,
};
} catch (e) {
opsLimit *= 2;
memLimit /= 2;
}
}
}
export async function deriveInteractiveKey(passphrase: string, salt: string) {
await sodium.ready;
const key = await toB64(
sodium.crypto_pwhash(
sodium.crypto_secretbox_KEYBYTES,
await fromUTF8(passphrase),
await fromB64(salt),
sodium.crypto_pwhash_OPSLIMIT_INTERACTIVE,
sodium.crypto_pwhash_MEMLIMIT_INTERACTIVE,
sodium.crypto_pwhash_ALG_ARGON2ID13
)
);
return {
key,
opsLimit: sodium.crypto_pwhash_OPSLIMIT_INTERACTIVE,
memLimit: sodium.crypto_pwhash_MEMLIMIT_INTERACTIVE,
};
}
export async function generateEncryptionKey() {
await sodium.ready;
return await toB64(sodium.crypto_kdf_keygen());
}
export async function generateSaltToDeriveKey() {
await sodium.ready;
return await toB64(sodium.randombytes_buf(sodium.crypto_pwhash_SALTBYTES));
}
export async function generateKeyPair() {
await sodium.ready;
const keyPair: sodium.KeyPair = sodium.crypto_box_keypair();
return {
privateKey: await toB64(keyPair.privateKey),
publicKey: await toB64(keyPair.publicKey),
};
}
export async function boxSealOpen(
input: string,
publicKey: string,
secretKey: string
) {
await sodium.ready;
return await toB64(
sodium.crypto_box_seal_open(
await fromB64(input),
await fromB64(publicKey),
await fromB64(secretKey)
)
);
}
export async function boxSeal(input: string, publicKey: string) {
await sodium.ready;
return await toB64(
sodium.crypto_box_seal(await fromB64(input), await fromB64(publicKey))
);
}
export async function generateSubKey(
key: string,
subKeyLength: number,
subKeyID: number,
context: string
) {
await sodium.ready;
return await toB64(
sodium.crypto_kdf_derive_from_key(
subKeyLength,
subKeyID,
context,
await fromB64(key)
)
);
}
export async function fromB64(input: string) {
await sodium.ready;
return sodium.from_base64(input, sodium.base64_variants.ORIGINAL);
}
export async function toB64(input: Uint8Array) {
await sodium.ready;
return sodium.to_base64(input, sodium.base64_variants.ORIGINAL);
}
export async function toURLSafeB64(input: Uint8Array) {
await sodium.ready;
return sodium.to_base64(input, sodium.base64_variants.URLSAFE);
}
export async function fromUTF8(input: string) {
await sodium.ready;
return sodium.from_string(input);
}
export async function toUTF8(input: string) {
await sodium.ready;
return sodium.to_string(await fromB64(input));
}
export async function toHex(input: string) {
await sodium.ready;
return sodium.to_hex(await fromB64(input));
}
export async function fromHex(input: string) {
await sodium.ready;
return await toB64(sodium.from_hex(input));
}

View file

@ -0,0 +1,163 @@
import { HttpStatusCode } from 'axios';
export class ApiErrorResponse {
code: string;
message: string;
}
export class ApiError extends Error {
httpStatusCode: number;
errCode: string;
constructor(message: string, errCode: string, httpStatus: number) {
super(message);
this.name = 'ApiError';
this.errCode = errCode;
this.httpStatusCode = httpStatus;
}
}
export function isApiErrorResponse(object: any): object is ApiErrorResponse {
return object && 'code' in object && 'message' in object;
}
export const CustomError = {
THUMBNAIL_GENERATION_FAILED: 'thumbnail generation failed',
VIDEO_PLAYBACK_FAILED: 'video playback failed',
ETAG_MISSING: 'no header/etag present in response body',
KEY_MISSING: 'encrypted key missing from localStorage',
FAILED_TO_LOAD_WEB_WORKER: 'failed to load web worker',
CHUNK_MORE_THAN_EXPECTED: 'chunks more than expected',
CHUNK_LESS_THAN_EXPECTED: 'chunks less than expected',
UNSUPPORTED_FILE_FORMAT: 'unsupported file format',
FILE_TOO_LARGE: 'file too large',
SUBSCRIPTION_EXPIRED: 'subscription expired',
STORAGE_QUOTA_EXCEEDED: 'storage quota exceeded',
SESSION_EXPIRED: 'session expired',
INVALID_MIME_TYPE: (type: string) => `invalid mime type -${type}`,
SIGNUP_FAILED: 'signup failed',
FAV_COLLECTION_MISSING: 'favorite collection missing',
INVALID_COLLECTION_OPERATION: 'invalid collection operation',
TO_MOVE_FILES_FROM_MULTIPLE_COLLECTIONS:
'to move files from multiple collections',
WAIT_TIME_EXCEEDED: 'operation wait time exceeded',
REQUEST_CANCELLED: 'request canceled',
REQUEST_FAILED: 'request failed',
TOKEN_EXPIRED: 'token expired',
TOKEN_MISSING: 'token missing',
TOO_MANY_REQUESTS: 'too many requests',
BAD_REQUEST: 'bad request',
SUBSCRIPTION_NEEDED: 'subscription not present',
NOT_FOUND: 'not found ',
NO_METADATA: 'no metadata',
TOO_LARGE_LIVE_PHOTO_ASSETS: 'too large live photo assets',
NOT_A_DATE: 'not a date',
NOT_A_LOCATION: 'not a location',
FILE_ID_NOT_FOUND: 'file with id not found',
WEAK_DEVICE: 'password decryption failed on the device',
INCORRECT_PASSWORD: 'incorrect password',
UPLOAD_CANCELLED: 'upload cancelled',
REQUEST_TIMEOUT: 'request taking too long',
HIDDEN_COLLECTION_SYNC_FILE_ATTEMPTED:
'hidden collection sync file attempted',
UNKNOWN_ERROR: 'Something went wrong, please try again',
TYPE_DETECTION_FAILED: (fileFormat: string) =>
`type detection failed ${fileFormat}`,
WINDOWS_NATIVE_IMAGE_PROCESSING_NOT_SUPPORTED:
'Windows native image processing is not supported',
NETWORK_ERROR: 'Network Error',
NOT_FILE_OWNER: 'not file owner',
UPDATE_EXPORTED_RECORD_FAILED: 'update file exported record failed',
EXPORT_STOPPED: 'export stopped',
NO_EXPORT_FOLDER_SELECTED: 'no export folder selected',
EXPORT_FOLDER_DOES_NOT_EXIST: 'export folder does not exist',
NO_INTERNET_CONNECTION: 'no internet connection',
AUTH_KEY_NOT_FOUND: 'auth key not found',
EXIF_DATA_NOT_FOUND: 'exif data not found',
SELECT_FOLDER_ABORTED: 'select folder aborted',
NON_MEDIA_FILE: 'non media file',
NOT_AVAILABLE_ON_WEB: 'not available on web',
UNSUPPORTED_RAW_FORMAT: 'unsupported raw format',
NON_PREVIEWABLE_FILE: 'non previewable file',
PROCESSING_FAILED: 'processing failed',
EXPORT_RECORD_JSON_PARSING_FAILED: 'export record json parsing failed',
TWO_FACTOR_ENABLED: 'two factor enabled',
CLIENT_ERROR: 'client error',
ServerError: 'server error',
};
export function handleUploadError(error): Error {
const parsedError = parseUploadErrorCodes(error);
// breaking errors
switch (parsedError.message) {
case CustomError.SUBSCRIPTION_EXPIRED:
case CustomError.STORAGE_QUOTA_EXCEEDED:
case CustomError.SESSION_EXPIRED:
case CustomError.UPLOAD_CANCELLED:
throw parsedError;
}
return parsedError;
}
export function errorWithContext(originalError: Error, context: string) {
const errorWithContext = new Error(context);
errorWithContext.stack =
errorWithContext.stack.split('\n').slice(2, 4).join('\n') +
'\n' +
originalError.stack;
return errorWithContext;
}
export function parseUploadErrorCodes(error) {
let parsedMessage = null;
if (error instanceof ApiError) {
switch (error.httpStatusCode) {
case HttpStatusCode.PaymentRequired:
parsedMessage = CustomError.SUBSCRIPTION_EXPIRED;
break;
case HttpStatusCode.UpgradeRequired:
parsedMessage = CustomError.STORAGE_QUOTA_EXCEEDED;
break;
case HttpStatusCode.Unauthorized:
parsedMessage = CustomError.SESSION_EXPIRED;
break;
case HttpStatusCode.PayloadTooLarge:
parsedMessage = CustomError.FILE_TOO_LARGE;
break;
default:
parsedMessage = `${CustomError.UNKNOWN_ERROR} statusCode:${error.httpStatusCode}`;
}
} else {
parsedMessage = error.message;
}
return new Error(parsedMessage);
}
export const parseSharingErrorCodes = (error) => {
let parsedMessage = null;
if (error instanceof ApiError) {
switch (error.httpStatusCode) {
case HttpStatusCode.BadRequest:
parsedMessage = CustomError.BAD_REQUEST;
break;
case HttpStatusCode.PaymentRequired:
parsedMessage = CustomError.SUBSCRIPTION_NEEDED;
break;
case HttpStatusCode.NotFound:
parsedMessage = CustomError.NOT_FOUND;
break;
case HttpStatusCode.Unauthorized:
case HttpStatusCode.Gone:
parsedMessage = CustomError.TOKEN_EXPIRED;
break;
case HttpStatusCode.TooManyRequests:
parsedMessage = CustomError.TOO_MANY_REQUESTS;
break;
default:
parsedMessage = `${CustomError.UNKNOWN_ERROR} statusCode:${error.httpStatusCode}`;
}
} else {
parsedMessage = error.message;
}
return new Error(parsedMessage);
};

View file

@ -0,0 +1,15 @@
export const readAsDataURL = (blob) =>
new Promise<string>((resolve, reject) => {
const fileReader = new FileReader();
fileReader.onload = () => resolve(fileReader.result as string);
fileReader.onerror = () => reject(fileReader.error);
fileReader.readAsDataURL(blob);
});
export const readAsText = (blob) =>
new Promise<string>((resolve, reject) => {
const fileReader = new FileReader();
fileReader.onload = () => resolve(fileReader.result as string);
fileReader.onerror = () => reject(fileReader.error);
fileReader.readAsText(blob);
});

View file

@ -0,0 +1,967 @@
import { SelectedState } from 'types/gallery';
import {
EnteFile,
EncryptedEnteFile,
FileWithUpdatedMagicMetadata,
FileMagicMetadata,
FileMagicMetadataProps,
FilePublicMagicMetadata,
FilePublicMagicMetadataProps,
} from 'types/file';
import { decodeLivePhoto } from 'services/livePhotoService';
import { getFileType } from 'services/typeDetectionService';
import DownloadManager from 'services/downloadManager';
import { logError } from 'utils/sentry';
import { User } from 'types/user';
import { getData, LS_KEYS } from 'utils/storage/localStorage';
import { updateFileCreationDateInEXIF } from 'services/upload/exifService';
import {
TYPE_JPEG,
TYPE_JPG,
TYPE_HEIC,
TYPE_HEIF,
FILE_TYPE,
SUPPORTED_RAW_FORMATS,
RAW_FORMATS,
} from 'constants/file';
import PublicCollectionDownloadManager from 'services/publicCollectionDownloadManager';
import heicConversionService from 'services/heicConversionService';
import * as ffmpegService from 'services/ffmpeg/ffmpegService';
import { VISIBILITY_STATE } from 'types/magicMetadata';
import { isArchivedFile, updateMagicMetadata } from 'utils/magicMetadata';
import { addLocalLog, addLogLine } from 'utils/logging';
import { CustomError } from 'utils/error';
import { convertBytesToHumanReadable } from './size';
import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker';
import {
deleteFromTrash,
trashFiles,
updateFileMagicMetadata,
updateFilePublicMagicMetadata,
} from 'services/fileService';
import isElectron from 'is-electron';
import imageProcessor from 'services/electron/imageProcessor';
import { isPlaybackPossible } from 'utils/photoFrame';
import { FileTypeInfo } from 'types/upload';
import { moveToHiddenCollection } from 'services/collectionService';
import ElectronFSService from 'services/electron/fs';
import { getFileExportPath, getUniqueFileExportName } from 'utils/export';
const WAIT_TIME_IMAGE_CONVERSION = 30 * 1000;
export enum FILE_OPS_TYPE {
DOWNLOAD,
FIX_TIME,
ARCHIVE,
UNARCHIVE,
HIDE,
TRASH,
DELETE_PERMANENTLY,
}
export function downloadAsFile(filename: string, content: string) {
const file = new Blob([content], {
type: 'text/plain',
});
const fileURL = URL.createObjectURL(file);
downloadUsingAnchor(fileURL, filename);
}
export async function getUpdatedEXIFFileForDownload(
fileReader: FileReader,
file: EnteFile,
fileStream: ReadableStream<Uint8Array>
): Promise<ReadableStream<Uint8Array>> {
const extension = getFileExtension(file.metadata.title);
if (
file.metadata.fileType === FILE_TYPE.IMAGE &&
file.pubMagicMetadata?.data.editedTime &&
(extension === TYPE_JPEG || extension === TYPE_JPG)
) {
const fileBlob = await new Response(fileStream).blob();
const updatedFileBlob = await updateFileCreationDateInEXIF(
fileReader,
fileBlob,
new Date(file.pubMagicMetadata.data.editedTime / 1000)
);
return updatedFileBlob.stream();
} else {
return fileStream;
}
}
export async function downloadFile(
file: EnteFile,
accessedThroughSharedURL: boolean,
token?: string,
passwordToken?: string
) {
try {
let fileBlob: Blob;
const fileReader = new FileReader();
if (accessedThroughSharedURL) {
const fileURL =
await PublicCollectionDownloadManager.getCachedOriginalFile(
file
)[0];
if (!fileURL) {
fileBlob = await new Response(
await PublicCollectionDownloadManager.downloadFile(
token,
passwordToken,
file
)
).blob();
} else {
fileBlob = await (await fetch(fileURL)).blob();
}
} else {
const fileURL = await DownloadManager.getCachedOriginalFile(
file
)[0];
if (!fileURL) {
fileBlob = await new Response(
await DownloadManager.downloadFile(file)
).blob();
} else {
fileBlob = await (await fetch(fileURL)).blob();
}
}
if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) {
const livePhoto = await decodeLivePhoto(file, fileBlob);
const image = new File([livePhoto.image], livePhoto.imageNameTitle);
const imageType = await getFileType(image);
const tempImageURL = URL.createObjectURL(
new Blob([livePhoto.image], { type: imageType.mimeType })
);
const video = new File([livePhoto.video], livePhoto.videoNameTitle);
const videoType = await getFileType(video);
const tempVideoURL = URL.createObjectURL(
new Blob([livePhoto.video], { type: videoType.mimeType })
);
downloadUsingAnchor(tempImageURL, livePhoto.imageNameTitle);
downloadUsingAnchor(tempVideoURL, livePhoto.videoNameTitle);
} else {
const fileType = await getFileType(
new File([fileBlob], file.metadata.title)
);
fileBlob = await new Response(
await getUpdatedEXIFFileForDownload(
fileReader,
file,
fileBlob.stream()
)
).blob();
fileBlob = new Blob([fileBlob], { type: fileType.mimeType });
const tempURL = URL.createObjectURL(fileBlob);
downloadUsingAnchor(tempURL, file.metadata.title);
}
} catch (e) {
logError(e, 'failed to download file');
throw e;
}
}
export function downloadUsingAnchor(link: string, name: string) {
const a = document.createElement('a');
a.style.display = 'none';
a.href = link;
a.download = name;
document.body.appendChild(a);
a.click();
URL.revokeObjectURL(link);
a.remove();
}
export function groupFilesBasedOnCollectionID(files: EnteFile[]) {
const collectionWiseFiles = new Map<number, EnteFile[]>();
for (const file of files) {
if (!collectionWiseFiles.has(file.collectionID)) {
collectionWiseFiles.set(file.collectionID, []);
}
collectionWiseFiles.get(file.collectionID).push(file);
}
return collectionWiseFiles;
}
function getSelectedFileIds(selectedFiles: SelectedState) {
const filesIDs: number[] = [];
for (const [key, val] of Object.entries(selectedFiles)) {
if (typeof val === 'boolean' && val) {
filesIDs.push(Number(key));
}
}
return new Set(filesIDs);
}
export function getSelectedFiles(
selected: SelectedState,
files: EnteFile[]
): EnteFile[] {
const selectedFilesIDs = getSelectedFileIds(selected);
return files.filter((file) => selectedFilesIDs.has(file.id));
}
export function sortFiles(files: EnteFile[], sortAsc = false) {
// sort based on the time of creation time of the file,
// for files with same creation time, sort based on the time of last modification
const factor = sortAsc ? -1 : 1;
return files.sort((a, b) => {
if (a.metadata.creationTime === b.metadata.creationTime) {
return (
factor *
(b.metadata.modificationTime - a.metadata.modificationTime)
);
}
return factor * (b.metadata.creationTime - a.metadata.creationTime);
});
}
export function sortTrashFiles(files: EnteFile[]) {
return files.sort((a, b) => {
if (a.deleteBy === b.deleteBy) {
if (a.metadata.creationTime === b.metadata.creationTime) {
return (
b.metadata.modificationTime - a.metadata.modificationTime
);
}
return b.metadata.creationTime - a.metadata.creationTime;
}
return a.deleteBy - b.deleteBy;
});
}
export async function decryptFile(
file: EncryptedEnteFile,
collectionKey: string
): Promise<EnteFile> {
try {
const worker = await ComlinkCryptoWorker.getInstance();
const {
encryptedKey,
keyDecryptionNonce,
metadata,
magicMetadata,
pubMagicMetadata,
...restFileProps
} = file;
const fileKey = await worker.decryptB64(
encryptedKey,
keyDecryptionNonce,
collectionKey
);
const fileMetadata = await worker.decryptMetadata(
metadata.encryptedData,
metadata.decryptionHeader,
fileKey
);
let fileMagicMetadata: FileMagicMetadata;
let filePubMagicMetadata: FilePublicMagicMetadata;
if (magicMetadata?.data) {
fileMagicMetadata = {
...file.magicMetadata,
data: await worker.decryptMetadata(
magicMetadata.data,
magicMetadata.header,
fileKey
),
};
}
if (pubMagicMetadata?.data) {
filePubMagicMetadata = {
...pubMagicMetadata,
data: await worker.decryptMetadata(
pubMagicMetadata.data,
pubMagicMetadata.header,
fileKey
),
};
}
return {
...restFileProps,
key: fileKey,
metadata: fileMetadata,
magicMetadata: fileMagicMetadata,
pubMagicMetadata: filePubMagicMetadata,
};
} catch (e) {
logError(e, 'file decryption failed');
throw e;
}
}
export function getFileNameWithoutExtension(filename: string) {
const lastDotPosition = filename.lastIndexOf('.');
if (lastDotPosition === -1) return filename;
else return filename.slice(0, lastDotPosition);
}
export function getFileExtensionWithDot(filename: string) {
const lastDotPosition = filename.lastIndexOf('.');
if (lastDotPosition === -1) return '';
else return filename.slice(lastDotPosition);
}
export function splitFilenameAndExtension(filename: string): [string, string] {
const lastDotPosition = filename.lastIndexOf('.');
if (lastDotPosition === -1) return [filename, null];
else
return [
filename.slice(0, lastDotPosition),
filename.slice(lastDotPosition + 1),
];
}
export function getFileExtension(filename: string) {
return splitFilenameAndExtension(filename)[1]?.toLocaleLowerCase();
}
export function generateStreamFromArrayBuffer(data: Uint8Array) {
return new ReadableStream({
async start(controller: ReadableStreamDefaultController) {
controller.enqueue(data);
controller.close();
},
});
}
export async function getRenderableFileURL(file: EnteFile, fileBlob: Blob) {
switch (file.metadata.fileType) {
case FILE_TYPE.IMAGE: {
const convertedBlob = await getRenderableImage(
file.metadata.title,
fileBlob
);
const { originalURL, convertedURL } = getFileObjectURLs(
fileBlob,
convertedBlob
);
return {
converted: [convertedURL],
original: [originalURL],
};
}
case FILE_TYPE.LIVE_PHOTO: {
return await getRenderableLivePhotoURL(file, fileBlob);
}
case FILE_TYPE.VIDEO: {
const convertedBlob = await getPlayableVideo(
file.metadata.title,
fileBlob
);
const { originalURL, convertedURL } = getFileObjectURLs(
fileBlob,
convertedBlob
);
return {
converted: [convertedURL],
original: [originalURL],
};
}
default: {
const previewURL = await createTypedObjectURL(
fileBlob,
file.metadata.title
);
return {
converted: [previewURL],
original: [previewURL],
};
}
}
}
async function getRenderableLivePhotoURL(
file: EnteFile,
fileBlob: Blob
): Promise<{ original: string[]; converted: string[] }> {
const livePhoto = await decodeLivePhoto(file, fileBlob);
const imageBlob = new Blob([livePhoto.image]);
const videoBlob = new Blob([livePhoto.video]);
const convertedImageBlob = await getRenderableImage(
livePhoto.imageNameTitle,
imageBlob
);
const convertedVideoBlob = await getPlayableVideo(
livePhoto.videoNameTitle,
videoBlob,
true
);
const { originalURL: originalImageURL, convertedURL: convertedImageURL } =
getFileObjectURLs(imageBlob, convertedImageBlob);
const { originalURL: originalVideoURL, convertedURL: convertedVideoURL } =
getFileObjectURLs(videoBlob, convertedVideoBlob);
return {
converted: [convertedImageURL, convertedVideoURL],
original: [originalImageURL, originalVideoURL],
};
}
export async function getPlayableVideo(
videoNameTitle: string,
videoBlob: Blob,
forceConvert = false
) {
try {
const isPlayable = await isPlaybackPossible(
URL.createObjectURL(videoBlob)
);
if (isPlayable && !forceConvert) {
return videoBlob;
} else {
if (!forceConvert && !isElectron()) {
return null;
}
addLogLine(
'video format not supported, converting it name:',
videoNameTitle
);
const mp4ConvertedVideo = await ffmpegService.convertToMP4(
new File([videoBlob], videoNameTitle)
);
addLogLine('video successfully converted', videoNameTitle);
return new Blob([await mp4ConvertedVideo.arrayBuffer()]);
}
} catch (e) {
addLogLine('video conversion failed', videoNameTitle);
logError(e, 'video conversion failed');
return null;
}
}
export async function getRenderableImage(fileName: string, imageBlob: Blob) {
let fileTypeInfo: FileTypeInfo;
try {
const tempFile = new File([imageBlob], fileName);
fileTypeInfo = await getFileType(tempFile);
addLocalLog(() => `file type info: ${JSON.stringify(fileTypeInfo)}`);
const { exactType } = fileTypeInfo;
let convertedImageBlob: Blob;
if (isRawFile(exactType)) {
try {
if (!isSupportedRawFormat(exactType)) {
throw Error(CustomError.UNSUPPORTED_RAW_FORMAT);
}
if (!isElectron()) {
throw Error(CustomError.NOT_AVAILABLE_ON_WEB);
}
addLogLine(
`RawConverter called for ${fileName}-${convertBytesToHumanReadable(
imageBlob.size
)}`
);
convertedImageBlob = await imageProcessor.convertToJPEG(
imageBlob,
fileName
);
addLogLine(`${fileName} successfully converted`);
} catch (e) {
try {
if (!isFileHEIC(exactType)) {
throw e;
}
addLogLine(
`HEICConverter called for ${fileName}-${convertBytesToHumanReadable(
imageBlob.size
)}`
);
convertedImageBlob = await heicConversionService.convert(
imageBlob
);
addLogLine(`${fileName} successfully converted`);
} catch (e) {
throw Error(CustomError.NON_PREVIEWABLE_FILE);
}
}
return convertedImageBlob;
} else {
return imageBlob;
}
} catch (e) {
logError(e, 'get Renderable Image failed', { fileTypeInfo });
return null;
}
}
export function isFileHEIC(exactType: string) {
return (
exactType.toLowerCase().endsWith(TYPE_HEIC) ||
exactType.toLowerCase().endsWith(TYPE_HEIF)
);
}
export function isRawFile(exactType: string) {
return RAW_FORMATS.includes(exactType.toLowerCase());
}
export function isRawFileFromFileName(fileName: string) {
for (const rawFormat of RAW_FORMATS) {
if (fileName.toLowerCase().endsWith(rawFormat)) {
return true;
}
}
return false;
}
export function isSupportedRawFormat(exactType: string) {
return SUPPORTED_RAW_FORMATS.includes(exactType.toLowerCase());
}
export async function changeFilesVisibility(
files: EnteFile[],
visibility: VISIBILITY_STATE
): Promise<EnteFile[]> {
const fileWithUpdatedMagicMetadataList: FileWithUpdatedMagicMetadata[] = [];
for (const file of files) {
const updatedMagicMetadataProps: FileMagicMetadataProps = {
visibility,
};
fileWithUpdatedMagicMetadataList.push({
file,
updatedMagicMetadata: await updateMagicMetadata(
updatedMagicMetadataProps,
file.magicMetadata,
file.key
),
});
}
return await updateFileMagicMetadata(fileWithUpdatedMagicMetadataList);
}
export async function changeFileCreationTime(
file: EnteFile,
editedTime: number
): Promise<EnteFile> {
const updatedPublicMagicMetadataProps: FilePublicMagicMetadataProps = {
editedTime,
};
const updatedPublicMagicMetadata: FilePublicMagicMetadata =
await updateMagicMetadata(
updatedPublicMagicMetadataProps,
file.pubMagicMetadata,
file.key
);
const updateResult = await updateFilePublicMagicMetadata([
{ file, updatedPublicMagicMetadata },
]);
return updateResult[0];
}
export async function changeFileName(
file: EnteFile,
editedName: string
): Promise<EnteFile> {
const updatedPublicMagicMetadataProps: FilePublicMagicMetadataProps = {
editedName,
};
const updatedPublicMagicMetadata: FilePublicMagicMetadata =
await updateMagicMetadata(
updatedPublicMagicMetadataProps,
file.pubMagicMetadata,
file.key
);
const updateResult = await updateFilePublicMagicMetadata([
{ file, updatedPublicMagicMetadata },
]);
return updateResult[0];
}
export async function changeCaption(
file: EnteFile,
caption: string
): Promise<EnteFile> {
const updatedPublicMagicMetadataProps: FilePublicMagicMetadataProps = {
caption,
};
const updatedPublicMagicMetadata: FilePublicMagicMetadata =
await updateMagicMetadata(
updatedPublicMagicMetadataProps,
file.pubMagicMetadata,
file.key
);
const updateResult = await updateFilePublicMagicMetadata([
{ file, updatedPublicMagicMetadata },
]);
return updateResult[0];
}
export function isSharedFile(user: User, file: EnteFile) {
if (!user?.id || !file?.ownerID) {
return false;
}
return file.ownerID !== user.id;
}
export function mergeMetadata(files: EnteFile[]): EnteFile[] {
return files.map((file) => {
if (file.pubMagicMetadata?.data.editedTime) {
file.metadata.creationTime = file.pubMagicMetadata.data.editedTime;
}
if (file.pubMagicMetadata?.data.editedName) {
file.metadata.title = file.pubMagicMetadata.data.editedName;
}
return file;
});
}
export function updateExistingFilePubMetadata(
existingFile: EnteFile,
updatedFile: EnteFile
) {
existingFile.pubMagicMetadata = updatedFile.pubMagicMetadata;
existingFile.metadata = mergeMetadata([existingFile])[0].metadata;
}
export async function getFileFromURL(fileURL: string) {
const fileBlob = await (await fetch(fileURL)).blob();
const fileFile = new File([fileBlob], 'temp');
return fileFile;
}
export function getUniqueFiles(files: EnteFile[]) {
const idSet = new Set<number>();
const uniqueFiles = files.filter((file) => {
if (!idSet.has(file.id)) {
idSet.add(file.id);
return true;
} else {
return false;
}
});
return uniqueFiles;
}
export async function downloadFiles(
files: EnteFile[],
progressBarUpdater?: {
increaseSuccess: () => void;
increaseFailed: () => void;
isCancelled: () => boolean;
}
) {
for (const file of files) {
try {
if (progressBarUpdater?.isCancelled()) {
return;
}
await downloadFile(file, false);
progressBarUpdater?.increaseSuccess();
} catch (e) {
logError(e, 'download fail for file');
progressBarUpdater?.increaseFailed();
}
}
}
export async function downloadFilesDesktop(
files: EnteFile[],
progressBarUpdater: {
increaseSuccess: () => void;
increaseFailed: () => void;
isCancelled: () => boolean;
},
downloadPath: string
) {
const fileReader = new FileReader();
for (const file of files) {
try {
if (progressBarUpdater?.isCancelled()) {
return;
}
await downloadFileDesktop(fileReader, file, downloadPath);
progressBarUpdater?.increaseSuccess();
} catch (e) {
logError(e, 'download fail for file');
progressBarUpdater?.increaseFailed();
}
}
}
export async function downloadFileDesktop(
fileReader: FileReader,
file: EnteFile,
downloadPath: string
) {
let fileStream: ReadableStream<Uint8Array>;
const fileURL = await DownloadManager.getCachedOriginalFile(file)[0];
if (!fileURL) {
fileStream = await DownloadManager.downloadFile(file);
} else {
fileStream = await fetch(fileURL).then((res) => res.body);
}
const updatedFileStream = await getUpdatedEXIFFileForDownload(
fileReader,
file,
fileStream
);
if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) {
const fileBlob = await new Response(updatedFileStream).blob();
const livePhoto = await decodeLivePhoto(file, fileBlob);
const imageExportName = getUniqueFileExportName(
downloadPath,
livePhoto.imageNameTitle
);
const imageStream = generateStreamFromArrayBuffer(livePhoto.image);
await ElectronFSService.saveMediaFile(
getFileExportPath(downloadPath, imageExportName),
imageStream
);
try {
const videoExportName = getUniqueFileExportName(
downloadPath,
livePhoto.videoNameTitle
);
const videoStream = generateStreamFromArrayBuffer(livePhoto.video);
await ElectronFSService.saveMediaFile(
getFileExportPath(downloadPath, videoExportName),
videoStream
);
} catch (e) {
ElectronFSService.deleteFile(
getFileExportPath(downloadPath, imageExportName)
);
throw e;
}
} else {
const fileExportName = getUniqueFileExportName(
downloadPath,
file.metadata.title
);
await ElectronFSService.saveMediaFile(
getFileExportPath(downloadPath, fileExportName),
updatedFileStream
);
}
}
export const isImageOrVideo = (fileType: FILE_TYPE) =>
[FILE_TYPE.IMAGE, FILE_TYPE.VIDEO].includes(fileType);
export const getArchivedFiles = (files: EnteFile[]) => {
return files.filter(isArchivedFile).map((file) => file.id);
};
export const createTypedObjectURL = async (blob: Blob, fileName: string) => {
const type = await getFileType(new File([blob], fileName));
return URL.createObjectURL(new Blob([blob], { type: type.mimeType }));
};
export const getUserOwnedFiles = (files: EnteFile[]) => {
const user: User = getData(LS_KEYS.USER);
if (!user?.id) {
throw Error('user missing');
}
return files.filter((file) => file.ownerID === user.id);
};
// doesn't work on firefox
export const copyFileToClipboard = async (fileUrl: string) => {
const canvas = document.createElement('canvas');
const canvasCTX = canvas.getContext('2d');
const image = new Image();
const blobPromise = new Promise<Blob>((resolve, reject) => {
let timeout: NodeJS.Timeout = null;
try {
image.setAttribute('src', fileUrl);
image.onload = () => {
canvas.width = image.width;
canvas.height = image.height;
canvasCTX.drawImage(image, 0, 0, image.width, image.height);
canvas.toBlob(
(blob) => {
resolve(blob);
},
'image/png',
1
);
clearTimeout(timeout);
};
} catch (e) {
void logError(e, 'failed to copy to clipboard');
reject(e);
} finally {
clearTimeout(timeout);
}
timeout = setTimeout(
() => reject(Error(CustomError.WAIT_TIME_EXCEEDED)),
WAIT_TIME_IMAGE_CONVERSION
);
});
const { ClipboardItem } = window;
await navigator.clipboard
.write([new ClipboardItem({ 'image/png': blobPromise })])
.catch((e) => logError(e, 'failed to copy to clipboard'));
};
export function getLatestVersionFiles(files: EnteFile[]) {
const latestVersionFiles = new Map<string, EnteFile>();
files.forEach((file) => {
const uid = `${file.collectionID}-${file.id}`;
if (
!latestVersionFiles.has(uid) ||
latestVersionFiles.get(uid).updationTime < file.updationTime
) {
latestVersionFiles.set(uid, file);
}
});
return Array.from(latestVersionFiles.values()).filter(
(file) => !file.isDeleted
);
}
export function getPersonalFiles(files: EnteFile[], user: User) {
if (!user?.id) {
throw Error('user missing');
}
return files.filter((file) => file.ownerID === user.id);
}
export function getIDBasedSortedFiles(files: EnteFile[]) {
return files.sort((a, b) => a.id - b.id);
}
export function constructFileToCollectionMap(files: EnteFile[]) {
const fileToCollectionsMap = new Map<number, number[]>();
(files ?? []).forEach((file) => {
if (!fileToCollectionsMap.get(file.id)) {
fileToCollectionsMap.set(file.id, []);
}
fileToCollectionsMap.get(file.id).push(file.collectionID);
});
return fileToCollectionsMap;
}
export const shouldShowAvatar = (file: EnteFile, user: User) => {
if (!file || !user) {
return false;
}
// is Shared file
else if (file.ownerID !== user.id) {
return true;
}
// is public collected file
else if (
file.ownerID === user.id &&
file.pubMagicMetadata?.data?.uploaderName
) {
return true;
} else {
return false;
}
};
export const handleFileOps = async (
ops: FILE_OPS_TYPE,
files: EnteFile[],
setDeletedFileIds: (
deletedFileIds: Set<number> | ((prev: Set<number>) => Set<number>)
) => void,
setHiddenFileIds: (
hiddenFileIds: Set<number> | ((prev: Set<number>) => Set<number>)
) => void,
setFixCreationTimeAttributes: (
fixCreationTimeAttributes:
| {
files: EnteFile[];
}
| ((prev: { files: EnteFile[] }) => { files: EnteFile[] })
) => void
) => {
switch (ops) {
case FILE_OPS_TYPE.TRASH:
await deleteFileHelper(files, false, setDeletedFileIds);
break;
case FILE_OPS_TYPE.DELETE_PERMANENTLY:
await deleteFileHelper(files, true, setDeletedFileIds);
break;
case FILE_OPS_TYPE.HIDE:
await hideFilesHelper(files, setHiddenFileIds);
break;
case FILE_OPS_TYPE.DOWNLOAD:
await downloadFiles(files);
break;
case FILE_OPS_TYPE.FIX_TIME:
fixTimeHelper(files, setFixCreationTimeAttributes);
break;
case FILE_OPS_TYPE.ARCHIVE:
await changeFilesVisibility(files, VISIBILITY_STATE.ARCHIVED);
break;
case FILE_OPS_TYPE.UNARCHIVE:
await changeFilesVisibility(files, VISIBILITY_STATE.VISIBLE);
break;
}
};
const deleteFileHelper = async (
selectedFiles: EnteFile[],
permanent: boolean,
setDeletedFileIds: (
deletedFileIds: Set<number> | ((prev: Set<number>) => Set<number>)
) => void
) => {
try {
setDeletedFileIds((deletedFileIds) => {
selectedFiles.forEach((file) => deletedFileIds.add(file.id));
return new Set(deletedFileIds);
});
if (permanent) {
await deleteFromTrash(selectedFiles.map((file) => file.id));
} else {
await trashFiles(selectedFiles);
}
} catch (e) {
setDeletedFileIds(new Set());
throw e;
}
};
const hideFilesHelper = async (
selectedFiles: EnteFile[],
setHiddenFileIds: (
hiddenFileIds: Set<number> | ((prev: Set<number>) => Set<number>)
) => void
) => {
try {
setHiddenFileIds((hiddenFileIds) => {
selectedFiles.forEach((file) => hiddenFileIds.add(file.id));
return new Set(hiddenFileIds);
});
await moveToHiddenCollection(selectedFiles);
} catch (e) {
setHiddenFileIds(new Set());
throw e;
}
};
const fixTimeHelper = async (
selectedFiles: EnteFile[],
setFixCreationTimeAttributes: (fixCreationTimeAttributes: {
files: EnteFile[];
}) => void
) => {
setFixCreationTimeAttributes({ files: selectedFiles });
};
const getFileObjectURLs = (originalBlob: Blob, convertedBlob: Blob) => {
const originalURL = URL.createObjectURL(originalBlob);
const convertedURL = convertedBlob
? convertedBlob === originalBlob
? originalURL
: URL.createObjectURL(convertedBlob)
: null;
return { originalURL, convertedURL };
};

View file

@ -0,0 +1,42 @@
import { FILE_TYPE } from 'constants/file';
import { getFileExtension } from 'utils/file';
const IMAGE_EXTENSIONS = [
'heic',
'heif',
'jpeg',
'jpg',
'png',
'gif',
'bmp',
'tiff',
'webp',
];
const VIDEO_EXTENSIONS = [
'mov',
'mp4',
'm4v',
'avi',
'wmv',
'flv',
'mkv',
'webm',
'3gp',
'3g2',
'avi',
'ogv',
'mpg',
'mp',
];
export function getFileTypeFromExtensionForLivePhotoClustering(
filename: string
) {
const extension = getFileExtension(filename)?.toLowerCase();
if (IMAGE_EXTENSIONS.includes(extension)) {
return FILE_TYPE.IMAGE;
} else if (VIDEO_EXTENSIONS.includes(extension)) {
return FILE_TYPE.VIDEO;
}
}

View file

@ -0,0 +1,12 @@
export function convertBytesToHumanReadable(
bytes: number,
precision = 2
): string {
if (bytes === 0 || isNaN(bytes)) {
return '0 MB';
}
const i = Math.floor(Math.log(bytes) / Math.log(1024));
const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
return (bytes / Math.pow(1024, i)).toFixed(precision) + ' ' + sizes[i];
}

View file

@ -0,0 +1,116 @@
import { ElectronFile } from 'types/upload';
import { convertBytesToHumanReadable } from 'utils/file/size';
import { formatDateTimeShort } from 'utils/time/format';
import { isDEVSentryENV } from 'constants/sentry';
import isElectron from 'is-electron';
// import ElectronService from 'services/electron/common';
import { logError } from 'utils/sentry';
import {
getData,
LS_KEYS,
removeData,
setData,
} from 'utils/storage/localStorage';
export const MAX_LOG_SIZE = 5 * 1024 * 1024; // 5MB
export const MAX_LOG_LINES = 1000;
export interface Log {
timestamp: number;
logLine: string;
}
export function addLogLine(
log: string | number | boolean,
...optionalParams: (string | number | boolean)[]
) {
try {
const completeLog = [log, ...optionalParams].join(' ');
if (isDEVSentryENV()) {
console.log(completeLog);
}
if (isElectron()) {
// ElectronService.logToDisk(completeLog);
} else {
saveLogLine({
timestamp: Date.now(),
logLine: completeLog,
});
}
} catch (e) {
if (e.name === 'QuotaExceededError') {
deleteLogs();
addLogLine('logs cleared');
}
logError(e, 'failed to addLogLine', undefined, true);
// ignore
}
}
export const addLocalLog = (getLog: () => string) => {
if (isDEVSentryENV()) {
console.log(getLog());
}
};
export function getDebugLogs() {
return combineLogLines(getLogs());
}
export function getFileNameSize(file: File | ElectronFile) {
return `${file.name}_${convertBytesToHumanReadable(file.size)}`;
}
export const clearLogsIfLocalStorageLimitExceeded = () => {
try {
const logs = getDebugLogs();
const logSize = getStringSize(logs);
if (logSize > MAX_LOG_SIZE) {
deleteLogs();
addLogLine('Logs cleared due to size limit exceeded');
} else {
try {
addLogLine(`app started`);
} catch (e) {
deleteLogs();
}
}
addLogLine(`logs size: ${convertBytesToHumanReadable(logSize)}`);
} catch (e) {
logError(e, 'failed to clearLogsIfLocalStorageLimitExceeded');
}
};
function saveLogLine(log: Log) {
const logs = getLogs();
if (logs.length > MAX_LOG_LINES) {
logs.slice(logs.length - MAX_LOG_LINES);
}
logs.push(log);
setLogs(logs);
}
function getLogs(): Log[] {
return getData(LS_KEYS.LOGS)?.logs ?? [];
}
function setLogs(logs: Log[]) {
setData(LS_KEYS.LOGS, { logs });
}
function deleteLogs() {
removeData(LS_KEYS.LOGS);
}
function getStringSize(str: string) {
return new Blob([str]).size;
}
function formatLog(log: Log) {
return `[${formatDateTimeShort(log.timestamp)}] ${log.logLine}`;
}
function combineLogLines(logs: Log[]) {
return logs.map(formatLog).join('\n');
}

View file

@ -0,0 +1,167 @@
import { FILE_TYPE } from 'constants/file';
import { Collection } from 'types/collection';
import { B64EncryptionResult, LocalFileAttributes } from 'types/crypto';
import {
MetadataFileAttributes,
S3FileAttributes,
FilePublicMagicMetadata,
FilePublicMagicMetadataProps,
} from 'types/file';
import { EncryptedMagicMetadata } from 'types/magicMetadata';
export interface DataStream {
stream: ReadableStream<Uint8Array>;
chunkCount: number;
}
export function isDataStream(object: any): object is DataStream {
return 'stream' in object;
}
export type Logger = (message: string) => void;
export interface Metadata {
title: string;
creationTime: number;
modificationTime: number;
latitude: number;
longitude: number;
fileType: FILE_TYPE;
hasStaticThumbnail?: boolean;
hash?: string;
imageHash?: string;
videoHash?: string;
localID?: number;
version?: number;
deviceFolder?: string;
}
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;
mimeType?: string;
imageType?: string;
videoType?: string;
}
/*
* ElectronFile is a custom interface that is used to represent
* any file on disk as a File-like object in the Electron desktop app.
*
* This was added to support the auto-resuming of failed uploads
* which needed absolute paths to the files which the
* normal File interface does not provide.
*/
export interface ElectronFile {
name: string;
path: string;
size: number;
lastModified: number;
stream: () => Promise<ReadableStream<Uint8Array>>;
blob: () => Promise<Blob>;
arrayBuffer: () => Promise<Uint8Array>;
}
export interface UploadAsset {
isLivePhoto?: boolean;
file?: File | ElectronFile;
livePhotoAssets?: LivePhotoAssets;
isElectron?: boolean;
}
export interface LivePhotoAssets {
image: globalThis.File | ElectronFile;
video: globalThis.File | ElectronFile;
}
export interface FileWithCollection extends UploadAsset {
localID: number;
collection?: Collection;
collectionID?: number;
}
export type ParsedMetadataJSONMap = Map<string, ParsedMetadataJSON>;
export interface UploadURL {
url: string;
objectKey: string;
}
export interface FileInMemory {
filedata: Uint8Array | DataStream;
thumbnail: Uint8Array;
hasStaticThumbnail: boolean;
}
export interface FileWithMetadata
extends Omit<FileInMemory, 'hasStaticThumbnail'> {
metadata: Metadata;
localID: number;
pubMagicMetadata: FilePublicMagicMetadata;
}
export interface EncryptedFile {
file: ProcessedFile;
fileKey: B64EncryptionResult;
}
export interface ProcessedFile {
file: LocalFileAttributes<Uint8Array | DataStream>;
thumbnail: LocalFileAttributes<Uint8Array>;
metadata: LocalFileAttributes<string>;
pubMagicMetadata: EncryptedMagicMetadata;
localID: number;
}
export interface BackupedFile {
file: S3FileAttributes;
thumbnail: S3FileAttributes;
metadata: MetadataFileAttributes;
pubMagicMetadata: EncryptedMagicMetadata;
}
export interface UploadFile extends BackupedFile {
collectionID: number;
encryptedKey: string;
keyDecryptionNonce: string;
}
export interface ParsedExtractedMetadata {
location: Location;
creationTime: number;
width: number;
height: number;
}
// This is used to prompt the user the make upload strategy choice
export interface ImportSuggestion {
rootFolderName: string;
hasNestedFolders: boolean;
hasRootLevelFileWithFolder: boolean;
}
export interface PublicUploadProps {
token: string;
passwordToken: string;
accessedThroughSharedURL: boolean;
}
export interface ExtractMetadataResult {
metadata: Metadata;
publicMagicMetadata: FilePublicMagicMetadataProps;
}

View file

@ -0,0 +1,43 @@
import { UPLOAD_RESULT, UPLOAD_STAGES } from 'constants/upload';
export type FileID = number;
export type FileName = string;
export type PercentageUploaded = number;
export type UploadFileNames = Map<FileID, FileName>;
export interface UploadCounter {
finished: number;
total: number;
}
export interface InProgressUpload {
localFileID: FileID;
progress: PercentageUploaded;
}
export interface FinishedUpload {
localFileID: FileID;
result: UPLOAD_RESULT;
}
export type InProgressUploads = Map<FileID, PercentageUploaded>;
export type FinishedUploads = Map<FileID, UPLOAD_RESULT>;
export type SegregatedFinishedUploads = Map<UPLOAD_RESULT, FileID[]>;
export interface ProgressUpdater {
setPercentComplete: React.Dispatch<React.SetStateAction<number>>;
setUploadCounter: React.Dispatch<React.SetStateAction<UploadCounter>>;
setUploadStage: React.Dispatch<React.SetStateAction<UPLOAD_STAGES>>;
setInProgressUploads: React.Dispatch<
React.SetStateAction<InProgressUpload[]>
>;
setFinishedUploads: React.Dispatch<
React.SetStateAction<SegregatedFinishedUploads>
>;
setUploadFilenames: React.Dispatch<React.SetStateAction<UploadFileNames>>;
setHasLivePhotos: React.Dispatch<React.SetStateAction<boolean>>;
setUploadProgressView: React.Dispatch<React.SetStateAction<boolean>>;
}

View file

@ -0,0 +1,97 @@
import { Collection } from 'types/collection';
import { EnteFile } from 'types/file';
import { MagicMetadataCore, VISIBILITY_STATE } from 'types/magicMetadata';
import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker';
export function isArchivedFile(item: EnteFile): boolean {
if (!item || !item.magicMetadata || !item.magicMetadata.data) {
return false;
}
return item.magicMetadata.data.visibility === VISIBILITY_STATE.ARCHIVED;
}
export function isArchivedCollection(item: Collection): boolean {
if (!item) {
return false;
}
if (item.magicMetadata && item.magicMetadata.data) {
return item.magicMetadata.data.visibility === VISIBILITY_STATE.ARCHIVED;
}
if (item.sharedMagicMetadata && item.sharedMagicMetadata.data) {
return (
item.sharedMagicMetadata.data.visibility ===
VISIBILITY_STATE.ARCHIVED
);
}
return false;
}
export function isPinnedCollection(item: Collection) {
if (
!item ||
!item.magicMetadata ||
!item.magicMetadata.data ||
typeof item.magicMetadata.data === 'string' ||
typeof item.magicMetadata.data.order === 'undefined'
) {
return false;
}
return item.magicMetadata.data.order !== 0;
}
export async function updateMagicMetadata<T>(
magicMetadataUpdates: T,
originalMagicMetadata?: MagicMetadataCore<T>,
decryptionKey?: string
): Promise<MagicMetadataCore<T>> {
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
if (!originalMagicMetadata) {
originalMagicMetadata = getNewMagicMetadata<T>();
}
if (typeof originalMagicMetadata?.data === 'string') {
originalMagicMetadata.data = await cryptoWorker.decryptMetadata(
originalMagicMetadata.data,
originalMagicMetadata.header,
decryptionKey
);
}
// copies the existing magic metadata properties of the files and updates the visibility value
// The expected behavior while updating magic metadata is to let the existing property as it is and update/add the property you want
const magicMetadataProps: T = {
...originalMagicMetadata.data,
...magicMetadataUpdates,
};
const nonEmptyMagicMetadataProps =
getNonEmptyMagicMetadataProps(magicMetadataProps);
const magicMetadata = {
...originalMagicMetadata,
data: nonEmptyMagicMetadataProps,
count: Object.keys(nonEmptyMagicMetadataProps).length,
};
return magicMetadata;
}
export const getNewMagicMetadata = <T>(): MagicMetadataCore<T> => {
return {
version: 1,
data: null,
header: null,
count: 0,
};
};
export const getNonEmptyMagicMetadataProps = <T>(magicMetadataProps: T): T => {
return Object.fromEntries(
Object.entries(magicMetadataProps).filter(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
([_, v]) => v !== null && v !== undefined
)
) as T;
};

View file

@ -0,0 +1,75 @@
import * as Sentry from '@sentry/nextjs';
import { addLocalLog, addLogLine } from 'utils/logging';
import { getSentryUserID } from 'utils/user';
import InMemoryStore, { MS_KEYS } from 'services/InMemoryStore';
import { getHasOptedOutOfCrashReports } from 'utils/storage';
import { ApiError } from 'utils/error';
export const logError = async (
error: any,
msg: string,
info?: Record<string, unknown>,
skipAddLogLine = false
) => {
const err = errorWithContext(error, msg);
if (!skipAddLogLine) {
if (error instanceof ApiError) {
addLogLine(`error: ${error?.name} ${error?.message}
msg: ${msg} errorCode: ${JSON.stringify(error?.errCode)}
httpStatusCode: ${JSON.stringify(error?.httpStatusCode)} ${
info ? `info: ${JSON.stringify(info)}` : ''
}
${error?.stack}`);
} else {
addLogLine(
`error: ${error?.name} ${error?.message}
msg: ${msg} ${info ? `info: ${JSON.stringify(info)}` : ''}
${error?.stack}`
);
}
}
if (!InMemoryStore.has(MS_KEYS.OPT_OUT_OF_CRASH_REPORTS)) {
const optedOutOfCrashReports = getHasOptedOutOfCrashReports();
InMemoryStore.set(
MS_KEYS.OPT_OUT_OF_CRASH_REPORTS,
optedOutOfCrashReports
);
}
if (InMemoryStore.get(MS_KEYS.OPT_OUT_OF_CRASH_REPORTS)) {
addLocalLog(() => `skipping sentry error: ${error?.name}`);
return;
}
if (isErrorUnnecessaryForSentry(error)) {
return;
}
Sentry.captureException(err, {
level: 'info',
user: { id: await getSentryUserID() },
contexts: {
...(info && {
info: info,
}),
rootCause: { message: error?.message, completeError: error },
},
});
};
// copy of errorWithContext to prevent importing error util
function errorWithContext(originalError: Error, context: string) {
const errorWithContext = new Error(context);
errorWithContext.stack =
errorWithContext.stack.split('\n').slice(2, 4).join('\n') +
'\n' +
originalError.stack;
return errorWithContext;
}
function isErrorUnnecessaryForSentry(error: any) {
if (error?.message?.includes('Network Error')) {
return true;
} else if (error?.status === 401) {
return true;
}
return false;
}

View file

@ -0,0 +1,40 @@
import { Language } from 'constants/locale';
import { getData, LS_KEYS, setData } from './localStorage';
export const isFirstLogin = () =>
getData(LS_KEYS.IS_FIRST_LOGIN)?.status ?? false;
export function setIsFirstLogin(status) {
setData(LS_KEYS.IS_FIRST_LOGIN, { status });
}
export const justSignedUp = () =>
getData(LS_KEYS.JUST_SIGNED_UP)?.status ?? false;
export function setJustSignedUp(status) {
setData(LS_KEYS.JUST_SIGNED_UP, { status });
}
export function getLivePhotoInfoShownCount() {
return getData(LS_KEYS.LIVE_PHOTO_INFO_SHOWN_COUNT)?.count ?? 0;
}
export function setLivePhotoInfoShownCount(count) {
setData(LS_KEYS.LIVE_PHOTO_INFO_SHOWN_COUNT, { count });
}
export function getUserLocale(): Language {
return getData(LS_KEYS.LOCALE)?.value;
}
export function getLocalMapEnabled(): boolean {
return getData(LS_KEYS.MAP_ENABLED)?.value ?? false;
}
export function setLocalMapEnabled(value: boolean) {
setData(LS_KEYS.MAP_ENABLED, { value });
}
export function getHasOptedOutOfCrashReports(): boolean {
return getData(LS_KEYS.OPT_OUT_OF_CRASH_REPORTS)?.value ?? false;
}

View file

@ -0,0 +1,12 @@
import { runningInBrowser } from 'utils/common';
import localForage from 'localforage';
if (runningInBrowser()) {
localForage.config({
name: 'ente-files',
version: 1.0,
storeName: 'files',
});
}
export default localForage;

View file

@ -0,0 +1,68 @@
import { logError } from 'utils/sentry';
export enum LS_KEYS {
USER = 'user',
SESSION = 'session',
KEY_ATTRIBUTES = 'keyAttributes',
ORIGINAL_KEY_ATTRIBUTES = 'originalKeyAttributes',
SUBSCRIPTION = 'subscription',
FAMILY_DATA = 'familyData',
PLANS = 'plans',
IS_FIRST_LOGIN = 'isFirstLogin',
JUST_SIGNED_UP = 'justSignedUp',
SHOW_BACK_BUTTON = 'showBackButton',
EXPORT = 'export',
AnonymizedUserID = 'anonymizedUserID',
THUMBNAIL_FIX_STATE = 'thumbnailFixState',
LIVE_PHOTO_INFO_SHOWN_COUNT = 'livePhotoInfoShownCount',
LOGS = 'logs',
USER_DETAILS = 'userDetails',
COLLECTION_SORT_BY = 'collectionSortBy',
THEME = 'theme',
WAIT_TIME = 'waitTime',
API_ENDPOINT = 'apiEndpoint',
LOCALE = 'locale',
MAP_ENABLED = 'mapEnabled',
SRP_SETUP_ATTRIBUTES = 'srpSetupAttributes',
SRP_ATTRIBUTES = 'srpAttributes',
OPT_OUT_OF_CRASH_REPORTS = 'optOutOfCrashReports',
CF_PROXY_DISABLED = 'cfProxyDisabled',
}
export const setData = (key: LS_KEYS, value: object) => {
if (typeof localStorage === 'undefined') {
return null;
}
localStorage.setItem(key, JSON.stringify(value));
};
export const removeData = (key: LS_KEYS) => {
if (typeof localStorage === 'undefined') {
return null;
}
localStorage.removeItem(key);
};
export const getData = (key: LS_KEYS) => {
try {
if (
typeof localStorage === 'undefined' ||
typeof key === 'undefined' ||
typeof localStorage.getItem(key) === 'undefined' ||
localStorage.getItem(key) === 'undefined'
) {
return null;
}
const data = localStorage.getItem(key);
return data && JSON.parse(data);
} catch (e) {
logError(e, 'Failed to Parse JSON for key ' + key);
}
};
export const clearData = () => {
if (typeof localStorage === 'undefined') {
return null;
}
localStorage.clear();
};

View file

@ -0,0 +1,32 @@
export enum SESSION_KEYS {
ENCRYPTION_KEY = 'encryptionKey',
KEY_ENCRYPTION_KEY = 'keyEncryptionKey',
}
export const setKey = (key: SESSION_KEYS, value: object) => {
if (typeof sessionStorage === 'undefined') {
return null;
}
sessionStorage.setItem(key, JSON.stringify(value));
};
export const getKey = (key: SESSION_KEYS) => {
if (typeof sessionStorage === 'undefined') {
return null;
}
return JSON.parse(sessionStorage.getItem(key));
};
export const removeKey = (key: SESSION_KEYS) => {
if (typeof sessionStorage === 'undefined') {
return null;
}
sessionStorage.removeItem(key);
};
export const clearKeys = () => {
if (typeof sessionStorage === 'undefined') {
return null;
}
sessionStorage.clear();
};

View file

@ -0,0 +1,78 @@
import i18n, { t } from 'i18next';
const dateTimeFullFormatter1 = new Intl.DateTimeFormat(i18n.language, {
weekday: 'short',
month: 'short',
day: 'numeric',
});
const dateTimeFullFormatter2 = new Intl.DateTimeFormat(i18n.language, {
year: 'numeric',
});
const dateTimeShortFormatter = new Intl.DateTimeFormat(i18n.language, {
month: 'short',
day: 'numeric',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
const timeFormatter = new Intl.DateTimeFormat(i18n.language, {
timeStyle: 'short',
});
export function formatDateFull(date: number | Date) {
return [dateTimeFullFormatter1, dateTimeFullFormatter2]
.map((f) => f.format(date))
.join(' ');
}
export function formatDate(date: number | Date) {
const withinYear =
new Date().getFullYear() === new Date(date).getFullYear();
const dateTimeFormat2 = !withinYear ? dateTimeFullFormatter2 : null;
return [dateTimeFullFormatter1, dateTimeFormat2]
.filter((f) => !!f)
.map((f) => f.format(date))
.join(' ');
}
export function formatDateTimeShort(date: number | Date) {
return dateTimeShortFormatter.format(date);
}
export function formatTime(date: number | Date) {
return timeFormatter.format(date).toUpperCase();
}
export function formatDateTimeFull(dateTime: number | Date): string {
return [formatDateFull(dateTime), t('at'), formatTime(dateTime)].join(' ');
}
export function formatDateTime(dateTime: number | Date): string {
return [formatDate(dateTime), t('at'), formatTime(dateTime)].join(' ');
}
export function formatDateRelative(date: number) {
const units = {
year: 24 * 60 * 60 * 1000 * 365,
month: (24 * 60 * 60 * 1000 * 365) / 12,
day: 24 * 60 * 60 * 1000,
hour: 60 * 60 * 1000,
minute: 60 * 1000,
second: 1000,
};
const relativeDateFormat = new Intl.RelativeTimeFormat(i18n.language, {
localeMatcher: 'best fit',
numeric: 'always',
style: 'long',
});
const elapsed = date - Date.now(); // "Math.abs" accounts for both "past" & "future" scenarios
for (const u in units)
if (Math.abs(elapsed) > units[u] || u === 'second')
return relativeDateFormat.format(
Math.round(elapsed / units[u]),
u as Intl.RelativeTimeFormatUnit
);
}

View file

@ -0,0 +1,177 @@
export interface TimeDelta {
hours?: number;
days?: number;
months?: number;
years?: number;
}
interface DateComponent<T = number> {
year: T;
month: T;
day: T;
hour: T;
minute: T;
second: T;
}
export function getUnixTimeInMicroSecondsWithDelta(delta: TimeDelta): number {
let currentDate = new Date();
if (delta?.hours) {
currentDate = _addHours(currentDate, delta.hours);
}
if (delta?.days) {
currentDate = _addDays(currentDate, delta.days);
}
if (delta?.months) {
currentDate = _addMonth(currentDate, delta.months);
}
if (delta?.years) {
currentDate = _addYears(currentDate, delta.years);
}
return currentDate.getTime() * 1000;
}
export function validateAndGetCreationUnixTimeInMicroSeconds(dateTime: Date) {
if (!dateTime || isNaN(dateTime.getTime())) {
return null;
}
const unixTime = dateTime.getTime() * 1000;
//ignoring dateTimeString = "0000:00:00 00:00:00"
if (unixTime === Date.UTC(0, 0, 0, 0, 0, 0, 0) || unixTime === 0) {
return null;
} else if (unixTime > Date.now() * 1000) {
return null;
} else {
return unixTime;
}
}
function _addDays(date: Date, days: number): Date {
const result = new Date(date);
result.setDate(date.getDate() + days);
return result;
}
function _addHours(date: Date, hours: number): Date {
const result = new Date(date);
result.setHours(date.getHours() + hours);
return result;
}
function _addMonth(date: Date, months: number) {
const result = new Date(date);
result.setMonth(date.getMonth() + months);
return result;
}
function _addYears(date: Date, years: number) {
const result = new Date(date);
result.setFullYear(date.getFullYear() + years);
return result;
}
/*
generates data component for date in format YYYYMMDD-HHMMSS
*/
export function parseDateFromFusedDateString(dateTime: string) {
const dateComponent: DateComponent<number> = convertDateComponentToNumber({
year: dateTime.slice(0, 4),
month: dateTime.slice(4, 6),
day: dateTime.slice(6, 8),
hour: dateTime.slice(9, 11),
minute: dateTime.slice(11, 13),
second: dateTime.slice(13, 15),
});
return validateAndGetDateFromComponents(dateComponent);
}
/* sample date format = 2018-08-19 12:34:45
the date has six symbol separated number values
which we would extract and use to form the date
*/
export function tryToParseDateTime(dateTime: string): Date {
const dateComponent = getDateComponentsFromSymbolJoinedString(dateTime);
if (dateComponent.year?.length === 8 && dateComponent.month?.length === 6) {
// the filename has size 8 consecutive and then 6 consecutive digits
// high possibility that the it is a date in format YYYYMMDD-HHMMSS
const possibleDateTime = dateComponent.year + '-' + dateComponent.month;
return parseDateFromFusedDateString(possibleDateTime);
}
return validateAndGetDateFromComponents(
convertDateComponentToNumber(dateComponent)
);
}
function getDateComponentsFromSymbolJoinedString(
dateTime: string
): DateComponent<string> {
const [year, month, day, hour, minute, second] =
dateTime.match(/\d+/g) ?? [];
return { year, month, day, hour, minute, second };
}
function validateAndGetDateFromComponents(
dateComponent: DateComponent<number>
) {
let date = getDateFromComponents(dateComponent);
if (hasTimeValues(dateComponent) && !isTimePartValid(date, dateComponent)) {
// if the date has time values but they are not valid
// then we remove the time values and try to validate the date
date = getDateFromComponents(removeTimeValues(dateComponent));
}
if (!isDatePartValid(date, dateComponent)) {
return null;
}
return date;
}
function isTimePartValid(date: Date, dateComponent: DateComponent<number>) {
return (
date.getHours() === dateComponent.hour &&
date.getMinutes() === dateComponent.minute &&
date.getSeconds() === dateComponent.second
);
}
function isDatePartValid(date: Date, dateComponent: DateComponent<number>) {
return (
date.getFullYear() === dateComponent.year &&
date.getMonth() === dateComponent.month &&
date.getDate() === dateComponent.day
);
}
function convertDateComponentToNumber(
dateComponent: DateComponent<string>
): DateComponent<number> {
return {
year: Number(dateComponent.year),
// https://stackoverflow.com/questions/2552483/why-does-the-month-argument-range-from-0-to-11-in-javascripts-date-constructor
month: Number(dateComponent.month) - 1,
day: Number(dateComponent.day),
hour: Number(dateComponent.hour),
minute: Number(dateComponent.minute),
second: Number(dateComponent.second),
};
}
function getDateFromComponents(dateComponent: DateComponent<number>) {
const { year, month, day, hour, minute, second } = dateComponent;
if (hasTimeValues(dateComponent)) {
return new Date(year, month, day, hour, minute, second);
} else {
return new Date(year, month, day);
}
}
function hasTimeValues(dateComponent: DateComponent<number>) {
const { hour, minute, second } = dateComponent;
return !isNaN(hour) && !isNaN(minute) && !isNaN(second);
}
function removeTimeValues(
dateComponent: DateComponent<number>
): DateComponent<number> {
return { ...dateComponent, hour: 0, minute: 0, second: 0 };
}

View file

@ -0,0 +1,45 @@
import { FamilyData, FamilyMember, User } from 'types/user';
import { logError } from 'utils/sentry';
import { getData, LS_KEYS } from 'utils/storage/localStorage';
export function getLocalFamilyData(): FamilyData {
return getData(LS_KEYS.FAMILY_DATA);
}
// isPartOfFamily return true if the current user is part of some family plan
export function isPartOfFamily(familyData: FamilyData): boolean {
return Boolean(
familyData && familyData.members && familyData.members.length > 0
);
}
// hasNonAdminFamilyMembers return true if the admin user has members in his family
export function hasNonAdminFamilyMembers(familyData: FamilyData): boolean {
return Boolean(isPartOfFamily(familyData) && familyData.members.length > 1);
}
export function isFamilyAdmin(familyData: FamilyData): boolean {
const familyAdmin: FamilyMember = getFamilyPlanAdmin(familyData);
const user: User = getData(LS_KEYS.USER);
return familyAdmin.email === user.email;
}
export function getFamilyPlanAdmin(familyData: FamilyData): FamilyMember {
if (isPartOfFamily(familyData)) {
return familyData.members.find((x) => x.isAdmin);
} else {
logError(
Error(
'verify user is part of family plan before calling this method'
),
'invalid getFamilyPlanAdmin call'
);
}
}
export function getTotalFamilyUsage(familyData: FamilyData): number {
return familyData.members.reduce(
(sum, currentMember) => sum + currentMember.usage,
0
);
}

View file

@ -0,0 +1,52 @@
import isElectron from 'is-electron';
import { UserDetails } from 'types/user';
import { getData, LS_KEYS, setData } from 'utils/storage/localStorage';
// import ElectronService from 'services/electron/common';
import { Buffer } from 'buffer';
export function makeID(length) {
let result = '';
const characters =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
const charactersLength = characters.length;
for (let i = 0; i < length; i++) {
result += characters.charAt(
Math.floor(Math.random() * charactersLength)
);
}
return result;
}
export async function getSentryUserID() {
if (isElectron()) {
// return await ElectronService.getSentryUserID();
} else {
let anonymizeUserID = getData(LS_KEYS.AnonymizedUserID)?.id;
if (!anonymizeUserID) {
anonymizeUserID = makeID(6);
setData(LS_KEYS.AnonymizedUserID, { id: anonymizeUserID });
}
return anonymizeUserID;
}
}
export function getLocalUserDetails(): UserDetails {
return getData(LS_KEYS.USER_DETAILS)?.value;
}
export const isInternalUser = () => {
const userEmail = getData(LS_KEYS.USER)?.email;
if (!userEmail) return false;
return (
userEmail.endsWith('@ente.io') || userEmail === 'kr.anand619@gmail.com'
);
};
export const convertBufferToBase64 = (buffer: Buffer) => {
return buffer.toString('base64');
};
export const convertBase64ToBuffer = (base64: string) => {
return Buffer.from(base64, 'base64');
};

View file

@ -0,0 +1,215 @@
import * as Comlink from 'comlink';
import { StateAddress } from 'libsodium-wrappers';
import * as libsodium from 'utils/crypto/libsodium';
const textDecoder = new TextDecoder();
const textEncoder = new TextEncoder();
export class DedicatedCryptoWorker {
async decryptMetadata(
encryptedMetadata: string,
header: string,
key: string
) {
const encodedMetadata = await libsodium.decryptChaChaOneShot(
await libsodium.fromB64(encryptedMetadata),
await libsodium.fromB64(header),
key
);
return JSON.parse(textDecoder.decode(encodedMetadata));
}
async decryptThumbnail(
fileData: Uint8Array,
header: Uint8Array,
key: string
) {
return libsodium.decryptChaChaOneShot(fileData, header, key);
}
async decryptEmbedding(
encryptedEmbedding: string,
header: string,
key: string
) {
const encodedEmbedding = await libsodium.decryptChaChaOneShot(
await libsodium.fromB64(encryptedEmbedding),
await libsodium.fromB64(header),
key
);
return Float32Array.from(
JSON.parse(textDecoder.decode(encodedEmbedding))
);
}
async decryptFile(fileData: Uint8Array, header: Uint8Array, key: string) {
return libsodium.decryptChaCha(fileData, header, key);
}
async encryptMetadata(metadata: Object, key: string) {
const encodedMetadata = textEncoder.encode(JSON.stringify(metadata));
const { file: encryptedMetadata } =
await libsodium.encryptChaChaOneShot(encodedMetadata, key);
const { encryptedData, ...other } = encryptedMetadata;
return {
file: {
encryptedData: await libsodium.toB64(encryptedData),
...other,
},
key,
};
}
async encryptThumbnail(fileData: Uint8Array, key: string) {
return libsodium.encryptChaChaOneShot(fileData, key);
}
async encryptEmbedding(embedding: Float32Array, key: string) {
const encodedEmbedding = textEncoder.encode(
JSON.stringify(Array.from(embedding))
);
const { file: encryptEmbedding } = await libsodium.encryptChaChaOneShot(
encodedEmbedding,
key
);
const { encryptedData, ...other } = encryptEmbedding;
return {
file: {
encryptedData: await libsodium.toB64(encryptedData),
...other,
},
key,
};
}
async encryptFile(fileData: Uint8Array) {
return libsodium.encryptChaCha(fileData);
}
async encryptFileChunk(
data: Uint8Array,
pushState: StateAddress,
isFinalChunk: boolean
) {
return libsodium.encryptFileChunk(data, pushState, isFinalChunk);
}
async initChunkEncryption() {
return libsodium.initChunkEncryption();
}
async initChunkDecryption(header: Uint8Array, key: Uint8Array) {
return libsodium.initChunkDecryption(header, key);
}
async decryptFileChunk(fileData: Uint8Array, pullState: StateAddress) {
return libsodium.decryptFileChunk(fileData, pullState);
}
async initChunkHashing() {
return libsodium.initChunkHashing();
}
async hashFileChunk(hashState: StateAddress, chunk: Uint8Array) {
return libsodium.hashFileChunk(hashState, chunk);
}
async completeChunkHashing(hashState: StateAddress) {
return libsodium.completeChunkHashing(hashState);
}
async deriveKey(
passphrase: string,
salt: string,
opsLimit: number,
memLimit: number
) {
return libsodium.deriveKey(passphrase, salt, opsLimit, memLimit);
}
async deriveSensitiveKey(passphrase: string, salt: string) {
return libsodium.deriveSensitiveKey(passphrase, salt);
}
async deriveInteractiveKey(passphrase: string, salt: string) {
return libsodium.deriveInteractiveKey(passphrase, salt);
}
async decryptB64(data: string, nonce: string, key: string) {
return libsodium.decryptB64(data, nonce, key);
}
async decryptToUTF8(data: string, nonce: string, key: string) {
return libsodium.decryptToUTF8(data, nonce, key);
}
async encryptToB64(data: string, key: string) {
return libsodium.encryptToB64(data, key);
}
async generateKeyAndEncryptToB64(data: string) {
return libsodium.generateKeyAndEncryptToB64(data);
}
async encryptUTF8(data: string, key: string) {
return libsodium.encryptUTF8(data, key);
}
async generateEncryptionKey() {
return libsodium.generateEncryptionKey();
}
async generateSaltToDeriveKey() {
return libsodium.generateSaltToDeriveKey();
}
async generateKeyPair() {
return libsodium.generateKeyPair();
}
async boxSealOpen(input: string, publicKey: string, secretKey: string) {
return libsodium.boxSealOpen(input, publicKey, secretKey);
}
async boxSeal(input: string, publicKey: string) {
return libsodium.boxSeal(input, publicKey);
}
async generateSubKey(
key: string,
subKeyLength: number,
subKeyID: number,
context: string
) {
return libsodium.generateSubKey(key, subKeyLength, subKeyID, context);
}
async fromUTF8(string: string) {
return libsodium.fromUTF8(string);
}
async toUTF8(data: string) {
return libsodium.toUTF8(data);
}
async toB64(data: Uint8Array) {
return libsodium.toB64(data);
}
async toURLSafeB64(data: Uint8Array) {
return libsodium.toURLSafeB64(data);
}
async fromB64(string: string) {
return libsodium.fromB64(string);
}
async toHex(string: string) {
return libsodium.toHex(string);
}
async fromHex(string: string) {
return libsodium.fromHex(string);
}
}
Comlink.expose(DedicatedCryptoWorker, self);

18
apps/cast/tsconfig.json Normal file
View file

@ -0,0 +1,18 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"baseUrl": "./src",
"downlevelIteration": true,
"jsx": "preserve",
"jsxImportSource": "@emotion/react",
"lib": ["dom", "dom.iterable", "esnext", "webworker"],
"noImplicitAny": false,
"noUnusedLocals": false,
"noUnusedParameters": false,
"strictNullChecks": false,
"target": "es5",
"useUnknownInCatchVariables": false
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.js"],
"exclude": ["node_modules", "out", ".next", "thirdparty"]
}

958
yarn.lock

File diff suppressed because it is too large Load diff