Merge branch 'lighter_buckets_web' into lighter_buckets_server
This commit is contained in:
@@ -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:
|
||||
|
||||
83
server/src/utils/database.spec.ts
Normal file
83
server/src/utils/database.spec.ts
Normal 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',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
}),
|
||||
}),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user