Merge branch 'lighter_buckets_web' into lighter_buckets_server

This commit is contained in:
Min Idzelis
2025-04-29 01:58:00 +00:00
328 changed files with 6090 additions and 2169 deletions

View File

@@ -221,6 +221,12 @@ const checkOtherAccess = async (access: AccessRepository, request: OtherAccessRe
return access.person.checkFaceOwnerAccess(auth.user.id, ids);
}
case Permission.NOTIFICATION_READ:
case Permission.NOTIFICATION_UPDATE:
case Permission.NOTIFICATION_DELETE: {
return access.notification.checkOwnerAccess(auth.user.id, ids);
}
case Permission.TAG_ASSET:
case Permission.TAG_READ:
case Permission.TAG_UPDATE:

View File

@@ -0,0 +1,83 @@
import { asPostgresConnectionConfig } from 'src/utils/database';
describe('database utils', () => {
describe('asPostgresConnectionConfig', () => {
it('should handle sslmode=require', () => {
expect(
asPostgresConnectionConfig({
connectionType: 'url',
url: 'postgres://postgres1:postgres2@database1:54320/immich?sslmode=require',
}),
).toMatchObject({ ssl: {} });
});
it('should handle sslmode=prefer', () => {
expect(
asPostgresConnectionConfig({
connectionType: 'url',
url: 'postgres://postgres1:postgres2@database1:54320/immich?sslmode=prefer',
}),
).toMatchObject({ ssl: {} });
});
it('should handle sslmode=verify-ca', () => {
expect(
asPostgresConnectionConfig({
connectionType: 'url',
url: 'postgres://postgres1:postgres2@database1:54320/immich?sslmode=verify-ca',
}),
).toMatchObject({ ssl: {} });
});
it('should handle sslmode=verify-full', () => {
expect(
asPostgresConnectionConfig({
connectionType: 'url',
url: 'postgres://postgres1:postgres2@database1:54320/immich?sslmode=verify-full',
}),
).toMatchObject({ ssl: {} });
});
it('should handle sslmode=no-verify', () => {
expect(
asPostgresConnectionConfig({
connectionType: 'url',
url: 'postgres://postgres1:postgres2@database1:54320/immich?sslmode=no-verify',
}),
).toMatchObject({ ssl: { rejectUnauthorized: false } });
});
it('should handle ssl=true', () => {
expect(
asPostgresConnectionConfig({
connectionType: 'url',
url: 'postgres://postgres1:postgres2@database1:54320/immich?ssl=true',
}),
).toMatchObject({ ssl: true });
});
it('should reject invalid ssl', () => {
expect(() =>
asPostgresConnectionConfig({
connectionType: 'url',
url: 'postgres://postgres1:postgres2@database1:54320/immich?ssl=invalid',
}),
).toThrowError('Invalid ssl option');
});
it('should handle socket: URLs', () => {
expect(
asPostgresConnectionConfig({ connectionType: 'url', url: 'socket:/run/postgresql?db=database1' }),
).toMatchObject({ host: '/run/postgresql', database: 'database1' });
});
it('should handle sockets in postgres: URLs', () => {
expect(
asPostgresConnectionConfig({ connectionType: 'url', url: 'postgres:///database2?host=/path/to/socket' }),
).toMatchObject({
host: '/path/to/socket',
database: 'database2',
});
});
});
});

View File

@@ -14,33 +14,57 @@ import {
} from 'kysely';
import { PostgresJSDialect } from 'kysely-postgres-js';
import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres';
import { parse } from 'pg-connection-string';
import postgres, { Notice } from 'postgres';
import { columns, Exif, Person } from 'src/database';
import { DB } from 'src/db';
import { AssetFileType } from 'src/enum';
import { TimeBucketSize } from 'src/repositories/asset.repository';
import { AssetSearchBuilderOptions } from 'src/repositories/search.repository';
import { DatabaseConnectionParams } from 'src/types';
type Ssl = 'require' | 'allow' | 'prefer' | 'verify-full' | boolean | object;
export type PostgresConnectionConfig = {
host?: string;
password?: string;
user?: string;
port?: number;
database?: string;
max?: number;
client_encoding?: string;
ssl?: Ssl;
application_name?: string;
fallback_application_name?: string;
options?: string;
};
export const isValidSsl = (ssl?: string | boolean | object): ssl is Ssl =>
const isValidSsl = (ssl?: string | boolean | object): ssl is Ssl =>
typeof ssl !== 'string' || ssl === 'require' || ssl === 'allow' || ssl === 'prefer' || ssl === 'verify-full';
export const getKyselyConfig = (options: PostgresConnectionConfig): KyselyConfig => {
export const asPostgresConnectionConfig = (params: DatabaseConnectionParams) => {
if (params.connectionType === 'parts') {
return {
host: params.host,
port: params.port,
username: params.username,
password: params.password,
database: params.database,
ssl: undefined,
};
}
const { host, port, user, password, database, ...rest } = parse(params.url);
let ssl: Ssl | undefined;
if (rest.ssl) {
if (!isValidSsl(rest.ssl)) {
throw new Error(`Invalid ssl option: ${rest.ssl}`);
}
ssl = rest.ssl;
}
return {
host: host ?? undefined,
port: port ? Number(port) : undefined,
username: user,
password,
database: database ?? undefined,
ssl,
};
};
export const getKyselyConfig = (
params: DatabaseConnectionParams,
options: Partial<postgres.Options<Record<string, postgres.PostgresType>>> = {},
): KyselyConfig => {
const config = asPostgresConnectionConfig(params);
return {
dialect: new PostgresJSDialect({
postgres: postgres({
@@ -67,6 +91,12 @@ export const getKyselyConfig = (options: PostgresConnectionConfig): KyselyConfig
connection: {
TimeZone: 'UTC',
},
host: config.host,
port: config.port,
username: config.username,
password: config.password,
database: config.database,
ssl: config.ssl,
...options,
}),
}),

View File

@@ -34,45 +34,40 @@ const raw: Record<string, string[]> = {
'.x3f': ['image/x3f', 'image/x-sigma-x3f'],
};
/**
* list of supported image extensions from https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Image_types excluding svg
* @TODO share with the client
* @see {@link web/src/lib/utils/asset-utils.ts#L329}
**/
const webSupportedImage = {
'.avif': ['image/avif'],
'.gif': ['image/gif'],
'.jpeg': ['image/jpeg'],
'.jpg': ['image/jpeg'],
'.png': ['image/png', 'image/apng'],
'.webp': ['image/webp'],
};
const image: Record<string, string[]> = {
...raw,
'.avif': ['image/avif'],
...webSupportedImage,
'.bmp': ['image/bmp'],
'.gif': ['image/gif'],
'.heic': ['image/heic'],
'.heif': ['image/heif'],
'.hif': ['image/hif'],
'.insp': ['image/jpeg'],
'.jp2': ['image/jp2'],
'.jpe': ['image/jpeg'],
'.jpeg': ['image/jpeg'],
'.jpg': ['image/jpeg'],
'.jxl': ['image/jxl'],
'.png': ['image/png'],
'.svg': ['image/svg'],
'.tif': ['image/tiff'],
'.tiff': ['image/tiff'],
'.webp': ['image/webp'],
};
const extensionOverrides: Record<string, string> = {
'image/jpeg': '.jpg',
};
/**
* list of supported image extensions from https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Image_types excluding svg
* @TODO share with the client
* @see {@link web/src/lib/utils/asset-utils.ts#L329}
**/
const webSupportedImageMimeTypes = new Set([
'image/apng',
'image/avif',
'image/gif',
'image/jpeg',
'image/png',
'image/webp',
]);
const profileExtensions = new Set(['.avif', '.dng', '.heic', '.heif', '.jpeg', '.jpg', '.png', '.webp', '.svg']);
const profile: Record<string, string[]> = Object.fromEntries(
Object.entries(image).filter(([key]) => profileExtensions.has(key)),
@@ -123,7 +118,7 @@ export const mimeTypes = {
isAsset: (filename: string) => isType(filename, image) || isType(filename, video),
isImage: (filename: string) => isType(filename, image),
isWebSupportedImage: (filename: string) => webSupportedImageMimeTypes.has(lookup(filename)),
isWebSupportedImage: (filename: string) => isType(filename, webSupportedImage),
isProfile: (filename: string) => isType(filename, profile),
isSidecar: (filename: string) => isType(filename, sidecar),
isVideo: (filename: string) => isType(filename, video),

View File

@@ -1,16 +1,11 @@
import _ from 'lodash';
import { UserPreferencesUpdateDto } from 'src/dtos/user-preferences.dto';
import { UserAvatarColor, UserMetadataKey } from 'src/enum';
import { UserMetadataKey } from 'src/enum';
import { DeepPartial, UserMetadataItem, UserPreferences } from 'src/types';
import { HumanReadableSize } from 'src/utils/bytes';
import { getKeysDeep } from 'src/utils/misc';
const getDefaultPreferences = (user: { email: string }): UserPreferences => {
const values = Object.values(UserAvatarColor);
const randomIndex = Math.floor(
[...user.email].map((letter) => letter.codePointAt(0) ?? 0).reduce((a, b) => a + b, 0) % values.length,
);
const getDefaultPreferences = (): UserPreferences => {
return {
folders: {
enabled: false,
@@ -34,9 +29,6 @@ const getDefaultPreferences = (user: { email: string }): UserPreferences => {
enabled: false,
sidebarWeb: false,
},
avatar: {
color: values[randomIndex],
},
emailNotifications: {
enabled: true,
albumInvite: true,
@@ -53,8 +45,8 @@ const getDefaultPreferences = (user: { email: string }): UserPreferences => {
};
};
export const getPreferences = (email: string, metadata: UserMetadataItem[]): UserPreferences => {
const preferences = getDefaultPreferences({ email });
export const getPreferences = (metadata: UserMetadataItem[]): UserPreferences => {
const preferences = getDefaultPreferences();
const item = metadata.find(({ key }) => key === UserMetadataKey.PREFERENCES);
const partial = item?.value || {};
for (const property of getKeysDeep(partial)) {
@@ -64,8 +56,8 @@ export const getPreferences = (email: string, metadata: UserMetadataItem[]): Use
return preferences;
};
export const getPreferencesPartial = (user: { email: string }, newPreferences: UserPreferences) => {
const defaultPreferences = getDefaultPreferences(user);
export const getPreferencesPartial = (newPreferences: UserPreferences) => {
const defaultPreferences = getDefaultPreferences();
const partial: DeepPartial<UserPreferences> = {};
for (const property of getKeysDeep(defaultPreferences)) {
const newValue = _.get(newPreferences, property);