From 74c921148bd2db5cd74065fa0ec18228346fa1d7 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Fri, 19 Apr 2024 11:19:23 -0400 Subject: [PATCH 01/27] refactor(server): cookies (#8920) --- e2e/src/api/specs/auth.e2e-spec.ts | 26 +++++++- server/src/constants.ts | 7 +- server/src/controllers/auth.controller.ts | 29 ++++++--- server/src/controllers/oauth.controller.ts | 15 ++++- .../src/controllers/shared-link.controller.ts | 24 ++++--- server/src/dtos/auth.dto.ts | 21 +++++- server/src/middleware/auth.guard.ts | 4 +- server/src/services/auth.service.spec.ts | 14 ---- server/src/services/auth.service.ts | 64 ++++--------------- server/src/utils/misc.ts | 15 ++--- server/src/utils/response.ts | 36 +++++++++++ server/test/fixtures/auth.stub.ts | 58 ++++------------- 12 files changed, 158 insertions(+), 155 deletions(-) create mode 100644 server/src/utils/response.ts diff --git a/e2e/src/api/specs/auth.e2e-spec.ts b/e2e/src/api/specs/auth.e2e-spec.ts index 4a6e1a773a..9174128bb8 100644 --- a/e2e/src/api/specs/auth.e2e-spec.ts +++ b/e2e/src/api/specs/auth.e2e-spec.ts @@ -112,9 +112,29 @@ describe('/auth/*', () => { const cookies = headers['set-cookie']; expect(cookies).toHaveLength(3); - expect(cookies[0]).toEqual(`immich_access_token=${token}; HttpOnly; Path=/; Max-Age=34560000; SameSite=Lax;`); - expect(cookies[1]).toEqual('immich_auth_type=password; HttpOnly; Path=/; Max-Age=34560000; SameSite=Lax;'); - expect(cookies[2]).toEqual('immich_is_authenticated=true; Path=/; Max-Age=34560000; SameSite=Lax;'); + expect(cookies[0].split(';').map((item) => item.trim())).toEqual([ + `immich_access_token=${token}`, + 'Max-Age=34560000', + 'Path=/', + expect.stringContaining('Expires='), + 'HttpOnly', + 'SameSite=Lax', + ]); + expect(cookies[1].split(';').map((item) => item.trim())).toEqual([ + 'immich_auth_type=password', + 'Max-Age=34560000', + 'Path=/', + expect.stringContaining('Expires='), + 'HttpOnly', + 'SameSite=Lax', + ]); + expect(cookies[2].split(';').map((item) => item.trim())).toEqual([ + 'immich_is_authenticated=true', + 'Max-Age=34560000', + 'Path=/', + expect.stringContaining('Expires='), + 'SameSite=Lax', + ]); }); }); diff --git a/server/src/constants.ts b/server/src/constants.ts index 1289701dd8..d9d4232396 100644 --- a/server/src/constants.ts +++ b/server/src/constants.ts @@ -26,12 +26,7 @@ export const geodataCities500Path = join(GEODATA_ROOT_PATH, citiesFile); export const MOBILE_REDIRECT = 'app.immich:/'; export const LOGIN_URL = '/auth/login?autoLaunch=0'; -export const IMMICH_ACCESS_COOKIE = 'immich_access_token'; -export const IMMICH_IS_AUTHENTICATED = 'immich_is_authenticated'; -export const IMMICH_AUTH_TYPE_COOKIE = 'immich_auth_type'; -export const IMMICH_API_KEY_NAME = 'api_key'; -export const IMMICH_API_KEY_HEADER = 'x-api-key'; -export const IMMICH_SHARED_LINK_ACCESS_COOKIE = 'immich_shared_link_token'; + export enum AuthType { PASSWORD = 'password', OAUTH = 'oauth', diff --git a/server/src/controllers/auth.controller.ts b/server/src/controllers/auth.controller.ts index f4e7666207..a4c7494f2b 100644 --- a/server/src/controllers/auth.controller.ts +++ b/server/src/controllers/auth.controller.ts @@ -1,10 +1,11 @@ import { Body, Controller, HttpCode, HttpStatus, Post, Req, Res } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { Request, Response } from 'express'; -import { IMMICH_ACCESS_COOKIE, IMMICH_AUTH_TYPE_COOKIE, IMMICH_IS_AUTHENTICATED } from 'src/constants'; +import { AuthType } from 'src/constants'; import { AuthDto, ChangePasswordDto, + ImmichCookie, LoginCredentialDto, LoginResponseDto, LogoutResponseDto, @@ -14,6 +15,7 @@ import { import { UserResponseDto, mapUser } from 'src/dtos/user.dto'; import { Auth, Authenticated, GetLoginDetails, PublicRoute } from 'src/middleware/auth.guard'; import { AuthService, LoginDetails } from 'src/services/auth.service'; +import { respondWithCookie, respondWithoutCookie } from 'src/utils/response'; @ApiTags('Authentication') @Controller('auth') @@ -28,9 +30,15 @@ export class AuthController { @Res({ passthrough: true }) res: Response, @GetLoginDetails() loginDetails: LoginDetails, ): Promise { - const { response, cookie } = await this.service.login(loginCredential, loginDetails); - res.header('Set-Cookie', cookie); - return response; + const body = await this.service.login(loginCredential, loginDetails); + return respondWithCookie(res, body, { + isSecure: loginDetails.isSecure, + values: [ + { key: ImmichCookie.ACCESS_TOKEN, value: body.accessToken }, + { key: ImmichCookie.AUTH_TYPE, value: AuthType.PASSWORD }, + { key: ImmichCookie.IS_AUTHENTICATED, value: 'true' }, + ], + }); } @PublicRoute() @@ -53,15 +61,18 @@ export class AuthController { @Post('logout') @HttpCode(HttpStatus.OK) - logout( + async logout( @Req() request: Request, @Res({ passthrough: true }) res: Response, @Auth() auth: AuthDto, ): Promise { - res.clearCookie(IMMICH_ACCESS_COOKIE); - res.clearCookie(IMMICH_AUTH_TYPE_COOKIE); - res.clearCookie(IMMICH_IS_AUTHENTICATED); + const authType = (request.cookies || {})[ImmichCookie.AUTH_TYPE]; - return this.service.logout(auth, (request.cookies || {})[IMMICH_AUTH_TYPE_COOKIE]); + const body = await this.service.logout(auth, authType); + return respondWithoutCookie(res, body, [ + ImmichCookie.ACCESS_TOKEN, + ImmichCookie.AUTH_TYPE, + ImmichCookie.IS_AUTHENTICATED, + ]); } } diff --git a/server/src/controllers/oauth.controller.ts b/server/src/controllers/oauth.controller.ts index debbd4e676..d87fb11d88 100644 --- a/server/src/controllers/oauth.controller.ts +++ b/server/src/controllers/oauth.controller.ts @@ -1,8 +1,10 @@ import { Body, Controller, Get, HttpStatus, Post, Redirect, Req, Res } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { Request, Response } from 'express'; +import { AuthType } from 'src/constants'; import { AuthDto, + ImmichCookie, LoginResponseDto, OAuthAuthorizeResponseDto, OAuthCallbackDto, @@ -11,6 +13,7 @@ import { import { UserResponseDto } from 'src/dtos/user.dto'; import { Auth, Authenticated, GetLoginDetails, PublicRoute } from 'src/middleware/auth.guard'; import { AuthService, LoginDetails } from 'src/services/auth.service'; +import { respondWithCookie } from 'src/utils/response'; @ApiTags('OAuth') @Controller('oauth') @@ -41,9 +44,15 @@ export class OAuthController { @Body() dto: OAuthCallbackDto, @GetLoginDetails() loginDetails: LoginDetails, ): Promise { - const { response, cookie } = await this.service.callback(dto, loginDetails); - res.header('Set-Cookie', cookie); - return response; + const body = await this.service.callback(dto, loginDetails); + return respondWithCookie(res, body, { + isSecure: loginDetails.isSecure, + values: [ + { key: ImmichCookie.ACCESS_TOKEN, value: body.accessToken }, + { key: ImmichCookie.AUTH_TYPE, value: AuthType.OAUTH }, + { key: ImmichCookie.IS_AUTHENTICATED, value: 'true' }, + ], + }); } @Post('link') diff --git a/server/src/controllers/shared-link.controller.ts b/server/src/controllers/shared-link.controller.ts index a7a8e3a1c6..58f2939b93 100644 --- a/server/src/controllers/shared-link.controller.ts +++ b/server/src/controllers/shared-link.controller.ts @@ -1,18 +1,19 @@ import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query, Req, Res } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { Request, Response } from 'express'; -import { IMMICH_SHARED_LINK_ACCESS_COOKIE } from 'src/constants'; import { AssetIdsResponseDto } from 'src/dtos/asset-ids.response.dto'; import { AssetIdsDto } from 'src/dtos/asset.dto'; -import { AuthDto } from 'src/dtos/auth.dto'; +import { AuthDto, ImmichCookie } from 'src/dtos/auth.dto'; import { SharedLinkCreateDto, SharedLinkEditDto, SharedLinkPasswordDto, SharedLinkResponseDto, } from 'src/dtos/shared-link.dto'; -import { Auth, Authenticated, SharedLinkRoute } from 'src/middleware/auth.guard'; +import { Auth, Authenticated, GetLoginDetails, SharedLinkRoute } from 'src/middleware/auth.guard'; +import { LoginDetails } from 'src/services/auth.service'; import { SharedLinkService } from 'src/services/shared-link.service'; +import { respondWithCookie } from 'src/utils/response'; import { UUIDParamDto } from 'src/validation'; @ApiTags('Shared Link') @@ -33,20 +34,17 @@ export class SharedLinkController { @Query() dto: SharedLinkPasswordDto, @Req() request: Request, @Res({ passthrough: true }) res: Response, + @GetLoginDetails() loginDetails: LoginDetails, ): Promise { - const sharedLinkToken = request.cookies?.[IMMICH_SHARED_LINK_ACCESS_COOKIE]; + const sharedLinkToken = request.cookies?.[ImmichCookie.SHARED_LINK_TOKEN]; if (sharedLinkToken) { dto.token = sharedLinkToken; } - const response = await this.service.getMine(auth, dto); - if (response.token) { - res.cookie(IMMICH_SHARED_LINK_ACCESS_COOKIE, response.token, { - expires: new Date(Date.now() + 1000 * 60 * 60 * 24), - httpOnly: true, - sameSite: 'lax', - }); - } - return response; + const body = await this.service.getMine(auth, dto); + return respondWithCookie(res, body, { + isSecure: loginDetails.isSecure, + values: body.token ? [{ key: ImmichCookie.SHARED_LINK_TOKEN, value: body.token }] : [], + }); } @Get(':id') diff --git a/server/src/dtos/auth.dto.ts b/server/src/dtos/auth.dto.ts index 4651c010b9..5c1e01b818 100644 --- a/server/src/dtos/auth.dto.ts +++ b/server/src/dtos/auth.dto.ts @@ -6,6 +6,25 @@ import { SessionEntity } from 'src/entities/session.entity'; import { SharedLinkEntity } from 'src/entities/shared-link.entity'; import { UserEntity } from 'src/entities/user.entity'; +export enum ImmichCookie { + ACCESS_TOKEN = 'immich_access_token', + AUTH_TYPE = 'immich_auth_type', + IS_AUTHENTICATED = 'immich_is_authenticated', + SHARED_LINK_TOKEN = 'immich_shared_link_token', +} + +export enum ImmichHeader { + API_KEY = 'x-api-key', + USER_TOKEN = 'x-immich-user-token', + SESSION_TOKEN = 'x-immich-session-token', + SHARED_LINK_TOKEN = 'x-immich-share-key', +} + +export type CookieResponse = { + isSecure: boolean; + values: Array<{ key: ImmichCookie; value: string }>; +}; + export class AuthDto { user!: UserEntity; @@ -39,7 +58,7 @@ export class LoginResponseDto { export function mapLoginResponse(entity: UserEntity, accessToken: string): LoginResponseDto { return { - accessToken: accessToken, + accessToken, userId: entity.id, userEmail: entity.email, name: entity.name, diff --git a/server/src/middleware/auth.guard.ts b/server/src/middleware/auth.guard.ts index 8b3abe6693..1253e99bbb 100644 --- a/server/src/middleware/auth.guard.ts +++ b/server/src/middleware/auth.guard.ts @@ -10,7 +10,6 @@ import { import { Reflector } from '@nestjs/core'; import { ApiBearerAuth, ApiCookieAuth, ApiOkResponse, ApiQuery, ApiSecurity } from '@nestjs/swagger'; import { Request } from 'express'; -import { IMMICH_API_KEY_NAME } from 'src/constants'; import { AuthDto } from 'src/dtos/auth.dto'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { AuthService, LoginDetails } from 'src/services/auth.service'; @@ -21,6 +20,7 @@ export enum Metadata { ADMIN_ROUTE = 'admin_route', SHARED_ROUTE = 'shared_route', PUBLIC_SECURITY = 'public_security', + API_KEY_SECURITY = 'api_key', } export interface AuthenticatedOptions { @@ -32,7 +32,7 @@ export const Authenticated = (options: AuthenticatedOptions = {}) => { const decorators: MethodDecorator[] = [ ApiBearerAuth(), ApiCookieAuth(), - ApiSecurity(IMMICH_API_KEY_NAME), + ApiSecurity(Metadata.API_KEY_SECURITY), SetMetadata(Metadata.AUTH_ROUTE, true), ]; diff --git a/server/src/services/auth.service.spec.ts b/server/src/services/auth.service.spec.ts index 9d83d5261f..cbee9faddf 100644 --- a/server/src/services/auth.service.spec.ts +++ b/server/src/services/auth.service.spec.ts @@ -143,20 +143,6 @@ describe('AuthService', () => { await expect(sut.login(fixtures.login, loginDetails)).resolves.toEqual(loginResponseStub.user1password); expect(userMock.getByEmail).toHaveBeenCalledTimes(1); }); - - it('should generate the cookie headers (insecure)', async () => { - userMock.getByEmail.mockResolvedValue(userStub.user1); - sessionMock.create.mockResolvedValue(sessionStub.valid); - await expect( - sut.login(fixtures.login, { - clientIp: '127.0.0.1', - isSecure: false, - deviceOS: '', - deviceType: '', - }), - ).resolves.toEqual(loginResponseStub.user1insecure); - expect(userMock.getByEmail).toHaveBeenCalledTimes(1); - }); }); describe('changePassword', () => { diff --git a/server/src/services/auth.service.ts b/server/src/services/auth.service.ts index 7e81d15ce5..bea7366555 100644 --- a/server/src/services/auth.service.ts +++ b/server/src/services/auth.service.ts @@ -10,23 +10,16 @@ import cookieParser from 'cookie'; import { DateTime } from 'luxon'; import { IncomingHttpHeaders } from 'node:http'; import { ClientMetadata, Issuer, UserinfoResponse, custom, generators } from 'openid-client'; -import { - AuthType, - IMMICH_ACCESS_COOKIE, - IMMICH_API_KEY_HEADER, - IMMICH_AUTH_TYPE_COOKIE, - IMMICH_IS_AUTHENTICATED, - LOGIN_URL, - MOBILE_REDIRECT, -} from 'src/constants'; +import { AuthType, LOGIN_URL, MOBILE_REDIRECT } from 'src/constants'; import { AccessCore } from 'src/cores/access.core'; import { SystemConfigCore } from 'src/cores/system-config.core'; import { UserCore } from 'src/cores/user.core'; import { AuthDto, ChangePasswordDto, + ImmichCookie, + ImmichHeader, LoginCredentialDto, - LoginResponseDto, LogoutResponseDto, OAuthAuthorizeResponseDto, OAuthCallbackDto, @@ -55,11 +48,6 @@ export interface LoginDetails { deviceOS: string; } -interface LoginResponse { - response: LoginResponseDto; - cookie: string[]; -} - interface OAuthProfile extends UserinfoResponse { email: string; } @@ -95,7 +83,7 @@ export class AuthService { custom.setHttpOptionsDefaults({ timeout: 30_000 }); } - async login(dto: LoginCredentialDto, details: LoginDetails): Promise { + async login(dto: LoginCredentialDto, details: LoginDetails) { const config = await this.configCore.getConfig(); if (!config.passwordLogin.enabled) { throw new UnauthorizedException('Password login has been disabled'); @@ -114,7 +102,7 @@ export class AuthService { throw new UnauthorizedException('Incorrect email or password'); } - return this.createLoginResponse(user, AuthType.PASSWORD, details); + return this.createLoginResponse(user, details); } async logout(auth: AuthDto, authType: AuthType): Promise { @@ -161,13 +149,13 @@ export class AuthService { } async validate(headers: IncomingHttpHeaders, params: Record): Promise { - const shareKey = (headers['x-immich-share-key'] || params.key) as string; - const session = (headers['x-immich-user-token'] || - headers['x-immich-session-token'] || + const shareKey = (headers[ImmichHeader.SHARED_LINK_TOKEN] || params.key) as string; + const session = (headers[ImmichHeader.USER_TOKEN] || + headers[ImmichHeader.SESSION_TOKEN] || params.sessionKey || this.getBearerToken(headers) || this.getCookieToken(headers)) as string; - const apiKey = (headers[IMMICH_API_KEY_HEADER] || params.apiKey) as string; + const apiKey = (headers[ImmichHeader.API_KEY] || params.apiKey) as string; if (shareKey) { return this.validateSharedLink(shareKey); @@ -204,10 +192,7 @@ export class AuthService { return { url }; } - async callback( - dto: OAuthCallbackDto, - loginDetails: LoginDetails, - ): Promise<{ response: LoginResponseDto; cookie: string[] }> { + async callback(dto: OAuthCallbackDto, loginDetails: LoginDetails) { const config = await this.configCore.getConfig(); const profile = await this.getOAuthProfile(config, dto.url); this.logger.debug(`Logging in with OAuth: ${JSON.stringify(profile)}`); @@ -256,7 +241,7 @@ export class AuthService { }); } - return this.createLoginResponse(user, AuthType.OAUTH, loginDetails); + return this.createLoginResponse(user, loginDetails); } async link(auth: AuthDto, dto: OAuthCallbackDto): Promise { @@ -353,7 +338,7 @@ export class AuthService { private getCookieToken(headers: IncomingHttpHeaders): string | null { const cookies = cookieParser.parse(headers.cookie || ''); - return cookies[IMMICH_ACCESS_COOKIE] || null; + return cookies[ImmichCookie.ACCESS_TOKEN] || null; } async validateSharedLink(key: string | string[]): Promise { @@ -405,7 +390,7 @@ export class AuthService { throw new UnauthorizedException('Invalid user token'); } - private async createLoginResponse(user: UserEntity, authType: AuthType, loginDetails: LoginDetails) { + private async createLoginResponse(user: UserEntity, loginDetails: LoginDetails) { const key = this.cryptoRepository.newPassword(32); const token = this.cryptoRepository.hashSha256(key); @@ -416,28 +401,7 @@ export class AuthService { deviceType: loginDetails.deviceType, }); - const response = mapLoginResponse(user, key); - const cookie = this.getCookies(response, authType, loginDetails); - return { response, cookie }; - } - - private getCookies(loginResponse: LoginResponseDto, authType: AuthType, { isSecure }: LoginDetails) { - const maxAge = 400 * 24 * 3600; // 400 days - - let authTypeCookie = ''; - let accessTokenCookie = ''; - let isAuthenticatedCookie = ''; - - if (isSecure) { - accessTokenCookie = `${IMMICH_ACCESS_COOKIE}=${loginResponse.accessToken}; HttpOnly; Secure; Path=/; Max-Age=${maxAge}; SameSite=Lax;`; - authTypeCookie = `${IMMICH_AUTH_TYPE_COOKIE}=${authType}; HttpOnly; Secure; Path=/; Max-Age=${maxAge}; SameSite=Lax;`; - isAuthenticatedCookie = `${IMMICH_IS_AUTHENTICATED}=true; Secure; Path=/; Max-Age=${maxAge}; SameSite=Lax;`; - } else { - accessTokenCookie = `${IMMICH_ACCESS_COOKIE}=${loginResponse.accessToken}; HttpOnly; Path=/; Max-Age=${maxAge}; SameSite=Lax;`; - authTypeCookie = `${IMMICH_AUTH_TYPE_COOKIE}=${authType}; HttpOnly; Path=/; Max-Age=${maxAge}; SameSite=Lax;`; - isAuthenticatedCookie = `${IMMICH_IS_AUTHENTICATED}=true; Path=/; Max-Age=${maxAge}; SameSite=Lax;`; - } - return [accessTokenCookie, authTypeCookie, isAuthenticatedCookie]; + return mapLoginResponse(user, key); } private getClaim(profile: OAuthProfile, options: ClaimOptions): T { diff --git a/server/src/utils/misc.ts b/server/src/utils/misc.ts index c11c936a1a..8262b6024b 100644 --- a/server/src/utils/misc.ts +++ b/server/src/utils/misc.ts @@ -10,13 +10,8 @@ import { SchemaObject } from '@nestjs/swagger/dist/interfaces/open-api-spec.inte import _ from 'lodash'; import { writeFileSync } from 'node:fs'; import path from 'node:path'; -import { - CLIP_MODEL_INFO, - IMMICH_ACCESS_COOKIE, - IMMICH_API_KEY_HEADER, - IMMICH_API_KEY_NAME, - serverVersion, -} from 'src/constants'; +import { CLIP_MODEL_INFO, serverVersion } from 'src/constants'; +import { ImmichCookie, ImmichHeader } from 'src/dtos/auth.dto'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { Metadata } from 'src/middleware/auth.guard'; @@ -143,14 +138,14 @@ export const useSwagger = (app: INestApplication, isDevelopment: boolean) => { scheme: 'Bearer', in: 'header', }) - .addCookieAuth(IMMICH_ACCESS_COOKIE) + .addCookieAuth(ImmichCookie.ACCESS_TOKEN) .addApiKey( { type: 'apiKey', in: 'header', - name: IMMICH_API_KEY_HEADER, + name: ImmichHeader.API_KEY, }, - IMMICH_API_KEY_NAME, + Metadata.API_KEY_SECURITY, ) .addServer('/api') .build(); diff --git a/server/src/utils/response.ts b/server/src/utils/response.ts new file mode 100644 index 0000000000..f318ca3300 --- /dev/null +++ b/server/src/utils/response.ts @@ -0,0 +1,36 @@ +import { CookieOptions, Response } from 'express'; +import { Duration } from 'luxon'; +import { CookieResponse, ImmichCookie } from 'src/dtos/auth.dto'; + +export const respondWithCookie = (res: Response, body: T, { isSecure, values }: CookieResponse) => { + const defaults: CookieOptions = { + path: '/', + sameSite: 'lax', + httpOnly: true, + secure: isSecure, + maxAge: Duration.fromObject({ days: 400 }).toMillis(), + }; + + const cookieOptions: Record = { + [ImmichCookie.AUTH_TYPE]: defaults, + [ImmichCookie.ACCESS_TOKEN]: defaults, + // no httpOnly so that the client can know the auth state + [ImmichCookie.IS_AUTHENTICATED]: { ...defaults, httpOnly: false }, + [ImmichCookie.SHARED_LINK_TOKEN]: { ...defaults, maxAge: Duration.fromObject({ days: 1 }).toMillis() }, + }; + + for (const { key, value } of values) { + const options = cookieOptions[key]; + res.cookie(key, value, options); + } + + return body; +}; + +export const respondWithoutCookie = (res: Response, body: T, cookies: ImmichCookie[]) => { + for (const cookie of cookies) { + res.clearCookie(cookie); + } + + return body; +}; diff --git a/server/test/fixtures/auth.stub.ts b/server/test/fixtures/auth.stub.ts index a4753a02e7..96a0bc0141 100644 --- a/server/test/fixtures/auth.stub.ts +++ b/server/test/fixtures/auth.stub.ts @@ -129,51 +129,21 @@ export const loginResponseStub = { }, }, user1oauth: { - response: { - accessToken: 'cmFuZG9tLWJ5dGVz', - userId: 'user-id', - userEmail: 'immich@test.com', - name: 'immich_name', - profileImagePath: '', - isAdmin: false, - shouldChangePassword: false, - }, - cookie: [ - 'immich_access_token=cmFuZG9tLWJ5dGVz; HttpOnly; Secure; Path=/; Max-Age=34560000; SameSite=Lax;', - 'immich_auth_type=oauth; HttpOnly; Secure; Path=/; Max-Age=34560000; SameSite=Lax;', - 'immich_is_authenticated=true; Secure; Path=/; Max-Age=34560000; SameSite=Lax;', - ], + accessToken: 'cmFuZG9tLWJ5dGVz', + userId: 'user-id', + userEmail: 'immich@test.com', + name: 'immich_name', + profileImagePath: '', + isAdmin: false, + shouldChangePassword: false, }, user1password: { - response: { - accessToken: 'cmFuZG9tLWJ5dGVz', - userId: 'user-id', - userEmail: 'immich@test.com', - name: 'immich_name', - profileImagePath: '', - isAdmin: false, - shouldChangePassword: false, - }, - cookie: [ - 'immich_access_token=cmFuZG9tLWJ5dGVz; HttpOnly; Secure; Path=/; Max-Age=34560000; SameSite=Lax;', - 'immich_auth_type=password; HttpOnly; Secure; Path=/; Max-Age=34560000; SameSite=Lax;', - 'immich_is_authenticated=true; Secure; Path=/; Max-Age=34560000; SameSite=Lax;', - ], - }, - user1insecure: { - response: { - accessToken: 'cmFuZG9tLWJ5dGVz', - userId: 'user-id', - userEmail: 'immich@test.com', - name: 'immich_name', - profileImagePath: '', - isAdmin: false, - shouldChangePassword: false, - }, - cookie: [ - 'immich_access_token=cmFuZG9tLWJ5dGVz; HttpOnly; Path=/; Max-Age=34560000; SameSite=Lax;', - 'immich_auth_type=password; HttpOnly; Path=/; Max-Age=34560000; SameSite=Lax;', - 'immich_is_authenticated=true; Path=/; Max-Age=34560000; SameSite=Lax;', - ], + accessToken: 'cmFuZG9tLWJ5dGVz', + userId: 'user-id', + userEmail: 'immich@test.com', + name: 'immich_name', + profileImagePath: '', + isAdmin: false, + shouldChangePassword: false, }, }; From 431ffebddda0a08d7ad327a37f392e01c2400c52 Mon Sep 17 00:00:00 2001 From: Mert <101130780+mertalev@users.noreply.github.com> Date: Fri, 19 Apr 2024 11:50:13 -0400 Subject: [PATCH 02/27] feat(server): use embedded preview from raw images (#8773) * extract embedded * update api * add tests * move temp file logic outside of media repo * formatting * revert `toSorted` * disable by default * clarify setting description * wording * wording * update docs * check extracted image dimensions * test that it unlinks * formatting --------- Co-authored-by: Alex Tran --- docs/docs/install/config-file.md | 3 +- mobile/openapi/doc/SystemConfigImageDto.md | 1 + .../lib/model/system_config_image_dto.dart | 10 +- .../test/system_config_image_dto_test.dart | 5 + open-api/immich-openapi-specs.json | 4 + open-api/typescript-sdk/src/fetch-client.ts | 1 + server/src/cores/storage.core.ts | 5 + server/src/cores/system-config.core.ts | 1 + server/src/dtos/system-config.dto.ts | 3 + server/src/entities/system-config.entity.ts | 2 + server/src/interfaces/media.interface.ts | 7 ++ server/src/repositories/media.repository.ts | 29 +++++- server/src/services/media.service.spec.ts | 97 ++++++++++++++++++- server/src/services/media.service.ts | 29 +++++- .../services/system-config.service.spec.ts | 1 + server/src/utils/mime-types.spec.ts | 30 +++--- server/src/utils/mime-types.ts | 39 +++++--- server/test/fixtures/asset.stub.ts | 41 ++++++++ .../repositories/media.repository.mock.ts | 2 + .../settings/image/image-settings.svelte | 10 ++ 20 files changed, 274 insertions(+), 46 deletions(-) diff --git a/docs/docs/install/config-file.md b/docs/docs/install/config-file.md index a890d674bc..256f3619f1 100644 --- a/docs/docs/install/config-file.md +++ b/docs/docs/install/config-file.md @@ -120,7 +120,8 @@ The default configuration looks like this: "previewFormat": "jpeg", "previewSize": 1440, "quality": 80, - "colorspace": "p3" + "colorspace": "p3", + "extractEmbedded": false }, "newVersionCheck": { "enabled": true diff --git a/mobile/openapi/doc/SystemConfigImageDto.md b/mobile/openapi/doc/SystemConfigImageDto.md index 1b9bbe726d..81e88045d5 100644 --- a/mobile/openapi/doc/SystemConfigImageDto.md +++ b/mobile/openapi/doc/SystemConfigImageDto.md @@ -9,6 +9,7 @@ import 'package:openapi/api.dart'; Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- **colorspace** | [**Colorspace**](Colorspace.md) | | +**extractEmbedded** | **bool** | | **previewFormat** | [**ImageFormat**](ImageFormat.md) | | **previewSize** | **int** | | **quality** | **int** | | diff --git a/mobile/openapi/lib/model/system_config_image_dto.dart b/mobile/openapi/lib/model/system_config_image_dto.dart index 1c830861af..7072e11270 100644 --- a/mobile/openapi/lib/model/system_config_image_dto.dart +++ b/mobile/openapi/lib/model/system_config_image_dto.dart @@ -14,6 +14,7 @@ class SystemConfigImageDto { /// Returns a new [SystemConfigImageDto] instance. SystemConfigImageDto({ required this.colorspace, + required this.extractEmbedded, required this.previewFormat, required this.previewSize, required this.quality, @@ -23,6 +24,8 @@ class SystemConfigImageDto { Colorspace colorspace; + bool extractEmbedded; + ImageFormat previewFormat; int previewSize; @@ -36,6 +39,7 @@ class SystemConfigImageDto { @override bool operator ==(Object other) => identical(this, other) || other is SystemConfigImageDto && other.colorspace == colorspace && + other.extractEmbedded == extractEmbedded && other.previewFormat == previewFormat && other.previewSize == previewSize && other.quality == quality && @@ -46,6 +50,7 @@ class SystemConfigImageDto { int get hashCode => // ignore: unnecessary_parenthesis (colorspace.hashCode) + + (extractEmbedded.hashCode) + (previewFormat.hashCode) + (previewSize.hashCode) + (quality.hashCode) + @@ -53,11 +58,12 @@ class SystemConfigImageDto { (thumbnailSize.hashCode); @override - String toString() => 'SystemConfigImageDto[colorspace=$colorspace, previewFormat=$previewFormat, previewSize=$previewSize, quality=$quality, thumbnailFormat=$thumbnailFormat, thumbnailSize=$thumbnailSize]'; + String toString() => 'SystemConfigImageDto[colorspace=$colorspace, extractEmbedded=$extractEmbedded, previewFormat=$previewFormat, previewSize=$previewSize, quality=$quality, thumbnailFormat=$thumbnailFormat, thumbnailSize=$thumbnailSize]'; Map toJson() { final json = {}; json[r'colorspace'] = this.colorspace; + json[r'extractEmbedded'] = this.extractEmbedded; json[r'previewFormat'] = this.previewFormat; json[r'previewSize'] = this.previewSize; json[r'quality'] = this.quality; @@ -75,6 +81,7 @@ class SystemConfigImageDto { return SystemConfigImageDto( colorspace: Colorspace.fromJson(json[r'colorspace'])!, + extractEmbedded: mapValueOfType(json, r'extractEmbedded')!, previewFormat: ImageFormat.fromJson(json[r'previewFormat'])!, previewSize: mapValueOfType(json, r'previewSize')!, quality: mapValueOfType(json, r'quality')!, @@ -128,6 +135,7 @@ class SystemConfigImageDto { /// The list of required keys that must be present in a JSON. static const requiredKeys = { 'colorspace', + 'extractEmbedded', 'previewFormat', 'previewSize', 'quality', diff --git a/mobile/openapi/test/system_config_image_dto_test.dart b/mobile/openapi/test/system_config_image_dto_test.dart index aef907bbe6..b46340455b 100644 --- a/mobile/openapi/test/system_config_image_dto_test.dart +++ b/mobile/openapi/test/system_config_image_dto_test.dart @@ -21,6 +21,11 @@ void main() { // TODO }); + // bool extractEmbedded + test('to test the property `extractEmbedded`', () async { + // TODO + }); + // ImageFormat previewFormat test('to test the property `previewFormat`', () async { // TODO diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index bfe3ec32c9..de3456e519 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -10531,6 +10531,9 @@ "colorspace": { "$ref": "#/components/schemas/Colorspace" }, + "extractEmbedded": { + "type": "boolean" + }, "previewFormat": { "$ref": "#/components/schemas/ImageFormat" }, @@ -10549,6 +10552,7 @@ }, "required": [ "colorspace", + "extractEmbedded", "previewFormat", "previewSize", "quality", diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 560295c94c..cfa60e9249 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -864,6 +864,7 @@ export type SystemConfigFFmpegDto = { }; export type SystemConfigImageDto = { colorspace: Colorspace; + extractEmbedded: boolean; previewFormat: ImageFormat; previewSize: number; quality: number; diff --git a/server/src/cores/storage.core.ts b/server/src/cores/storage.core.ts index f1c16e5698..4e5f4742a4 100644 --- a/server/src/cores/storage.core.ts +++ b/server/src/cores/storage.core.ts @@ -1,3 +1,4 @@ +import { randomUUID } from 'node:crypto'; import { dirname, join, resolve } from 'node:path'; import { APP_MEDIA_LOCATION } from 'src/constants'; import { SystemConfigCore } from 'src/cores/system-config.core'; @@ -308,4 +309,8 @@ export class StorageCore { static getNestedPath(folder: StorageFolder, ownerId: string, filename: string): string { return join(this.getNestedFolder(folder, ownerId, filename), filename); } + + static getTempPathInDir(dir: string): string { + return join(dir, `${randomUUID()}.tmp`); + } } diff --git a/server/src/cores/system-config.core.ts b/server/src/cores/system-config.core.ts index 9cbe3b8414..2520840173 100644 --- a/server/src/cores/system-config.core.ts +++ b/server/src/cores/system-config.core.ts @@ -120,6 +120,7 @@ export const defaults = Object.freeze({ previewSize: 1440, quality: 80, colorspace: Colorspace.P3, + extractEmbedded: false, }, newVersionCheck: { enabled: true, diff --git a/server/src/dtos/system-config.dto.ts b/server/src/dtos/system-config.dto.ts index 9f80e8d6a3..d23eef4994 100644 --- a/server/src/dtos/system-config.dto.ts +++ b/server/src/dtos/system-config.dto.ts @@ -417,6 +417,9 @@ class SystemConfigImageDto { @IsEnum(Colorspace) @ApiProperty({ enumName: 'Colorspace', enum: Colorspace }) colorspace!: Colorspace; + + @ValidateBoolean() + extractEmbedded!: boolean; } class SystemConfigTrashDto { diff --git a/server/src/entities/system-config.entity.ts b/server/src/entities/system-config.entity.ts index a8a550fd6d..7126297ce3 100644 --- a/server/src/entities/system-config.entity.ts +++ b/server/src/entities/system-config.entity.ts @@ -114,6 +114,7 @@ export const SystemConfigKey = { IMAGE_PREVIEW_SIZE: 'image.previewSize', IMAGE_QUALITY: 'image.quality', IMAGE_COLORSPACE: 'image.colorspace', + IMAGE_EXTRACT_EMBEDDED: 'image.extractEmbedded', TRASH_ENABLED: 'trash.enabled', TRASH_DAYS: 'trash.days', @@ -284,6 +285,7 @@ export interface SystemConfig { previewSize: number; quality: number; colorspace: Colorspace; + extractEmbedded: boolean; }; newVersionCheck: { enabled: boolean; diff --git a/server/src/interfaces/media.interface.ts b/server/src/interfaces/media.interface.ts index 5e51e94a52..a82b38b6de 100644 --- a/server/src/interfaces/media.interface.ts +++ b/server/src/interfaces/media.interface.ts @@ -34,6 +34,11 @@ export interface VideoFormat { bitrate: number; } +export interface ImageDimensions { + width: number; + height: number; +} + export interface VideoInfo { format: VideoFormat; videoStreams: VideoStreamInfo[]; @@ -70,9 +75,11 @@ export interface VideoCodecHWConfig extends VideoCodecSWConfig { export interface IMediaRepository { // image + extract(input: string, output: string): Promise; resize(input: string | Buffer, output: string, options: ResizeOptions): Promise; crop(input: string, options: CropOptions): Promise; generateThumbhash(imagePath: string): Promise; + getImageDimensions(input: string): Promise; // video probe(input: string): Promise; diff --git a/server/src/repositories/media.repository.ts b/server/src/repositories/media.repository.ts index 3936ad7e42..434fb585f8 100644 --- a/server/src/repositories/media.repository.ts +++ b/server/src/repositories/media.repository.ts @@ -1,4 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; +import { exiftool } from 'exiftool-vendored'; import ffmpeg, { FfprobeData } from 'fluent-ffmpeg'; import fs from 'node:fs/promises'; import { Writable } from 'node:stream'; @@ -9,6 +10,7 @@ import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { CropOptions, IMediaRepository, + ImageDimensions, ResizeOptions, TranscodeOptions, VideoInfo, @@ -26,6 +28,23 @@ export class MediaRepository implements IMediaRepository { constructor(@Inject(ILoggerRepository) private logger: ILoggerRepository) { this.logger.setContext(MediaRepository.name); } + + async extract(input: string, output: string): Promise { + try { + await exiftool.extractJpgFromRaw(input, output); + } catch (error: any) { + this.logger.debug('Could not extract JPEG from image, trying preview', error.message); + try { + await exiftool.extractPreview(input, output); + } catch (error: any) { + this.logger.debug('Could not extract preview from image', error.message); + return false; + } + } + + return true; + } + crop(input: string | Buffer, options: CropOptions): Promise { return sharp(input, { failOn: 'none' }) .pipelineColorspace('rgb16') @@ -133,6 +152,11 @@ export class MediaRepository implements IMediaRepository { return Buffer.from(thumbhash.rgbaToThumbHash(info.width, info.height, data)); } + async getImageDimensions(input: string): Promise { + const { width = 0, height = 0 } = await sharp(input).metadata(); + return { width, height }; + } + private configureFfmpegCall(input: string, output: string | Writable, options: TranscodeOptions) { return ffmpeg(input, { niceness: 10 }) .inputOptions(options.inputOptions) @@ -140,9 +164,4 @@ export class MediaRepository implements IMediaRepository { .output(output) .on('error', (error, stdout, stderr) => this.logger.error(stderr || error)); } - - private chainPath(existing: string, path: string) { - const separator = existing.endsWith(':') ? '' : ':'; - return `${existing}${separator}${path}`; - } } diff --git a/server/src/services/media.service.spec.ts b/server/src/services/media.service.spec.ts index c6301c7c33..6f02e72253 100644 --- a/server/src/services/media.service.spec.ts +++ b/server/src/services/media.service.spec.ts @@ -393,14 +393,12 @@ describe(MediaService.name, () => { }); it('should generate a P3 thumbnail for a wide gamut image', async () => { - assetMock.getByIds.mockResolvedValue([ - { ...assetStub.image, exifInfo: { profileDescription: 'Adobe RGB', bitsPerSample: 14 } as ExifEntity }, - ]); + assetMock.getByIds.mockResolvedValue([assetStub.imageDng]); await sut.handleGenerateThumbnail({ id: assetStub.image.id }); expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs/user-id/as/se'); expect(mediaMock.resize).toHaveBeenCalledWith( - '/original/path.jpg', + assetStub.imageDng.originalPath, 'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp', { format: ImageFormat.WEBP, @@ -415,7 +413,96 @@ describe(MediaService.name, () => { }); }); - describe('handleGenerateThumbhashThumbnail', () => { + it('should extract embedded image if enabled and available', async () => { + mediaMock.extract.mockResolvedValue(true); + mediaMock.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 }); + configMock.load.mockResolvedValue([{ key: SystemConfigKey.IMAGE_EXTRACT_EMBEDDED, value: true }]); + assetMock.getByIds.mockResolvedValue([assetStub.imageDng]); + + await sut.handleGenerateThumbnail({ id: assetStub.image.id }); + + const extractedPath = mediaMock.extract.mock.calls.at(-1)?.[1].toString(); + expect(mediaMock.resize.mock.calls).toEqual([ + [ + extractedPath, + 'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp', + { + format: ImageFormat.WEBP, + size: 250, + quality: 80, + colorspace: Colorspace.P3, + }, + ], + ]); + expect(extractedPath?.endsWith('.tmp')).toBe(true); + expect(storageMock.unlink).toHaveBeenCalledWith(extractedPath); + }); + + it('should resize original image if embedded image is too small', async () => { + mediaMock.extract.mockResolvedValue(true); + mediaMock.getImageDimensions.mockResolvedValue({ width: 1000, height: 1000 }); + configMock.load.mockResolvedValue([{ key: SystemConfigKey.IMAGE_EXTRACT_EMBEDDED, value: true }]); + assetMock.getByIds.mockResolvedValue([assetStub.imageDng]); + + await sut.handleGenerateThumbnail({ id: assetStub.image.id }); + + expect(mediaMock.resize.mock.calls).toEqual([ + [ + assetStub.imageDng.originalPath, + 'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp', + { + format: ImageFormat.WEBP, + size: 250, + quality: 80, + colorspace: Colorspace.P3, + }, + ], + ]); + const extractedPath = mediaMock.extract.mock.calls.at(-1)?.[1].toString(); + expect(extractedPath?.endsWith('.tmp')).toBe(true); + expect(storageMock.unlink).toHaveBeenCalledWith(extractedPath); + }); + + it('should resize original image if embedded image not found', async () => { + configMock.load.mockResolvedValue([{ key: SystemConfigKey.IMAGE_EXTRACT_EMBEDDED, value: true }]); + assetMock.getByIds.mockResolvedValue([assetStub.imageDng]); + + await sut.handleGenerateThumbnail({ id: assetStub.image.id }); + + expect(mediaMock.resize).toHaveBeenCalledWith( + assetStub.imageDng.originalPath, + 'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp', + { + format: ImageFormat.WEBP, + size: 250, + quality: 80, + colorspace: Colorspace.P3, + }, + ); + expect(mediaMock.getImageDimensions).not.toHaveBeenCalled(); + }); + + it('should resize original image if embedded image extraction is not enabled', async () => { + configMock.load.mockResolvedValue([{ key: SystemConfigKey.IMAGE_EXTRACT_EMBEDDED, value: false }]); + assetMock.getByIds.mockResolvedValue([assetStub.imageDng]); + + await sut.handleGenerateThumbnail({ id: assetStub.image.id }); + + expect(mediaMock.extract).not.toHaveBeenCalled(); + expect(mediaMock.resize).toHaveBeenCalledWith( + assetStub.imageDng.originalPath, + 'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp', + { + format: ImageFormat.WEBP, + size: 250, + quality: 80, + colorspace: Colorspace.P3, + }, + ); + expect(mediaMock.getImageDimensions).not.toHaveBeenCalled(); + }); + + describe('handleGenerateThumbhash', () => { it('should skip thumbhash generation if asset not found', async () => { assetMock.getByIds.mockResolvedValue([]); await sut.handleGenerateThumbhash({ id: assetStub.image.id }); diff --git a/server/src/services/media.service.ts b/server/src/services/media.service.ts index ca72b6cbdd..1795db86d0 100644 --- a/server/src/services/media.service.ts +++ b/server/src/services/media.service.ts @@ -1,4 +1,5 @@ import { Inject, Injectable, UnsupportedMediaTypeException } from '@nestjs/common'; +import { dirname } from 'node:path'; import { GeneratedImageType, StorageCore, StorageFolder } from 'src/cores/storage.core'; import { SystemConfigCore } from 'src/cores/system-config.core'; import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto'; @@ -42,6 +43,7 @@ import { VAAPIConfig, VP9Config, } from 'src/utils/media'; +import { mimeTypes } from 'src/utils/mime-types'; import { usePagination } from 'src/utils/pagination'; @Injectable() @@ -195,9 +197,21 @@ export class MediaService { switch (asset.type) { case AssetType.IMAGE: { - const colorspace = this.isSRGB(asset) ? Colorspace.SRGB : image.colorspace; - const imageOptions = { format, size, colorspace, quality: image.quality }; - await this.mediaRepository.resize(asset.originalPath, path, imageOptions); + const shouldExtract = image.extractEmbedded && mimeTypes.isRaw(asset.originalPath); + const extractedPath = StorageCore.getTempPathInDir(dirname(path)); + const didExtract = shouldExtract && (await this.mediaRepository.extract(asset.originalPath, extractedPath)); + + try { + const useExtracted = didExtract && (await this.shouldUseExtractedImage(extractedPath, image.previewSize)); + const colorspace = this.isSRGB(asset) ? Colorspace.SRGB : image.colorspace; + const imageOptions = { format, size, colorspace, quality: image.quality }; + + await this.mediaRepository.resize(useExtracted ? extractedPath : asset.originalPath, path, imageOptions); + } finally { + if (didExtract) { + await this.storageRepository.unlink(extractedPath); + } + } break; } @@ -527,7 +541,7 @@ export class MediaService { } } - parseBitrateToBps(bitrateString: string) { + private parseBitrateToBps(bitrateString: string) { const bitrateValue = Number.parseInt(bitrateString); if (Number.isNaN(bitrateValue)) { @@ -542,4 +556,11 @@ export class MediaService { return bitrateValue; } } + + private async shouldUseExtractedImage(extractedPath: string, targetSize: number) { + const { width, height } = await this.mediaRepository.getImageDimensions(extractedPath); + const extractedSize = Math.min(width, height); + + return extractedSize >= targetSize; + } } diff --git a/server/src/services/system-config.service.spec.ts b/server/src/services/system-config.service.spec.ts index 49bf8d6544..5f55effcac 100644 --- a/server/src/services/system-config.service.spec.ts +++ b/server/src/services/system-config.service.spec.ts @@ -129,6 +129,7 @@ const updatedConfig = Object.freeze({ previewSize: 1440, quality: 80, colorspace: Colorspace.P3, + extractEmbedded: false, }, newVersionCheck: { enabled: true, diff --git a/server/src/utils/mime-types.spec.ts b/server/src/utils/mime-types.spec.ts index bce75e1e10..cbbf751bc5 100644 --- a/server/src/utils/mime-types.spec.ts +++ b/server/src/utils/mime-types.spec.ts @@ -106,12 +106,6 @@ describe('mimeTypes', () => { expect(values).toEqual(values.map((mimeType) => mimeType.toLowerCase())); }); - it('should be a sorted list', () => { - const keys = Object.keys(mimeTypes.profile); - // TODO: use toSorted in NodeJS 20. - expect(keys).toEqual([...keys].sort()); - }); - for (const [extension, v] of Object.entries(mimeTypes.profile)) { it(`should lookup ${extension}`, () => { expect(mimeTypes.lookup(`test.${extension}`)).toEqual(v[0]); @@ -128,12 +122,6 @@ describe('mimeTypes', () => { expect(values).toEqual(values.map((mimeType) => mimeType.toLowerCase())); }); - it('should be a sorted list', () => { - const keys = Object.keys(mimeTypes.image); - // TODO: use toSorted in NodeJS 20. - expect(keys).toEqual([...keys].sort()); - }); - it('should contain only image mime types', () => { const values = Object.values(mimeTypes.image).flat(); expect(values).toEqual(values.filter((mimeType) => mimeType.startsWith('image/'))); @@ -157,7 +145,6 @@ describe('mimeTypes', () => { it('should be a sorted list', () => { const keys = Object.keys(mimeTypes.video); - // TODO: use toSorted in NodeJS 20. expect(keys).toEqual([...keys].sort()); }); @@ -184,7 +171,6 @@ describe('mimeTypes', () => { it('should be a sorted list', () => { const keys = Object.keys(mimeTypes.sidecar); - // TODO: use toSorted in NodeJS 20. expect(keys).toEqual([...keys].sort()); }); @@ -198,4 +184,20 @@ describe('mimeTypes', () => { }); } }); + + describe('raw', () => { + it('should contain only lowercase mime types', () => { + const keys = Object.keys(mimeTypes.raw); + expect(keys).toEqual(keys.map((mimeType) => mimeType.toLowerCase())); + + const values = Object.values(mimeTypes.raw).flat(); + expect(values).toEqual(values.map((mimeType) => mimeType.toLowerCase())); + }); + + for (const [extension, v] of Object.entries(mimeTypes.video)) { + it(`should lookup ${extension}`, () => { + expect(mimeTypes.lookup(`test.${extension}`)).toEqual(v[0]); + }); + } + }); }); diff --git a/server/src/utils/mime-types.ts b/server/src/utils/mime-types.ts index a888e4f423..495efc9ebc 100644 --- a/server/src/utils/mime-types.ts +++ b/server/src/utils/mime-types.ts @@ -1,12 +1,10 @@ import { extname } from 'node:path'; import { AssetType } from 'src/entities/asset.entity'; -const image: Record = { +const raw: Record = { '.3fr': ['image/3fr', 'image/x-hasselblad-3fr'], '.ari': ['image/ari', 'image/x-arriflex-ari'], '.arw': ['image/arw', 'image/x-sony-arw'], - '.avif': ['image/avif'], - '.bmp': ['image/bmp'], '.cap': ['image/cap', 'image/x-phaseone-cap'], '.cin': ['image/cin', 'image/x-phantom-cin'], '.cr2': ['image/cr2', 'image/x-canon-cr2'], @@ -16,16 +14,7 @@ const image: Record = { '.dng': ['image/dng', 'image/x-adobe-dng'], '.erf': ['image/erf', 'image/x-epson-erf'], '.fff': ['image/fff', 'image/x-hasselblad-fff'], - '.gif': ['image/gif'], - '.heic': ['image/heic'], - '.heif': ['image/heif'], - '.hif': ['image/hif'], '.iiq': ['image/iiq', 'image/x-phaseone-iiq'], - '.insp': ['image/jpeg'], - '.jpe': ['image/jpeg'], - '.jpeg': ['image/jpeg'], - '.jpg': ['image/jpeg'], - '.jxl': ['image/jxl'], '.k25': ['image/k25', 'image/x-kodak-k25'], '.kdc': ['image/kdc', 'image/x-kodak-kdc'], '.mrw': ['image/mrw', 'image/x-minolta-mrw'], @@ -33,7 +22,6 @@ const image: Record = { '.orf': ['image/orf', 'image/x-olympus-orf'], '.ori': ['image/ori', 'image/x-olympus-ori'], '.pef': ['image/pef', 'image/x-pentax-pef'], - '.png': ['image/png'], '.psd': ['image/psd', 'image/vnd.adobe.photoshop'], '.raf': ['image/raf', 'image/x-fuji-raf'], '.raw': ['image/raw', 'image/x-panasonic-raw'], @@ -42,11 +30,27 @@ const image: Record = { '.sr2': ['image/sr2', 'image/x-sony-sr2'], '.srf': ['image/srf', 'image/x-sony-srf'], '.srw': ['image/srw', 'image/x-samsung-srw'], + '.x3f': ['image/x3f', 'image/x-sigma-x3f'], +}; + +const image: Record = { + ...raw, + '.avif': ['image/avif'], + '.bmp': ['image/bmp'], + '.gif': ['image/gif'], + '.heic': ['image/heic'], + '.heif': ['image/heif'], + '.hif': ['image/hif'], + '.insp': ['image/jpeg'], + '.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'], - '.x3f': ['image/x3f', 'image/x-sigma-x3f'], }; const profileExtensions = new Set(['.avif', '.dng', '.heic', '.heif', '.jpeg', '.jpg', '.png', '.webp', '.svg']); @@ -77,22 +81,25 @@ const sidecar: Record = { '.xmp': ['application/xml', 'text/xml'], }; +const types = { ...image, ...video, ...sidecar }; + const isType = (filename: string, r: Record) => extname(filename).toLowerCase() in r; -const lookup = (filename: string) => - ({ ...image, ...video, ...sidecar })[extname(filename).toLowerCase()]?.[0] ?? 'application/octet-stream'; +const lookup = (filename: string) => types[extname(filename).toLowerCase()]?.[0] ?? 'application/octet-stream'; export const mimeTypes = { image, profile, sidecar, video, + raw, isAsset: (filename: string) => isType(filename, image) || isType(filename, video), isImage: (filename: string) => isType(filename, image), isProfile: (filename: string) => isType(filename, profile), isSidecar: (filename: string) => isType(filename, sidecar), isVideo: (filename: string) => isType(filename, video), + isRaw: (filename: string) => isType(filename, raw), lookup, assetType: (filename: string) => { const contentType = lookup(filename); diff --git a/server/test/fixtures/asset.stub.ts b/server/test/fixtures/asset.stub.ts index 7aa49866d0..ce2b070672 100644 --- a/server/test/fixtures/asset.stub.ts +++ b/server/test/fixtures/asset.stub.ts @@ -757,4 +757,45 @@ export const assetStub = { fileSizeInByte: 5000, } as ExifEntity, }), + imageDng: Object.freeze({ + id: 'asset-id', + deviceAssetId: 'device-asset-id', + fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), + fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), + owner: userStub.user1, + ownerId: 'user-id', + deviceId: 'device-id', + originalPath: '/original/path.dng', + previewPath: '/uploads/user-id/thumbs/path.jpg', + checksum: Buffer.from('file hash', 'utf8'), + type: AssetType.IMAGE, + thumbnailPath: '/uploads/user-id/webp/path.ext', + thumbhash: Buffer.from('blablabla', 'base64'), + encodedVideoPath: null, + createdAt: new Date('2023-02-23T05:06:29.716Z'), + updatedAt: new Date('2023-02-23T05:06:29.716Z'), + localDateTime: new Date('2023-02-23T05:06:29.716Z'), + isFavorite: true, + isArchived: false, + isReadOnly: false, + duration: null, + isVisible: true, + isExternal: false, + livePhotoVideo: null, + livePhotoVideoId: null, + isOffline: false, + libraryId: 'library-id', + library: libraryStub.uploadLibrary1, + tags: [], + sharedLinks: [], + originalFileName: 'asset-id.jpg', + faces: [], + deletedAt: null, + sidecarPath: null, + exifInfo: { + fileSizeInByte: 5000, + profileDescription: 'Adobe RGB', + bitsPerSample: 14, + } as ExifEntity, + }), }; diff --git a/server/test/repositories/media.repository.mock.ts b/server/test/repositories/media.repository.mock.ts index 2eea47b6ac..da3e05fe81 100644 --- a/server/test/repositories/media.repository.mock.ts +++ b/server/test/repositories/media.repository.mock.ts @@ -4,9 +4,11 @@ import { Mocked, vitest } from 'vitest'; export const newMediaRepositoryMock = (): Mocked => { return { generateThumbhash: vitest.fn(), + extract: vitest.fn().mockResolvedValue(false), resize: vitest.fn(), crop: vitest.fn(), probe: vitest.fn(), transcode: vitest.fn(), + getImageDimensions: vitest.fn(), }; }; diff --git a/web/src/lib/components/admin-page/settings/image/image-settings.svelte b/web/src/lib/components/admin-page/settings/image/image-settings.svelte index 5b984e2305..2a1853f904 100644 --- a/web/src/lib/components/admin-page/settings/image/image-settings.svelte +++ b/web/src/lib/components/admin-page/settings/image/image-settings.svelte @@ -101,6 +101,16 @@ isEdited={config.image.colorspace !== savedConfig.image.colorspace} {disabled} /> + + (config.image.extractEmbedded = !config.image.extractEmbedded)} + isEdited={config.image.extractEmbedded !== savedConfig.image.extractEmbedded} + {disabled} + />
From 886e07604e559fd263ec2b7609df7506ddc5a495 Mon Sep 17 00:00:00 2001 From: Alex The Bot Date: Fri, 19 Apr 2024 20:08:02 +0000 Subject: [PATCH 03/27] Version v1.102.0 --- cli/package-lock.json | 6 +++--- e2e/package-lock.json | 10 +++++----- e2e/package.json | 2 +- machine-learning/pyproject.toml | 2 +- mobile/android/fastlane/Fastfile | 4 ++-- mobile/ios/fastlane/Fastfile | 2 +- mobile/openapi/README.md | 2 +- mobile/pubspec.yaml | 2 +- open-api/immich-openapi-specs.json | 2 +- open-api/typescript-sdk/package-lock.json | 4 ++-- open-api/typescript-sdk/package.json | 2 +- open-api/typescript-sdk/src/fetch-client.ts | 2 +- server/package-lock.json | 4 ++-- server/package.json | 2 +- web/package-lock.json | 6 +++--- web/package.json | 2 +- 16 files changed, 27 insertions(+), 27 deletions(-) diff --git a/cli/package-lock.json b/cli/package-lock.json index 11154fec52..899c1cd487 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -47,15 +47,15 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.101.0", + "version": "1.102.0", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^20.12.7", - "typescript": "^5.4.5" + "@types/node": "^20.11.0", + "typescript": "^5.3.3" } }, "node_modules/@aashutoshrathi/word-wrap": { diff --git a/e2e/package-lock.json b/e2e/package-lock.json index 6cd8dd90ec..bdafbadd4f 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich-e2e", - "version": "1.101.0", + "version": "1.102.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-e2e", - "version": "1.101.0", + "version": "1.102.0", "license": "GNU Affero General Public License version 3", "devDependencies": { "@immich/cli": "file:../cli", @@ -81,15 +81,15 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.101.0", + "version": "1.102.0", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^20.12.7", - "typescript": "^5.4.5" + "@types/node": "^20.11.0", + "typescript": "^5.3.3" } }, "node_modules/@aashutoshrathi/word-wrap": { diff --git a/e2e/package.json b/e2e/package.json index 9023de8162..9e5ad85fb4 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -1,6 +1,6 @@ { "name": "immich-e2e", - "version": "1.101.0", + "version": "1.102.0", "description": "", "main": "index.js", "type": "module", diff --git a/machine-learning/pyproject.toml b/machine-learning/pyproject.toml index e5d8e06d43..5610f8438d 100644 --- a/machine-learning/pyproject.toml +++ b/machine-learning/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "machine-learning" -version = "1.101.0" +version = "1.102.0" description = "" authors = ["Hau Tran "] readme = "README.md" diff --git a/mobile/android/fastlane/Fastfile b/mobile/android/fastlane/Fastfile index 65307abde8..50a22b6451 100644 --- a/mobile/android/fastlane/Fastfile +++ b/mobile/android/fastlane/Fastfile @@ -35,8 +35,8 @@ platform :android do task: 'bundle', build_type: 'Release', properties: { - "android.injected.version.code" => 131, - "android.injected.version.name" => "1.101.0", + "android.injected.version.code" => 132, + "android.injected.version.name" => "1.102.0", } ) upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab') diff --git a/mobile/ios/fastlane/Fastfile b/mobile/ios/fastlane/Fastfile index 6cf9173c1c..74ffcf4237 100644 --- a/mobile/ios/fastlane/Fastfile +++ b/mobile/ios/fastlane/Fastfile @@ -19,7 +19,7 @@ platform :ios do desc "iOS Beta" lane :beta do increment_version_number( - version_number: "1.101.0" + version_number: "1.102.0" ) increment_build_number( build_number: latest_testflight_build_number + 1, diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 7fb4681f79..7bd651ad10 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -3,7 +3,7 @@ Immich API This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project: -- API version: 1.101.0 +- API version: 1.102.0 - Build package: org.openapitools.codegen.languages.DartClientCodegen ## Requirements diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 8dcc892a06..70c443d832 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -2,7 +2,7 @@ name: immich_mobile description: Immich - selfhosted backup media file on mobile phone publish_to: 'none' -version: 1.101.0+131 +version: 1.102.0+132 environment: sdk: '>=3.0.0 <4.0.0' diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index de3456e519..6e616febbc 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -7006,7 +7006,7 @@ "info": { "title": "Immich", "description": "Immich API", - "version": "1.101.0", + "version": "1.102.0", "contact": {} }, "tags": [], diff --git a/open-api/typescript-sdk/package-lock.json b/open-api/typescript-sdk/package-lock.json index 8def6adffd..e698fb97ff 100644 --- a/open-api/typescript-sdk/package-lock.json +++ b/open-api/typescript-sdk/package-lock.json @@ -1,12 +1,12 @@ { "name": "@immich/sdk", - "version": "1.101.0", + "version": "1.102.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@immich/sdk", - "version": "1.101.0", + "version": "1.102.0", "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" diff --git a/open-api/typescript-sdk/package.json b/open-api/typescript-sdk/package.json index 887fece059..eab26bc4e1 100644 --- a/open-api/typescript-sdk/package.json +++ b/open-api/typescript-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@immich/sdk", - "version": "1.101.0", + "version": "1.102.0", "description": "Auto-generated TypeScript SDK for the Immich API", "type": "module", "main": "./build/index.js", diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index cfa60e9249..dc121117b3 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -1,6 +1,6 @@ /** * Immich - * 1.101.0 + * 1.102.0 * DO NOT MODIFY - This file has been generated using oazapfts. * See https://www.npmjs.com/package/oazapfts */ diff --git a/server/package-lock.json b/server/package-lock.json index 286f1006b9..c3f8e8cf79 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich", - "version": "1.101.0", + "version": "1.102.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "immich", - "version": "1.101.0", + "version": "1.102.0", "license": "GNU Affero General Public License version 3", "dependencies": { "@nestjs/bullmq": "^10.0.1", diff --git a/server/package.json b/server/package.json index d5828822cd..bdecc1362a 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "immich", - "version": "1.101.0", + "version": "1.102.0", "description": "", "author": "", "private": true, diff --git a/web/package-lock.json b/web/package-lock.json index b5e3a6c2f9..d36059ca05 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich-web", - "version": "1.101.0", + "version": "1.102.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-web", - "version": "1.101.0", + "version": "1.102.0", "license": "GNU Affero General Public License version 3", "dependencies": { "@immich/sdk": "file:../open-api/typescript-sdk", @@ -63,7 +63,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.101.0", + "version": "1.102.0", "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" diff --git a/web/package.json b/web/package.json index 8418af55aa..5e2ec844a1 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "immich-web", - "version": "1.101.0", + "version": "1.102.0", "license": "GNU Affero General Public License version 3", "scripts": { "dev": "vite dev --host 0.0.0.0 --port 3000", From 57be9182d4506d886f87061e6bbbf0b61e5f50ff Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 19 Apr 2024 15:32:45 -0500 Subject: [PATCH 04/27] chore: post release tasks --- mobile/android/fastlane/report.xml | 6 +++--- mobile/ios/Runner.xcodeproj/project.pbxproj | 6 +++--- mobile/ios/Runner/Info.plist | 4 ++-- mobile/ios/fastlane/report.xml | 12 ++++++------ 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/mobile/android/fastlane/report.xml b/mobile/android/fastlane/report.xml index d39c4a373f..9ff4624d67 100644 --- a/mobile/android/fastlane/report.xml +++ b/mobile/android/fastlane/report.xml @@ -5,17 +5,17 @@ - + - + - + diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index 1894e39798..26153c05cd 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -383,7 +383,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 147; + CURRENT_PROJECT_VERSION = 148; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -525,7 +525,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 147; + CURRENT_PROJECT_VERSION = 148; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -553,7 +553,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 147; + CURRENT_PROJECT_VERSION = 148; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist index 64b4ea5474..3e7f6a874a 100644 --- a/mobile/ios/Runner/Info.plist +++ b/mobile/ios/Runner/Info.plist @@ -58,11 +58,11 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.101.0 + 1.102.0 CFBundleSignature ???? CFBundleVersion - 147 + 148 FLTEnableImpeller ITSAppUsesNonExemptEncryption diff --git a/mobile/ios/fastlane/report.xml b/mobile/ios/fastlane/report.xml index 1d6f7ff460..85320ab12a 100644 --- a/mobile/ios/fastlane/report.xml +++ b/mobile/ios/fastlane/report.xml @@ -5,32 +5,32 @@ - + - + - + - + - + - + From 78c7ff855de0fe43d8aa956a90c4fa0f87d6f0f7 Mon Sep 17 00:00:00 2001 From: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Sat, 20 Apr 2024 02:35:54 +0200 Subject: [PATCH 05/27] refactor(server): move file file report endpoints to their own controller (#8925) * move file report to its own controller * chore: open api --- mobile/openapi/.openapi-generator/FILES | 3 + mobile/openapi/README.md | 6 +- mobile/openapi/doc/AuditApi.md | 163 ------------- mobile/openapi/doc/FileReportApi.md | 176 ++++++++++++++ mobile/openapi/lib/api.dart | 1 + mobile/openapi/lib/api/audit_api.dart | 130 ---------- mobile/openapi/lib/api/file_report_api.dart | 148 ++++++++++++ mobile/openapi/test/audit_api_test.dart | 15 -- mobile/openapi/test/file_report_api_test.dart | 36 +++ open-api/immich-openapi-specs.json | 224 +++++++++--------- open-api/typescript-sdk/src/fetch-client.ts | 128 +++++----- server/src/controllers/audit.controller.ts | 31 +-- .../src/controllers/file-report.controller.ts | 30 +++ server/src/controllers/index.ts | 16 +- 14 files changed, 585 insertions(+), 522 deletions(-) create mode 100644 mobile/openapi/doc/FileReportApi.md create mode 100644 mobile/openapi/lib/api/file_report_api.dart create mode 100644 mobile/openapi/test/file_report_api_test.dart create mode 100644 server/src/controllers/file-report.controller.ts diff --git a/mobile/openapi/.openapi-generator/FILES b/mobile/openapi/.openapi-generator/FILES index 2181476b3a..42f1034dce 100644 --- a/mobile/openapi/.openapi-generator/FILES +++ b/mobile/openapi/.openapi-generator/FILES @@ -69,6 +69,7 @@ doc/FaceApi.md doc/FaceDto.md doc/FileChecksumDto.md doc/FileChecksumResponseDto.md +doc/FileReportApi.md doc/FileReportDto.md doc/FileReportFixDto.md doc/FileReportItemDto.md @@ -212,6 +213,7 @@ lib/api/audit_api.dart lib/api/authentication_api.dart lib/api/download_api.dart lib/api/face_api.dart +lib/api/file_report_api.dart lib/api/job_api.dart lib/api/library_api.dart lib/api/memory_api.dart @@ -478,6 +480,7 @@ test/face_api_test.dart test/face_dto_test.dart test/file_checksum_dto_test.dart test/file_checksum_response_dto_test.dart +test/file_report_api_test.dart test/file_report_dto_test.dart test/file_report_fix_dto_test.dart test/file_report_item_dto_test.dart diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 7bd651ad10..3ebd65025b 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -112,10 +112,7 @@ Class | Method | HTTP request | Description *AssetApi* | [**updateAssets**](doc//AssetApi.md#updateassets) | **PUT** /asset | *AssetApi* | [**updateStackParent**](doc//AssetApi.md#updatestackparent) | **PUT** /asset/stack/parent | *AssetApi* | [**uploadFile**](doc//AssetApi.md#uploadfile) | **POST** /asset/upload | -*AuditApi* | [**fixAuditFiles**](doc//AuditApi.md#fixauditfiles) | **POST** /audit/file-report/fix | *AuditApi* | [**getAuditDeletes**](doc//AuditApi.md#getauditdeletes) | **GET** /audit/deletes | -*AuditApi* | [**getAuditFiles**](doc//AuditApi.md#getauditfiles) | **GET** /audit/file-report | -*AuditApi* | [**getFileChecksums**](doc//AuditApi.md#getfilechecksums) | **POST** /audit/file-report/checksum | *AuthenticationApi* | [**changePassword**](doc//AuthenticationApi.md#changepassword) | **POST** /auth/change-password | *AuthenticationApi* | [**login**](doc//AuthenticationApi.md#login) | **POST** /auth/login | *AuthenticationApi* | [**logout**](doc//AuthenticationApi.md#logout) | **POST** /auth/logout | @@ -126,6 +123,9 @@ Class | Method | HTTP request | Description *DownloadApi* | [**getDownloadInfo**](doc//DownloadApi.md#getdownloadinfo) | **POST** /download/info | *FaceApi* | [**getFaces**](doc//FaceApi.md#getfaces) | **GET** /face | *FaceApi* | [**reassignFacesById**](doc//FaceApi.md#reassignfacesbyid) | **PUT** /face/{id} | +*FileReportApi* | [**fixAuditFiles**](doc//FileReportApi.md#fixauditfiles) | **POST** /report/fix | +*FileReportApi* | [**getAuditFiles**](doc//FileReportApi.md#getauditfiles) | **GET** /report | +*FileReportApi* | [**getFileChecksums**](doc//FileReportApi.md#getfilechecksums) | **POST** /report/checksum | *JobApi* | [**getAllJobsStatus**](doc//JobApi.md#getalljobsstatus) | **GET** /jobs | *JobApi* | [**sendJobCommand**](doc//JobApi.md#sendjobcommand) | **PUT** /jobs/{id} | *LibraryApi* | [**createLibrary**](doc//LibraryApi.md#createlibrary) | **POST** /library | diff --git a/mobile/openapi/doc/AuditApi.md b/mobile/openapi/doc/AuditApi.md index 8514cdec73..2c768c40d1 100644 --- a/mobile/openapi/doc/AuditApi.md +++ b/mobile/openapi/doc/AuditApi.md @@ -9,66 +9,9 @@ All URIs are relative to */api* Method | HTTP request | Description ------------- | ------------- | ------------- -[**fixAuditFiles**](AuditApi.md#fixauditfiles) | **POST** /audit/file-report/fix | [**getAuditDeletes**](AuditApi.md#getauditdeletes) | **GET** /audit/deletes | -[**getAuditFiles**](AuditApi.md#getauditfiles) | **GET** /audit/file-report | -[**getFileChecksums**](AuditApi.md#getfilechecksums) | **POST** /audit/file-report/checksum | -# **fixAuditFiles** -> fixAuditFiles(fileReportFixDto) - - - -### Example -```dart -import 'package:openapi/api.dart'; -// TODO Configure API key authorization: cookie -//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; -// uncomment below to setup prefix (e.g. Bearer) for API key, if needed -//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; -// TODO Configure API key authorization: api_key -//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; -// uncomment below to setup prefix (e.g. Bearer) for API key, if needed -//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; -// TODO Configure HTTP Bearer authorization: bearer -// Case 1. Use String Token -//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); -// Case 2. Use Function which generate token. -// String yourTokenGeneratorFunction() { ... } -//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); - -final api_instance = AuditApi(); -final fileReportFixDto = FileReportFixDto(); // FileReportFixDto | - -try { - api_instance.fixAuditFiles(fileReportFixDto); -} catch (e) { - print('Exception when calling AuditApi->fixAuditFiles: $e\n'); -} -``` - -### Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **fileReportFixDto** | [**FileReportFixDto**](FileReportFixDto.md)| | - -### Return type - -void (empty response body) - -### Authorization - -[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) - -### HTTP request headers - - - **Content-Type**: application/json - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - # **getAuditDeletes** > AuditDeletesResponseDto getAuditDeletes(after, entityType, userId) @@ -128,109 +71,3 @@ Name | Type | Description | Notes [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) -# **getAuditFiles** -> FileReportDto getAuditFiles() - - - -### Example -```dart -import 'package:openapi/api.dart'; -// TODO Configure API key authorization: cookie -//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; -// uncomment below to setup prefix (e.g. Bearer) for API key, if needed -//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; -// TODO Configure API key authorization: api_key -//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; -// uncomment below to setup prefix (e.g. Bearer) for API key, if needed -//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; -// TODO Configure HTTP Bearer authorization: bearer -// Case 1. Use String Token -//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); -// Case 2. Use Function which generate token. -// String yourTokenGeneratorFunction() { ... } -//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); - -final api_instance = AuditApi(); - -try { - final result = api_instance.getAuditFiles(); - print(result); -} catch (e) { - print('Exception when calling AuditApi->getAuditFiles: $e\n'); -} -``` - -### Parameters -This endpoint does not need any parameter. - -### Return type - -[**FileReportDto**](FileReportDto.md) - -### Authorization - -[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: application/json - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - -# **getFileChecksums** -> List getFileChecksums(fileChecksumDto) - - - -### Example -```dart -import 'package:openapi/api.dart'; -// TODO Configure API key authorization: cookie -//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; -// uncomment below to setup prefix (e.g. Bearer) for API key, if needed -//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; -// TODO Configure API key authorization: api_key -//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; -// uncomment below to setup prefix (e.g. Bearer) for API key, if needed -//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; -// TODO Configure HTTP Bearer authorization: bearer -// Case 1. Use String Token -//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); -// Case 2. Use Function which generate token. -// String yourTokenGeneratorFunction() { ... } -//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); - -final api_instance = AuditApi(); -final fileChecksumDto = FileChecksumDto(); // FileChecksumDto | - -try { - final result = api_instance.getFileChecksums(fileChecksumDto); - print(result); -} catch (e) { - print('Exception when calling AuditApi->getFileChecksums: $e\n'); -} -``` - -### Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **fileChecksumDto** | [**FileChecksumDto**](FileChecksumDto.md)| | - -### Return type - -[**List**](FileChecksumResponseDto.md) - -### Authorization - -[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) - -### HTTP request headers - - - **Content-Type**: application/json - - **Accept**: application/json - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - diff --git a/mobile/openapi/doc/FileReportApi.md b/mobile/openapi/doc/FileReportApi.md new file mode 100644 index 0000000000..b722c86041 --- /dev/null +++ b/mobile/openapi/doc/FileReportApi.md @@ -0,0 +1,176 @@ +# openapi.api.FileReportApi + +## Load the API package +```dart +import 'package:openapi/api.dart'; +``` + +All URIs are relative to */api* + +Method | HTTP request | Description +------------- | ------------- | ------------- +[**fixAuditFiles**](FileReportApi.md#fixauditfiles) | **POST** /report/fix | +[**getAuditFiles**](FileReportApi.md#getauditfiles) | **GET** /report | +[**getFileChecksums**](FileReportApi.md#getfilechecksums) | **POST** /report/checksum | + + +# **fixAuditFiles** +> fixAuditFiles(fileReportFixDto) + + + +### Example +```dart +import 'package:openapi/api.dart'; +// TODO Configure API key authorization: cookie +//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; +// TODO Configure API key authorization: api_key +//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; +// TODO Configure HTTP Bearer authorization: bearer +// Case 1. Use String Token +//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); +// Case 2. Use Function which generate token. +// String yourTokenGeneratorFunction() { ... } +//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); + +final api_instance = FileReportApi(); +final fileReportFixDto = FileReportFixDto(); // FileReportFixDto | + +try { + api_instance.fixAuditFiles(fileReportFixDto); +} catch (e) { + print('Exception when calling FileReportApi->fixAuditFiles: $e\n'); +} +``` + +### Parameters + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **fileReportFixDto** | [**FileReportFixDto**](FileReportFixDto.md)| | + +### Return type + +void (empty response body) + +### Authorization + +[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) + +### HTTP request headers + + - **Content-Type**: application/json + - **Accept**: Not defined + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **getAuditFiles** +> FileReportDto getAuditFiles() + + + +### Example +```dart +import 'package:openapi/api.dart'; +// TODO Configure API key authorization: cookie +//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; +// TODO Configure API key authorization: api_key +//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; +// TODO Configure HTTP Bearer authorization: bearer +// Case 1. Use String Token +//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); +// Case 2. Use Function which generate token. +// String yourTokenGeneratorFunction() { ... } +//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); + +final api_instance = FileReportApi(); + +try { + final result = api_instance.getAuditFiles(); + print(result); +} catch (e) { + print('Exception when calling FileReportApi->getAuditFiles: $e\n'); +} +``` + +### Parameters +This endpoint does not need any parameter. + +### Return type + +[**FileReportDto**](FileReportDto.md) + +### Authorization + +[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **getFileChecksums** +> List getFileChecksums(fileChecksumDto) + + + +### Example +```dart +import 'package:openapi/api.dart'; +// TODO Configure API key authorization: cookie +//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; +// TODO Configure API key authorization: api_key +//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; +// TODO Configure HTTP Bearer authorization: bearer +// Case 1. Use String Token +//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); +// Case 2. Use Function which generate token. +// String yourTokenGeneratorFunction() { ... } +//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); + +final api_instance = FileReportApi(); +final fileChecksumDto = FileChecksumDto(); // FileChecksumDto | + +try { + final result = api_instance.getFileChecksums(fileChecksumDto); + print(result); +} catch (e) { + print('Exception when calling FileReportApi->getFileChecksums: $e\n'); +} +``` + +### Parameters + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **fileChecksumDto** | [**FileChecksumDto**](FileChecksumDto.md)| | + +### Return type + +[**List**](FileChecksumResponseDto.md) + +### Authorization + +[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) + +### HTTP request headers + + - **Content-Type**: application/json + - **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index b484d38b68..8520bab305 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -37,6 +37,7 @@ part 'api/audit_api.dart'; part 'api/authentication_api.dart'; part 'api/download_api.dart'; part 'api/face_api.dart'; +part 'api/file_report_api.dart'; part 'api/job_api.dart'; part 'api/library_api.dart'; part 'api/memory_api.dart'; diff --git a/mobile/openapi/lib/api/audit_api.dart b/mobile/openapi/lib/api/audit_api.dart index 871c8e1905..83dde34da7 100644 --- a/mobile/openapi/lib/api/audit_api.dart +++ b/mobile/openapi/lib/api/audit_api.dart @@ -16,45 +16,6 @@ class AuditApi { final ApiClient apiClient; - /// Performs an HTTP 'POST /audit/file-report/fix' operation and returns the [Response]. - /// Parameters: - /// - /// * [FileReportFixDto] fileReportFixDto (required): - Future fixAuditFilesWithHttpInfo(FileReportFixDto fileReportFixDto,) async { - // ignore: prefer_const_declarations - final path = r'/audit/file-report/fix'; - - // ignore: prefer_final_locals - Object? postBody = fileReportFixDto; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - const contentTypes = ['application/json']; - - - return apiClient.invokeAPI( - path, - 'POST', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Parameters: - /// - /// * [FileReportFixDto] fileReportFixDto (required): - Future fixAuditFiles(FileReportFixDto fileReportFixDto,) async { - final response = await fixAuditFilesWithHttpInfo(fileReportFixDto,); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - } - /// Performs an HTTP 'GET /audit/deletes' operation and returns the [Response]. /// Parameters: /// @@ -115,95 +76,4 @@ class AuditApi { } return null; } - - /// Performs an HTTP 'GET /audit/file-report' operation and returns the [Response]. - Future getAuditFilesWithHttpInfo() async { - // ignore: prefer_const_declarations - final path = r'/audit/file-report'; - - // ignore: prefer_final_locals - Object? postBody; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - const contentTypes = []; - - - return apiClient.invokeAPI( - path, - 'GET', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - Future getAuditFiles() async { - final response = await getAuditFilesWithHttpInfo(); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'FileReportDto',) as FileReportDto; - - } - return null; - } - - /// Performs an HTTP 'POST /audit/file-report/checksum' operation and returns the [Response]. - /// Parameters: - /// - /// * [FileChecksumDto] fileChecksumDto (required): - Future getFileChecksumsWithHttpInfo(FileChecksumDto fileChecksumDto,) async { - // ignore: prefer_const_declarations - final path = r'/audit/file-report/checksum'; - - // ignore: prefer_final_locals - Object? postBody = fileChecksumDto; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - const contentTypes = ['application/json']; - - - return apiClient.invokeAPI( - path, - 'POST', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Parameters: - /// - /// * [FileChecksumDto] fileChecksumDto (required): - Future?> getFileChecksums(FileChecksumDto fileChecksumDto,) async { - final response = await getFileChecksumsWithHttpInfo(fileChecksumDto,); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - final responseBody = await _decodeBodyBytes(response); - return (await apiClient.deserializeAsync(responseBody, 'List') as List) - .cast() - .toList(growable: false); - - } - return null; - } } diff --git a/mobile/openapi/lib/api/file_report_api.dart b/mobile/openapi/lib/api/file_report_api.dart new file mode 100644 index 0000000000..df307e12c7 --- /dev/null +++ b/mobile/openapi/lib/api/file_report_api.dart @@ -0,0 +1,148 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + + +class FileReportApi { + FileReportApi([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient; + + final ApiClient apiClient; + + /// Performs an HTTP 'POST /report/fix' operation and returns the [Response]. + /// Parameters: + /// + /// * [FileReportFixDto] fileReportFixDto (required): + Future fixAuditFilesWithHttpInfo(FileReportFixDto fileReportFixDto,) async { + // ignore: prefer_const_declarations + final path = r'/report/fix'; + + // ignore: prefer_final_locals + Object? postBody = fileReportFixDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + path, + 'POST', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [FileReportFixDto] fileReportFixDto (required): + Future fixAuditFiles(FileReportFixDto fileReportFixDto,) async { + final response = await fixAuditFilesWithHttpInfo(fileReportFixDto,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + } + + /// Performs an HTTP 'GET /report' operation and returns the [Response]. + Future getAuditFilesWithHttpInfo() async { + // ignore: prefer_const_declarations + final path = r'/report'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + path, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + Future getAuditFiles() async { + final response = await getAuditFilesWithHttpInfo(); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'FileReportDto',) as FileReportDto; + + } + return null; + } + + /// Performs an HTTP 'POST /report/checksum' operation and returns the [Response]. + /// Parameters: + /// + /// * [FileChecksumDto] fileChecksumDto (required): + Future getFileChecksumsWithHttpInfo(FileChecksumDto fileChecksumDto,) async { + // ignore: prefer_const_declarations + final path = r'/report/checksum'; + + // ignore: prefer_final_locals + Object? postBody = fileChecksumDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + path, + 'POST', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [FileChecksumDto] fileChecksumDto (required): + Future?> getFileChecksums(FileChecksumDto fileChecksumDto,) async { + final response = await getFileChecksumsWithHttpInfo(fileChecksumDto,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + final responseBody = await _decodeBodyBytes(response); + return (await apiClient.deserializeAsync(responseBody, 'List') as List) + .cast() + .toList(growable: false); + + } + return null; + } +} diff --git a/mobile/openapi/test/audit_api_test.dart b/mobile/openapi/test/audit_api_test.dart index 8161d4e4db..8114283a1a 100644 --- a/mobile/openapi/test/audit_api_test.dart +++ b/mobile/openapi/test/audit_api_test.dart @@ -17,25 +17,10 @@ void main() { // final instance = AuditApi(); group('tests for AuditApi', () { - //Future fixAuditFiles(FileReportFixDto fileReportFixDto) async - test('test fixAuditFiles', () async { - // TODO - }); - //Future getAuditDeletes(DateTime after, EntityType entityType, { String userId }) async test('test getAuditDeletes', () async { // TODO }); - //Future getAuditFiles() async - test('test getAuditFiles', () async { - // TODO - }); - - //Future> getFileChecksums(FileChecksumDto fileChecksumDto) async - test('test getFileChecksums', () async { - // TODO - }); - }); } diff --git a/mobile/openapi/test/file_report_api_test.dart b/mobile/openapi/test/file_report_api_test.dart new file mode 100644 index 0000000000..255c787002 --- /dev/null +++ b/mobile/openapi/test/file_report_api_test.dart @@ -0,0 +1,36 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +import 'package:openapi/api.dart'; +import 'package:test/test.dart'; + + +/// tests for FileReportApi +void main() { + // final instance = FileReportApi(); + + group('tests for FileReportApi', () { + //Future fixAuditFiles(FileReportFixDto fileReportFixDto) async + test('test fixAuditFiles', () async { + // TODO + }); + + //Future getAuditFiles() async + test('test getAuditFiles', () async { + // TODO + }); + + //Future> getFileChecksums(FileChecksumDto fileChecksumDto) async + test('test getFileChecksums', () async { + // TODO + }); + + }); +} diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 6e616febbc..f49df7baea 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -2345,118 +2345,6 @@ ] } }, - "/audit/file-report": { - "get": { - "operationId": "getAuditFiles", - "parameters": [], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/FileReportDto" - } - } - }, - "description": "" - } - }, - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ], - "tags": [ - "Audit" - ] - } - }, - "/audit/file-report/checksum": { - "post": { - "operationId": "getFileChecksums", - "parameters": [], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/FileChecksumDto" - } - } - }, - "required": true - }, - "responses": { - "201": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/FileChecksumResponseDto" - }, - "type": "array" - } - } - }, - "description": "" - } - }, - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ], - "tags": [ - "Audit" - ] - } - }, - "/audit/file-report/fix": { - "post": { - "operationId": "fixAuditFiles", - "parameters": [], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/FileReportFixDto" - } - } - }, - "required": true - }, - "responses": { - "201": { - "description": "" - } - }, - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ], - "tags": [ - "Audit" - ] - } - }, "/auth/admin-sign-up": { "post": { "operationId": "signUpAdmin", @@ -4429,6 +4317,118 @@ ] } }, + "/report": { + "get": { + "operationId": "getAuditFiles", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileReportDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "File Report" + ] + } + }, + "/report/checksum": { + "post": { + "operationId": "getFileChecksums", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileChecksumDto" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/FileChecksumResponseDto" + }, + "type": "array" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "File Report" + ] + } + }, + "/report/fix": { + "post": { + "operationId": "fixAuditFiles", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileReportFixDto" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "File Report" + ] + } + }, "/search": { "get": { "deprecated": true, diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index dc121117b3..9148b4d3b1 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -316,27 +316,6 @@ export type AuditDeletesResponseDto = { ids: string[]; needsFullSync: boolean; }; -export type FileReportItemDto = { - checksum?: string; - entityId: string; - entityType: PathEntityType; - pathType: PathType; - pathValue: string; -}; -export type FileReportDto = { - extras: string[]; - orphans: FileReportItemDto[]; -}; -export type FileChecksumDto = { - filenames: string[]; -}; -export type FileChecksumResponseDto = { - checksum: string; - filename: string; -}; -export type FileReportFixDto = { - items: FileReportItemDto[]; -}; export type SignUpDto = { email: string; name: string; @@ -599,6 +578,27 @@ export type AssetFaceUpdateDto = { export type PersonStatisticsResponseDto = { assets: number; }; +export type FileReportItemDto = { + checksum?: string; + entityId: string; + entityType: PathEntityType; + pathType: PathType; + pathValue: string; +}; +export type FileReportDto = { + extras: string[]; + orphans: FileReportItemDto[]; +}; +export type FileChecksumDto = { + filenames: string[]; +}; +export type FileChecksumResponseDto = { + checksum: string; + filename: string; +}; +export type FileReportFixDto = { + items: FileReportItemDto[]; +}; export type SearchFacetCountResponseDto = { count: number; value: string; @@ -1651,35 +1651,6 @@ export function getAuditDeletes({ after, entityType, userId }: { ...opts })); } -export function getAuditFiles(opts?: Oazapfts.RequestOpts) { - return oazapfts.ok(oazapfts.fetchJson<{ - status: 200; - data: FileReportDto; - }>("/audit/file-report", { - ...opts - })); -} -export function getFileChecksums({ fileChecksumDto }: { - fileChecksumDto: FileChecksumDto; -}, opts?: Oazapfts.RequestOpts) { - return oazapfts.ok(oazapfts.fetchJson<{ - status: 201; - data: FileChecksumResponseDto[]; - }>("/audit/file-report/checksum", oazapfts.json({ - ...opts, - method: "POST", - body: fileChecksumDto - }))); -} -export function fixAuditFiles({ fileReportFixDto }: { - fileReportFixDto: FileReportFixDto; -}, opts?: Oazapfts.RequestOpts) { - return oazapfts.ok(oazapfts.fetchText("/audit/file-report/fix", oazapfts.json({ - ...opts, - method: "POST", - body: fileReportFixDto - }))); -} export function signUpAdmin({ signUpDto }: { signUpDto: SignUpDto; }, opts?: Oazapfts.RequestOpts) { @@ -2206,6 +2177,35 @@ export function getPersonThumbnail({ id }: { ...opts })); } +export function getAuditFiles(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: FileReportDto; + }>("/report", { + ...opts + })); +} +export function getFileChecksums({ fileChecksumDto }: { + fileChecksumDto: FileChecksumDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 201; + data: FileChecksumResponseDto[]; + }>("/report/checksum", oazapfts.json({ + ...opts, + method: "POST", + body: fileChecksumDto + }))); +} +export function fixAuditFiles({ fileReportFixDto }: { + fileReportFixDto: FileReportFixDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText("/report/fix", oazapfts.json({ + ...opts, + method: "POST", + body: fileReportFixDto + }))); +} export function search({ clip, motion, page, q, query, recent, size, smart, $type, withArchived }: { clip?: boolean; motion?: boolean; @@ -2948,20 +2948,6 @@ export enum EntityType { Asset = "ASSET", Album = "ALBUM" } -export enum PathEntityType { - Asset = "asset", - Person = "person", - User = "user" -} -export enum PathType { - Original = "original", - Preview = "preview", - Thumbnail = "thumbnail", - EncodedVideo = "encoded_video", - Sidecar = "sidecar", - Face = "face", - Profile = "profile" -} export enum JobName { ThumbnailGeneration = "thumbnailGeneration", MetadataExtraction = "metadataExtraction", @@ -2993,6 +2979,20 @@ export enum Type2 { export enum MemoryType { OnThisDay = "on_this_day" } +export enum PathEntityType { + Asset = "asset", + Person = "person", + User = "user" +} +export enum PathType { + Original = "original", + Preview = "preview", + Thumbnail = "thumbnail", + EncodedVideo = "encoded_video", + Sidecar = "sidecar", + Face = "face", + Profile = "profile" +} export enum SearchSuggestionType { Country = "country", State = "state", diff --git a/server/src/controllers/audit.controller.ts b/server/src/controllers/audit.controller.ts index 1487e78d47..8eea6a6e3e 100644 --- a/server/src/controllers/audit.controller.ts +++ b/server/src/controllers/audit.controller.ts @@ -1,15 +1,8 @@ -import { Body, Controller, Get, Post, Query } from '@nestjs/common'; +import { Controller, Get, Query } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; -import { - AuditDeletesDto, - AuditDeletesResponseDto, - FileChecksumDto, - FileChecksumResponseDto, - FileReportDto, - FileReportFixDto, -} from 'src/dtos/audit.dto'; +import { AuditDeletesDto, AuditDeletesResponseDto } from 'src/dtos/audit.dto'; import { AuthDto } from 'src/dtos/auth.dto'; -import { AdminRoute, Auth, Authenticated } from 'src/middleware/auth.guard'; +import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { AuditService } from 'src/services/audit.service'; @ApiTags('Audit') @@ -22,22 +15,4 @@ export class AuditController { getAuditDeletes(@Auth() auth: AuthDto, @Query() dto: AuditDeletesDto): Promise { return this.service.getDeletes(auth, dto); } - - @AdminRoute() - @Get('file-report') - getAuditFiles(): Promise { - return this.service.getFileReport(); - } - - @AdminRoute() - @Post('file-report/checksum') - getFileChecksums(@Body() dto: FileChecksumDto): Promise { - return this.service.getChecksums(dto); - } - - @AdminRoute() - @Post('file-report/fix') - fixAuditFiles(@Body() dto: FileReportFixDto): Promise { - return this.service.fixItems(dto.items); - } } diff --git a/server/src/controllers/file-report.controller.ts b/server/src/controllers/file-report.controller.ts new file mode 100644 index 0000000000..6bdf726073 --- /dev/null +++ b/server/src/controllers/file-report.controller.ts @@ -0,0 +1,30 @@ +import { Body, Controller, Get, Post } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { FileChecksumDto, FileChecksumResponseDto, FileReportDto, FileReportFixDto } from 'src/dtos/audit.dto'; +import { AdminRoute, Authenticated } from 'src/middleware/auth.guard'; +import { AuditService } from 'src/services/audit.service'; + +@ApiTags('File Report') +@Controller('report') +@Authenticated() +export class ReportController { + constructor(private service: AuditService) {} + + @AdminRoute() + @Get() + getAuditFiles(): Promise { + return this.service.getFileReport(); + } + + @AdminRoute() + @Post('/checksum') + getFileChecksums(@Body() dto: FileChecksumDto): Promise { + return this.service.getChecksums(dto); + } + + @AdminRoute() + @Post('/fix') + fixAuditFiles(@Body() dto: FileReportFixDto): Promise { + return this.service.fixItems(dto.items); + } +} diff --git a/server/src/controllers/index.ts b/server/src/controllers/index.ts index 5e109f1eb3..ad2f6e8de1 100644 --- a/server/src/controllers/index.ts +++ b/server/src/controllers/index.ts @@ -8,6 +8,7 @@ import { AuditController } from 'src/controllers/audit.controller'; import { AuthController } from 'src/controllers/auth.controller'; import { DownloadController } from 'src/controllers/download.controller'; import { FaceController } from 'src/controllers/face.controller'; +import { ReportController } from 'src/controllers/file-report.controller'; import { JobController } from 'src/controllers/job.controller'; import { LibraryController } from 'src/controllers/library.controller'; import { MemoryController } from 'src/controllers/memory.controller'; @@ -26,13 +27,13 @@ import { TrashController } from 'src/controllers/trash.controller'; import { UserController } from 'src/controllers/user.controller'; export const controllers = [ - ActivityController, - AssetsController, - AssetControllerV1, - AssetController, - AppController, - AlbumController, APIKeyController, + ActivityController, + AlbumController, + AppController, + AssetController, + AssetControllerV1, + AssetsController, AuditController, AuthController, DownloadController, @@ -42,6 +43,8 @@ export const controllers = [ MemoryController, OAuthController, PartnerController, + PersonController, + ReportController, SearchController, ServerInfoController, SessionController, @@ -52,5 +55,4 @@ export const controllers = [ TimelineController, TrashController, UserController, - PersonController, ]; From 171b6bb0a6571262420c0bd3789d66267bc097e1 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Fri, 19 Apr 2024 20:36:15 -0400 Subject: [PATCH 06/27] refactor: system metadata (#8923) refactor(server): system metadata --- e2e/src/api/specs/server-info.e2e-spec.ts | 17 +- e2e/src/api/specs/system-metadata.e2e-spec.ts | 76 ++++++++ e2e/src/utils.ts | 7 +- mobile/openapi/.openapi-generator/FILES | 9 + mobile/openapi/README.md | 6 +- .../openapi/doc/AdminOnboardingUpdateDto.md | 15 ++ .../doc/ReverseGeocodingStateResponseDto.md | 16 ++ mobile/openapi/doc/ServerInfoApi.md | 51 ------ mobile/openapi/doc/SystemMetadataApi.md | 172 ++++++++++++++++++ mobile/openapi/lib/api.dart | 3 + mobile/openapi/lib/api/server_info_api.dart | 33 ---- .../openapi/lib/api/system_metadata_api.dart | 139 ++++++++++++++ mobile/openapi/lib/api_client.dart | 4 + .../model/admin_onboarding_update_dto.dart | 98 ++++++++++ .../reverse_geocoding_state_response_dto.dart | 114 ++++++++++++ .../admin_onboarding_update_dto_test.dart | 27 +++ ...rse_geocoding_state_response_dto_test.dart | 32 ++++ mobile/openapi/test/server_info_api_test.dart | 5 - .../test/system_metadata_api_test.dart | 36 ++++ open-api/immich-openapi-specs.json | 150 ++++++++++++--- open-api/typescript-sdk/src/fetch-client.ts | 38 +++- server/src/controllers/index.ts | 2 + .../src/controllers/server-info.controller.ts | 9 +- .../controllers/system-metadata.controller.ts | 28 +++ server/src/dtos/system-metadata.dto.ts | 15 ++ .../src/repositories/metadata.repository.ts | 2 +- server/src/services/index.ts | 2 + .../src/services/server-info.service.spec.ts | 8 - server/src/services/server-info.service.ts | 8 +- .../services/system-metadata.service.spec.ts | 31 ++++ .../src/services/system-metadata.service.ts | 29 +++ web/src/routes/auth/onboarding/+page.svelte | 4 +- 32 files changed, 1023 insertions(+), 163 deletions(-) create mode 100644 e2e/src/api/specs/system-metadata.e2e-spec.ts create mode 100644 mobile/openapi/doc/AdminOnboardingUpdateDto.md create mode 100644 mobile/openapi/doc/ReverseGeocodingStateResponseDto.md create mode 100644 mobile/openapi/doc/SystemMetadataApi.md create mode 100644 mobile/openapi/lib/api/system_metadata_api.dart create mode 100644 mobile/openapi/lib/model/admin_onboarding_update_dto.dart create mode 100644 mobile/openapi/lib/model/reverse_geocoding_state_response_dto.dart create mode 100644 mobile/openapi/test/admin_onboarding_update_dto_test.dart create mode 100644 mobile/openapi/test/reverse_geocoding_state_response_dto_test.dart create mode 100644 mobile/openapi/test/system_metadata_api_test.dart create mode 100644 server/src/controllers/system-metadata.controller.ts create mode 100644 server/src/dtos/system-metadata.dto.ts create mode 100644 server/src/services/system-metadata.service.spec.ts create mode 100644 server/src/services/system-metadata.service.ts diff --git a/e2e/src/api/specs/server-info.e2e-spec.ts b/e2e/src/api/specs/server-info.e2e-spec.ts index 5cfd6a8b98..690bfae744 100644 --- a/e2e/src/api/specs/server-info.e2e-spec.ts +++ b/e2e/src/api/specs/server-info.e2e-spec.ts @@ -1,4 +1,4 @@ -import { LoginResponseDto, getServerConfig } from '@immich/sdk'; +import { LoginResponseDto } from '@immich/sdk'; import { createUserDto } from 'src/fixtures'; import { errorDto } from 'src/responses'; import { app, utils } from 'src/utils'; @@ -162,19 +162,4 @@ describe('/server-info', () => { }); }); }); - - describe('POST /server-info/admin-onboarding', () => { - it('should set admin onboarding', async () => { - const config = await getServerConfig({}); - expect(config.isOnboarded).toBe(false); - - const { status } = await request(app) - .post('/server-info/admin-onboarding') - .set('Authorization', `Bearer ${admin.accessToken}`); - expect(status).toBe(204); - - const newConfig = await getServerConfig({}); - expect(newConfig.isOnboarded).toBe(true); - }); - }); }); diff --git a/e2e/src/api/specs/system-metadata.e2e-spec.ts b/e2e/src/api/specs/system-metadata.e2e-spec.ts new file mode 100644 index 0000000000..bd17bf2524 --- /dev/null +++ b/e2e/src/api/specs/system-metadata.e2e-spec.ts @@ -0,0 +1,76 @@ +import { LoginResponseDto, getServerConfig } from '@immich/sdk'; +import { createUserDto } from 'src/fixtures'; +import { errorDto } from 'src/responses'; +import { app, utils } from 'src/utils'; +import request from 'supertest'; +import { beforeAll, describe, expect, it } from 'vitest'; + +describe('/server-info', () => { + let admin: LoginResponseDto; + let nonAdmin: LoginResponseDto; + + beforeAll(async () => { + await utils.resetDatabase(); + admin = await utils.adminSetup({ onboarding: false }); + nonAdmin = await utils.userSetup(admin.accessToken, createUserDto.user1); + }); + + describe('POST /system-metadata/admin-onboarding', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).post('/system-metadata/admin-onboarding').send({ isOnboarded: true }); + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should only work for admins', async () => { + const { status, body } = await request(app) + .post('/system-metadata/admin-onboarding') + .set('Authorization', `Bearer ${nonAdmin.accessToken}`) + .send({ isOnboarded: true }); + expect(status).toBe(403); + expect(body).toEqual(errorDto.forbidden); + }); + + it('should set admin onboarding', async () => { + const config = await getServerConfig({}); + expect(config.isOnboarded).toBe(false); + + const { status } = await request(app) + .post('/system-metadata/admin-onboarding') + .set('Authorization', `Bearer ${admin.accessToken}`) + .send({ isOnboarded: true }); + expect(status).toBe(204); + + const newConfig = await getServerConfig({}); + expect(newConfig.isOnboarded).toBe(true); + }); + }); + + describe('GET /system-metadata/reverse-geocoding-state', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).get('/system-metadata/reverse-geocoding-state'); + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should only work for admins', async () => { + const { status, body } = await request(app) + .get('/system-metadata/reverse-geocoding-state') + .set('Authorization', `Bearer ${nonAdmin.accessToken}`); + expect(status).toBe(403); + expect(body).toEqual(errorDto.forbidden); + }); + + it('should get the reverse geocoding state', async () => { + const { status, body } = await request(app) + .get('/system-metadata/reverse-geocoding-state') + .set('Authorization', `Bearer ${admin.accessToken}`); + + expect(status).toBe(200); + expect(body).toEqual({ + lastUpdate: expect.any(String), + lastImportFileName: 'cities500.txt', + }); + }); + }); +}); diff --git a/e2e/src/utils.ts b/e2e/src/utils.ts index 0047502023..96994c7f0a 100644 --- a/e2e/src/utils.ts +++ b/e2e/src/utils.ts @@ -24,8 +24,8 @@ import { getConfigDefaults, login, searchMetadata, - setAdminOnboarding, signUpAdmin, + updateAdminOnboarding, updateConfig, validate, } from '@immich/sdk'; @@ -264,7 +264,10 @@ export const utils = { await signUpAdmin({ signUpDto: signupDto.admin }); const response = await login({ loginCredentialDto: loginDto.admin }); if (options.onboarding) { - await setAdminOnboarding({ headers: asBearerAuth(response.accessToken) }); + await updateAdminOnboarding( + { adminOnboardingUpdateDto: { isOnboarded: true } }, + { headers: asBearerAuth(response.accessToken) }, + ); } return response; }, diff --git a/mobile/openapi/.openapi-generator/FILES b/mobile/openapi/.openapi-generator/FILES index 42f1034dce..64229329aa 100644 --- a/mobile/openapi/.openapi-generator/FILES +++ b/mobile/openapi/.openapi-generator/FILES @@ -13,6 +13,7 @@ doc/ActivityCreateDto.md doc/ActivityResponseDto.md doc/ActivityStatisticsResponseDto.md doc/AddUsersDto.md +doc/AdminOnboardingUpdateDto.md doc/AlbumApi.md doc/AlbumCountResponseDto.md doc/AlbumResponseDto.md @@ -123,6 +124,7 @@ doc/QueueStatusDto.md doc/ReactionLevel.md doc/ReactionType.md doc/RecognitionConfig.md +doc/ReverseGeocodingStateResponseDto.md doc/ScanLibraryDto.md doc/SearchAlbumResponseDto.md doc/SearchApi.md @@ -174,6 +176,7 @@ doc/SystemConfigTemplateStorageOptionDto.md doc/SystemConfigThemeDto.md doc/SystemConfigTrashDto.md doc/SystemConfigUserDto.md +doc/SystemMetadataApi.md doc/TagApi.md doc/TagResponseDto.md doc/TagTypeEnum.md @@ -226,6 +229,7 @@ lib/api/sessions_api.dart lib/api/shared_link_api.dart lib/api/sync_api.dart lib/api/system_config_api.dart +lib/api/system_metadata_api.dart lib/api/tag_api.dart lib/api/timeline_api.dart lib/api/trash_api.dart @@ -242,6 +246,7 @@ lib/model/activity_create_dto.dart lib/model/activity_response_dto.dart lib/model/activity_statistics_response_dto.dart lib/model/add_users_dto.dart +lib/model/admin_onboarding_update_dto.dart lib/model/album_count_response_dto.dart lib/model/album_response_dto.dart lib/model/all_job_status_response_dto.dart @@ -343,6 +348,7 @@ lib/model/queue_status_dto.dart lib/model/reaction_level.dart lib/model/reaction_type.dart lib/model/recognition_config.dart +lib/model/reverse_geocoding_state_response_dto.dart lib/model/scan_library_dto.dart lib/model/search_album_response_dto.dart lib/model/search_asset_response_dto.dart @@ -419,6 +425,7 @@ test/activity_create_dto_test.dart test/activity_response_dto_test.dart test/activity_statistics_response_dto_test.dart test/add_users_dto_test.dart +test/admin_onboarding_update_dto_test.dart test/album_api_test.dart test/album_count_response_dto_test.dart test/album_response_dto_test.dart @@ -534,6 +541,7 @@ test/queue_status_dto_test.dart test/reaction_level_test.dart test/reaction_type_test.dart test/recognition_config_test.dart +test/reverse_geocoding_state_response_dto_test.dart test/scan_library_dto_test.dart test/search_album_response_dto_test.dart test/search_api_test.dart @@ -585,6 +593,7 @@ test/system_config_template_storage_option_dto_test.dart test/system_config_theme_dto_test.dart test/system_config_trash_dto_test.dart test/system_config_user_dto_test.dart +test/system_metadata_api_test.dart test/tag_api_test.dart test/tag_response_dto_test.dart test/tag_type_enum_test.dart diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 3ebd65025b..27d631e4fd 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -179,7 +179,6 @@ Class | Method | HTTP request | Description *ServerInfoApi* | [**getSupportedMediaTypes**](doc//ServerInfoApi.md#getsupportedmediatypes) | **GET** /server-info/media-types | *ServerInfoApi* | [**getTheme**](doc//ServerInfoApi.md#gettheme) | **GET** /server-info/theme | *ServerInfoApi* | [**pingServer**](doc//ServerInfoApi.md#pingserver) | **GET** /server-info/ping | -*ServerInfoApi* | [**setAdminOnboarding**](doc//ServerInfoApi.md#setadminonboarding) | **POST** /server-info/admin-onboarding | *SessionsApi* | [**deleteAllSessions**](doc//SessionsApi.md#deleteallsessions) | **DELETE** /sessions | *SessionsApi* | [**deleteSession**](doc//SessionsApi.md#deletesession) | **DELETE** /sessions/{id} | *SessionsApi* | [**getSessions**](doc//SessionsApi.md#getsessions) | **GET** /sessions | @@ -198,6 +197,9 @@ Class | Method | HTTP request | Description *SystemConfigApi* | [**getMapStyle**](doc//SystemConfigApi.md#getmapstyle) | **GET** /system-config/map/style.json | *SystemConfigApi* | [**getStorageTemplateOptions**](doc//SystemConfigApi.md#getstoragetemplateoptions) | **GET** /system-config/storage-template-options | *SystemConfigApi* | [**updateConfig**](doc//SystemConfigApi.md#updateconfig) | **PUT** /system-config | +*SystemMetadataApi* | [**getAdminOnboarding**](doc//SystemMetadataApi.md#getadminonboarding) | **GET** /system-metadata/admin-onboarding | +*SystemMetadataApi* | [**getReverseGeocodingState**](doc//SystemMetadataApi.md#getreversegeocodingstate) | **GET** /system-metadata/reverse-geocoding-state | +*SystemMetadataApi* | [**updateAdminOnboarding**](doc//SystemMetadataApi.md#updateadminonboarding) | **POST** /system-metadata/admin-onboarding | *TagApi* | [**createTag**](doc//TagApi.md#createtag) | **POST** /tag | *TagApi* | [**deleteTag**](doc//TagApi.md#deletetag) | **DELETE** /tag/{id} | *TagApi* | [**getAllTags**](doc//TagApi.md#getalltags) | **GET** /tag | @@ -233,6 +235,7 @@ Class | Method | HTTP request | Description - [ActivityResponseDto](doc//ActivityResponseDto.md) - [ActivityStatisticsResponseDto](doc//ActivityStatisticsResponseDto.md) - [AddUsersDto](doc//AddUsersDto.md) + - [AdminOnboardingUpdateDto](doc//AdminOnboardingUpdateDto.md) - [AlbumCountResponseDto](doc//AlbumCountResponseDto.md) - [AlbumResponseDto](doc//AlbumResponseDto.md) - [AllJobStatusResponseDto](doc//AllJobStatusResponseDto.md) @@ -330,6 +333,7 @@ Class | Method | HTTP request | Description - [ReactionLevel](doc//ReactionLevel.md) - [ReactionType](doc//ReactionType.md) - [RecognitionConfig](doc//RecognitionConfig.md) + - [ReverseGeocodingStateResponseDto](doc//ReverseGeocodingStateResponseDto.md) - [ScanLibraryDto](doc//ScanLibraryDto.md) - [SearchAlbumResponseDto](doc//SearchAlbumResponseDto.md) - [SearchAssetResponseDto](doc//SearchAssetResponseDto.md) diff --git a/mobile/openapi/doc/AdminOnboardingUpdateDto.md b/mobile/openapi/doc/AdminOnboardingUpdateDto.md new file mode 100644 index 0000000000..b250843019 --- /dev/null +++ b/mobile/openapi/doc/AdminOnboardingUpdateDto.md @@ -0,0 +1,15 @@ +# openapi.model.AdminOnboardingUpdateDto + +## Load the model package +```dart +import 'package:openapi/api.dart'; +``` + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**isOnboarded** | **bool** | | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/mobile/openapi/doc/ReverseGeocodingStateResponseDto.md b/mobile/openapi/doc/ReverseGeocodingStateResponseDto.md new file mode 100644 index 0000000000..87f8aa8ab7 --- /dev/null +++ b/mobile/openapi/doc/ReverseGeocodingStateResponseDto.md @@ -0,0 +1,16 @@ +# openapi.model.ReverseGeocodingStateResponseDto + +## Load the model package +```dart +import 'package:openapi/api.dart'; +``` + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**lastImportFileName** | **String** | | +**lastUpdate** | **String** | | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/mobile/openapi/doc/ServerInfoApi.md b/mobile/openapi/doc/ServerInfoApi.md index cb5cf0fd3e..e8121a8001 100644 --- a/mobile/openapi/doc/ServerInfoApi.md +++ b/mobile/openapi/doc/ServerInfoApi.md @@ -17,7 +17,6 @@ Method | HTTP request | Description [**getSupportedMediaTypes**](ServerInfoApi.md#getsupportedmediatypes) | **GET** /server-info/media-types | [**getTheme**](ServerInfoApi.md#gettheme) | **GET** /server-info/theme | [**pingServer**](ServerInfoApi.md#pingserver) | **GET** /server-info/ping | -[**setAdminOnboarding**](ServerInfoApi.md#setadminonboarding) | **POST** /server-info/admin-onboarding | # **getServerConfig** @@ -344,53 +343,3 @@ No authorization required [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) -# **setAdminOnboarding** -> setAdminOnboarding() - - - -### Example -```dart -import 'package:openapi/api.dart'; -// TODO Configure API key authorization: cookie -//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; -// uncomment below to setup prefix (e.g. Bearer) for API key, if needed -//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; -// TODO Configure API key authorization: api_key -//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; -// uncomment below to setup prefix (e.g. Bearer) for API key, if needed -//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; -// TODO Configure HTTP Bearer authorization: bearer -// Case 1. Use String Token -//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); -// Case 2. Use Function which generate token. -// String yourTokenGeneratorFunction() { ... } -//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); - -final api_instance = ServerInfoApi(); - -try { - api_instance.setAdminOnboarding(); -} catch (e) { - print('Exception when calling ServerInfoApi->setAdminOnboarding: $e\n'); -} -``` - -### Parameters -This endpoint does not need any parameter. - -### Return type - -void (empty response body) - -### Authorization - -[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - diff --git a/mobile/openapi/doc/SystemMetadataApi.md b/mobile/openapi/doc/SystemMetadataApi.md new file mode 100644 index 0000000000..f8c2347afe --- /dev/null +++ b/mobile/openapi/doc/SystemMetadataApi.md @@ -0,0 +1,172 @@ +# openapi.api.SystemMetadataApi + +## Load the API package +```dart +import 'package:openapi/api.dart'; +``` + +All URIs are relative to */api* + +Method | HTTP request | Description +------------- | ------------- | ------------- +[**getAdminOnboarding**](SystemMetadataApi.md#getadminonboarding) | **GET** /system-metadata/admin-onboarding | +[**getReverseGeocodingState**](SystemMetadataApi.md#getreversegeocodingstate) | **GET** /system-metadata/reverse-geocoding-state | +[**updateAdminOnboarding**](SystemMetadataApi.md#updateadminonboarding) | **POST** /system-metadata/admin-onboarding | + + +# **getAdminOnboarding** +> AdminOnboardingUpdateDto getAdminOnboarding() + + + +### Example +```dart +import 'package:openapi/api.dart'; +// TODO Configure API key authorization: cookie +//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; +// TODO Configure API key authorization: api_key +//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; +// TODO Configure HTTP Bearer authorization: bearer +// Case 1. Use String Token +//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); +// Case 2. Use Function which generate token. +// String yourTokenGeneratorFunction() { ... } +//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); + +final api_instance = SystemMetadataApi(); + +try { + final result = api_instance.getAdminOnboarding(); + print(result); +} catch (e) { + print('Exception when calling SystemMetadataApi->getAdminOnboarding: $e\n'); +} +``` + +### Parameters +This endpoint does not need any parameter. + +### Return type + +[**AdminOnboardingUpdateDto**](AdminOnboardingUpdateDto.md) + +### Authorization + +[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **getReverseGeocodingState** +> ReverseGeocodingStateResponseDto getReverseGeocodingState() + + + +### Example +```dart +import 'package:openapi/api.dart'; +// TODO Configure API key authorization: cookie +//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; +// TODO Configure API key authorization: api_key +//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; +// TODO Configure HTTP Bearer authorization: bearer +// Case 1. Use String Token +//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); +// Case 2. Use Function which generate token. +// String yourTokenGeneratorFunction() { ... } +//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); + +final api_instance = SystemMetadataApi(); + +try { + final result = api_instance.getReverseGeocodingState(); + print(result); +} catch (e) { + print('Exception when calling SystemMetadataApi->getReverseGeocodingState: $e\n'); +} +``` + +### Parameters +This endpoint does not need any parameter. + +### Return type + +[**ReverseGeocodingStateResponseDto**](ReverseGeocodingStateResponseDto.md) + +### Authorization + +[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **updateAdminOnboarding** +> updateAdminOnboarding(adminOnboardingUpdateDto) + + + +### Example +```dart +import 'package:openapi/api.dart'; +// TODO Configure API key authorization: cookie +//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; +// TODO Configure API key authorization: api_key +//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; +// TODO Configure HTTP Bearer authorization: bearer +// Case 1. Use String Token +//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); +// Case 2. Use Function which generate token. +// String yourTokenGeneratorFunction() { ... } +//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); + +final api_instance = SystemMetadataApi(); +final adminOnboardingUpdateDto = AdminOnboardingUpdateDto(); // AdminOnboardingUpdateDto | + +try { + api_instance.updateAdminOnboarding(adminOnboardingUpdateDto); +} catch (e) { + print('Exception when calling SystemMetadataApi->updateAdminOnboarding: $e\n'); +} +``` + +### Parameters + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **adminOnboardingUpdateDto** | [**AdminOnboardingUpdateDto**](AdminOnboardingUpdateDto.md)| | + +### Return type + +void (empty response body) + +### Authorization + +[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) + +### HTTP request headers + + - **Content-Type**: application/json + - **Accept**: Not defined + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 8520bab305..44bd35a683 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -50,6 +50,7 @@ part 'api/sessions_api.dart'; part 'api/shared_link_api.dart'; part 'api/sync_api.dart'; part 'api/system_config_api.dart'; +part 'api/system_metadata_api.dart'; part 'api/tag_api.dart'; part 'api/timeline_api.dart'; part 'api/trash_api.dart'; @@ -63,6 +64,7 @@ part 'model/activity_create_dto.dart'; part 'model/activity_response_dto.dart'; part 'model/activity_statistics_response_dto.dart'; part 'model/add_users_dto.dart'; +part 'model/admin_onboarding_update_dto.dart'; part 'model/album_count_response_dto.dart'; part 'model/album_response_dto.dart'; part 'model/all_job_status_response_dto.dart'; @@ -160,6 +162,7 @@ part 'model/queue_status_dto.dart'; part 'model/reaction_level.dart'; part 'model/reaction_type.dart'; part 'model/recognition_config.dart'; +part 'model/reverse_geocoding_state_response_dto.dart'; part 'model/scan_library_dto.dart'; part 'model/search_album_response_dto.dart'; part 'model/search_asset_response_dto.dart'; diff --git a/mobile/openapi/lib/api/server_info_api.dart b/mobile/openapi/lib/api/server_info_api.dart index 77840acd19..b67045add1 100644 --- a/mobile/openapi/lib/api/server_info_api.dart +++ b/mobile/openapi/lib/api/server_info_api.dart @@ -343,37 +343,4 @@ class ServerInfoApi { } return null; } - - /// Performs an HTTP 'POST /server-info/admin-onboarding' operation and returns the [Response]. - Future setAdminOnboardingWithHttpInfo() async { - // ignore: prefer_const_declarations - final path = r'/server-info/admin-onboarding'; - - // ignore: prefer_final_locals - Object? postBody; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - const contentTypes = []; - - - return apiClient.invokeAPI( - path, - 'POST', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - Future setAdminOnboarding() async { - final response = await setAdminOnboardingWithHttpInfo(); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - } } diff --git a/mobile/openapi/lib/api/system_metadata_api.dart b/mobile/openapi/lib/api/system_metadata_api.dart new file mode 100644 index 0000000000..f3952fda8a --- /dev/null +++ b/mobile/openapi/lib/api/system_metadata_api.dart @@ -0,0 +1,139 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + + +class SystemMetadataApi { + SystemMetadataApi([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient; + + final ApiClient apiClient; + + /// Performs an HTTP 'GET /system-metadata/admin-onboarding' operation and returns the [Response]. + Future getAdminOnboardingWithHttpInfo() async { + // ignore: prefer_const_declarations + final path = r'/system-metadata/admin-onboarding'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + path, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + Future getAdminOnboarding() async { + final response = await getAdminOnboardingWithHttpInfo(); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'AdminOnboardingUpdateDto',) as AdminOnboardingUpdateDto; + + } + return null; + } + + /// Performs an HTTP 'GET /system-metadata/reverse-geocoding-state' operation and returns the [Response]. + Future getReverseGeocodingStateWithHttpInfo() async { + // ignore: prefer_const_declarations + final path = r'/system-metadata/reverse-geocoding-state'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + path, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + Future getReverseGeocodingState() async { + final response = await getReverseGeocodingStateWithHttpInfo(); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'ReverseGeocodingStateResponseDto',) as ReverseGeocodingStateResponseDto; + + } + return null; + } + + /// Performs an HTTP 'POST /system-metadata/admin-onboarding' operation and returns the [Response]. + /// Parameters: + /// + /// * [AdminOnboardingUpdateDto] adminOnboardingUpdateDto (required): + Future updateAdminOnboardingWithHttpInfo(AdminOnboardingUpdateDto adminOnboardingUpdateDto,) async { + // ignore: prefer_const_declarations + final path = r'/system-metadata/admin-onboarding'; + + // ignore: prefer_final_locals + Object? postBody = adminOnboardingUpdateDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + path, + 'POST', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [AdminOnboardingUpdateDto] adminOnboardingUpdateDto (required): + Future updateAdminOnboarding(AdminOnboardingUpdateDto adminOnboardingUpdateDto,) async { + final response = await updateAdminOnboardingWithHttpInfo(adminOnboardingUpdateDto,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + } +} diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 0a0cd80088..a92f1df7a7 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -198,6 +198,8 @@ class ApiClient { return ActivityStatisticsResponseDto.fromJson(value); case 'AddUsersDto': return AddUsersDto.fromJson(value); + case 'AdminOnboardingUpdateDto': + return AdminOnboardingUpdateDto.fromJson(value); case 'AlbumCountResponseDto': return AlbumCountResponseDto.fromJson(value); case 'AlbumResponseDto': @@ -392,6 +394,8 @@ class ApiClient { return ReactionTypeTypeTransformer().decode(value); case 'RecognitionConfig': return RecognitionConfig.fromJson(value); + case 'ReverseGeocodingStateResponseDto': + return ReverseGeocodingStateResponseDto.fromJson(value); case 'ScanLibraryDto': return ScanLibraryDto.fromJson(value); case 'SearchAlbumResponseDto': diff --git a/mobile/openapi/lib/model/admin_onboarding_update_dto.dart b/mobile/openapi/lib/model/admin_onboarding_update_dto.dart new file mode 100644 index 0000000000..50c4ae090e --- /dev/null +++ b/mobile/openapi/lib/model/admin_onboarding_update_dto.dart @@ -0,0 +1,98 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class AdminOnboardingUpdateDto { + /// Returns a new [AdminOnboardingUpdateDto] instance. + AdminOnboardingUpdateDto({ + required this.isOnboarded, + }); + + bool isOnboarded; + + @override + bool operator ==(Object other) => identical(this, other) || other is AdminOnboardingUpdateDto && + other.isOnboarded == isOnboarded; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (isOnboarded.hashCode); + + @override + String toString() => 'AdminOnboardingUpdateDto[isOnboarded=$isOnboarded]'; + + Map toJson() { + final json = {}; + json[r'isOnboarded'] = this.isOnboarded; + return json; + } + + /// Returns a new [AdminOnboardingUpdateDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static AdminOnboardingUpdateDto? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return AdminOnboardingUpdateDto( + isOnboarded: mapValueOfType(json, r'isOnboarded')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = AdminOnboardingUpdateDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = AdminOnboardingUpdateDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of AdminOnboardingUpdateDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = AdminOnboardingUpdateDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'isOnboarded', + }; +} + diff --git a/mobile/openapi/lib/model/reverse_geocoding_state_response_dto.dart b/mobile/openapi/lib/model/reverse_geocoding_state_response_dto.dart new file mode 100644 index 0000000000..71e1d3ad99 --- /dev/null +++ b/mobile/openapi/lib/model/reverse_geocoding_state_response_dto.dart @@ -0,0 +1,114 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class ReverseGeocodingStateResponseDto { + /// Returns a new [ReverseGeocodingStateResponseDto] instance. + ReverseGeocodingStateResponseDto({ + required this.lastImportFileName, + required this.lastUpdate, + }); + + String? lastImportFileName; + + String? lastUpdate; + + @override + bool operator ==(Object other) => identical(this, other) || other is ReverseGeocodingStateResponseDto && + other.lastImportFileName == lastImportFileName && + other.lastUpdate == lastUpdate; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (lastImportFileName == null ? 0 : lastImportFileName!.hashCode) + + (lastUpdate == null ? 0 : lastUpdate!.hashCode); + + @override + String toString() => 'ReverseGeocodingStateResponseDto[lastImportFileName=$lastImportFileName, lastUpdate=$lastUpdate]'; + + Map toJson() { + final json = {}; + if (this.lastImportFileName != null) { + json[r'lastImportFileName'] = this.lastImportFileName; + } else { + // json[r'lastImportFileName'] = null; + } + if (this.lastUpdate != null) { + json[r'lastUpdate'] = this.lastUpdate; + } else { + // json[r'lastUpdate'] = null; + } + return json; + } + + /// Returns a new [ReverseGeocodingStateResponseDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static ReverseGeocodingStateResponseDto? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return ReverseGeocodingStateResponseDto( + lastImportFileName: mapValueOfType(json, r'lastImportFileName'), + lastUpdate: mapValueOfType(json, r'lastUpdate'), + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = ReverseGeocodingStateResponseDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = ReverseGeocodingStateResponseDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of ReverseGeocodingStateResponseDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = ReverseGeocodingStateResponseDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'lastImportFileName', + 'lastUpdate', + }; +} + diff --git a/mobile/openapi/test/admin_onboarding_update_dto_test.dart b/mobile/openapi/test/admin_onboarding_update_dto_test.dart new file mode 100644 index 0000000000..09cc73e977 --- /dev/null +++ b/mobile/openapi/test/admin_onboarding_update_dto_test.dart @@ -0,0 +1,27 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +import 'package:openapi/api.dart'; +import 'package:test/test.dart'; + +// tests for AdminOnboardingUpdateDto +void main() { + // final instance = AdminOnboardingUpdateDto(); + + group('test AdminOnboardingUpdateDto', () { + // bool isOnboarded + test('to test the property `isOnboarded`', () async { + // TODO + }); + + + }); + +} diff --git a/mobile/openapi/test/reverse_geocoding_state_response_dto_test.dart b/mobile/openapi/test/reverse_geocoding_state_response_dto_test.dart new file mode 100644 index 0000000000..91fdfcfea4 --- /dev/null +++ b/mobile/openapi/test/reverse_geocoding_state_response_dto_test.dart @@ -0,0 +1,32 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +import 'package:openapi/api.dart'; +import 'package:test/test.dart'; + +// tests for ReverseGeocodingStateResponseDto +void main() { + // final instance = ReverseGeocodingStateResponseDto(); + + group('test ReverseGeocodingStateResponseDto', () { + // String lastImportFileName + test('to test the property `lastImportFileName`', () async { + // TODO + }); + + // String lastUpdate + test('to test the property `lastUpdate`', () async { + // TODO + }); + + + }); + +} diff --git a/mobile/openapi/test/server_info_api_test.dart b/mobile/openapi/test/server_info_api_test.dart index 68cd1c348b..dac465116e 100644 --- a/mobile/openapi/test/server_info_api_test.dart +++ b/mobile/openapi/test/server_info_api_test.dart @@ -57,10 +57,5 @@ void main() { // TODO }); - //Future setAdminOnboarding() async - test('test setAdminOnboarding', () async { - // TODO - }); - }); } diff --git a/mobile/openapi/test/system_metadata_api_test.dart b/mobile/openapi/test/system_metadata_api_test.dart new file mode 100644 index 0000000000..bc1ce6f6f3 --- /dev/null +++ b/mobile/openapi/test/system_metadata_api_test.dart @@ -0,0 +1,36 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +import 'package:openapi/api.dart'; +import 'package:test/test.dart'; + + +/// tests for SystemMetadataApi +void main() { + // final instance = SystemMetadataApi(); + + group('tests for SystemMetadataApi', () { + //Future getAdminOnboarding() async + test('test getAdminOnboarding', () async { + // TODO + }); + + //Future getReverseGeocodingState() async + test('test getReverseGeocodingState', () async { + // TODO + }); + + //Future updateAdminOnboarding(AdminOnboardingUpdateDto adminOnboardingUpdateDto) async + test('test updateAdminOnboarding', () async { + // TODO + }); + + }); +} diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index f49df7baea..4f666b303c 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -4908,31 +4908,6 @@ ] } }, - "/server-info/admin-onboarding": { - "post": { - "operationId": "setAdminOnboarding", - "parameters": [], - "responses": { - "204": { - "description": "" - } - }, - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ], - "tags": [ - "Server Info" - ] - } - }, "/server-info/config": { "get": { "operationId": "getServerConfig", @@ -5885,6 +5860,103 @@ ] } }, + "/system-metadata/admin-onboarding": { + "get": { + "operationId": "getAdminOnboarding", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AdminOnboardingUpdateDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "System Metadata" + ] + }, + "post": { + "operationId": "updateAdminOnboarding", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AdminOnboardingUpdateDto" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "System Metadata" + ] + } + }, + "/system-metadata/reverse-geocoding-state": { + "get": { + "operationId": "getReverseGeocodingState", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReverseGeocodingStateResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "System Metadata" + ] + } + }, "/tag": { "get": { "operationId": "getAllTags", @@ -7180,6 +7252,17 @@ ], "type": "object" }, + "AdminOnboardingUpdateDto": { + "properties": { + "isOnboarded": { + "type": "boolean" + } + }, + "required": [ + "isOnboarded" + ], + "type": "object" + }, "AlbumCountResponseDto": { "properties": { "notShared": { @@ -9618,6 +9701,23 @@ ], "type": "object" }, + "ReverseGeocodingStateResponseDto": { + "properties": { + "lastImportFileName": { + "nullable": true, + "type": "string" + }, + "lastUpdate": { + "nullable": true, + "type": "string" + } + }, + "required": [ + "lastImportFileName", + "lastUpdate" + ], + "type": "object" + }, "ScanLibraryDto": { "properties": { "refreshAllFiles": { diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 9148b4d3b1..1bf219162d 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -998,6 +998,13 @@ export type SystemConfigTemplateStorageOptionDto = { weekOptions: string[]; yearOptions: string[]; }; +export type AdminOnboardingUpdateDto = { + isOnboarded: boolean; +}; +export type ReverseGeocodingStateResponseDto = { + lastImportFileName: string | null; + lastUpdate: string | null; +}; export type CreateTagDto = { name: string; "type": TagTypeEnum; @@ -2330,12 +2337,6 @@ export function getServerInfo(opts?: Oazapfts.RequestOpts) { ...opts })); } -export function setAdminOnboarding(opts?: Oazapfts.RequestOpts) { - return oazapfts.ok(oazapfts.fetchText("/server-info/admin-onboarding", { - ...opts, - method: "POST" - })); -} export function getServerConfig(opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; @@ -2597,6 +2598,31 @@ export function getStorageTemplateOptions(opts?: Oazapfts.RequestOpts) { ...opts })); } +export function getAdminOnboarding(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: AdminOnboardingUpdateDto; + }>("/system-metadata/admin-onboarding", { + ...opts + })); +} +export function updateAdminOnboarding({ adminOnboardingUpdateDto }: { + adminOnboardingUpdateDto: AdminOnboardingUpdateDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText("/system-metadata/admin-onboarding", oazapfts.json({ + ...opts, + method: "POST", + body: adminOnboardingUpdateDto + }))); +} +export function getReverseGeocodingState(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: ReverseGeocodingStateResponseDto; + }>("/system-metadata/reverse-geocoding-state", { + ...opts + })); +} export function getAllTags(opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; diff --git a/server/src/controllers/index.ts b/server/src/controllers/index.ts index ad2f6e8de1..bd10c41a43 100644 --- a/server/src/controllers/index.ts +++ b/server/src/controllers/index.ts @@ -21,6 +21,7 @@ import { SessionController } from 'src/controllers/session.controller'; import { SharedLinkController } from 'src/controllers/shared-link.controller'; import { SyncController } from 'src/controllers/sync.controller'; import { SystemConfigController } from 'src/controllers/system-config.controller'; +import { SystemMetadataController } from 'src/controllers/system-metadata.controller'; import { TagController } from 'src/controllers/tag.controller'; import { TimelineController } from 'src/controllers/timeline.controller'; import { TrashController } from 'src/controllers/trash.controller'; @@ -51,6 +52,7 @@ export const controllers = [ SharedLinkController, SyncController, SystemConfigController, + SystemMetadataController, TagController, TimelineController, TrashController, diff --git a/server/src/controllers/server-info.controller.ts b/server/src/controllers/server-info.controller.ts index e32b0d191c..35e5e17594 100644 --- a/server/src/controllers/server-info.controller.ts +++ b/server/src/controllers/server-info.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, HttpCode, HttpStatus, Post } from '@nestjs/common'; +import { Controller, Get } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { ServerConfigDto, @@ -65,11 +65,4 @@ export class ServerInfoController { getSupportedMediaTypes(): ServerMediaTypesResponseDto { return this.service.getSupportedMediaTypes(); } - - @AdminRoute() - @Post('admin-onboarding') - @HttpCode(HttpStatus.NO_CONTENT) - setAdminOnboarding(): Promise { - return this.service.setAdminOnboarding(); - } } diff --git a/server/src/controllers/system-metadata.controller.ts b/server/src/controllers/system-metadata.controller.ts new file mode 100644 index 0000000000..7f186fec03 --- /dev/null +++ b/server/src/controllers/system-metadata.controller.ts @@ -0,0 +1,28 @@ +import { Body, Controller, Get, HttpCode, HttpStatus, Post } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { AdminOnboardingUpdateDto, ReverseGeocodingStateResponseDto } from 'src/dtos/system-metadata.dto'; +import { Authenticated } from 'src/middleware/auth.guard'; +import { SystemMetadataService } from 'src/services/system-metadata.service'; + +@ApiTags('System Metadata') +@Controller('system-metadata') +@Authenticated({ admin: true }) +export class SystemMetadataController { + constructor(private service: SystemMetadataService) {} + + @Get('admin-onboarding') + getAdminOnboarding(): Promise { + return this.service.getAdminOnboarding(); + } + + @Post('admin-onboarding') + @HttpCode(HttpStatus.NO_CONTENT) + updateAdminOnboarding(@Body() dto: AdminOnboardingUpdateDto): Promise { + return this.service.updateAdminOnboarding(dto); + } + + @Get('reverse-geocoding-state') + getReverseGeocodingState(): Promise { + return this.service.getReverseGeocodingState(); + } +} diff --git a/server/src/dtos/system-metadata.dto.ts b/server/src/dtos/system-metadata.dto.ts new file mode 100644 index 0000000000..1c04435341 --- /dev/null +++ b/server/src/dtos/system-metadata.dto.ts @@ -0,0 +1,15 @@ +import { IsBoolean } from 'class-validator'; + +export class AdminOnboardingUpdateDto { + @IsBoolean() + isOnboarded!: boolean; +} + +export class AdminOnboardingResponseDto { + isOnboarded!: boolean; +} + +export class ReverseGeocodingStateResponseDto { + lastUpdate!: string | null; + lastImportFileName!: string | null; +} diff --git a/server/src/repositories/metadata.repository.ts b/server/src/repositories/metadata.repository.ts index 8eeb0064ac..e7d37407d9 100644 --- a/server/src/repositories/metadata.repository.ts +++ b/server/src/repositories/metadata.repository.ts @@ -36,8 +36,8 @@ export class MetadataRepository implements IMetadataRepository { this.logger.log('Initializing metadata repository'); const geodataDate = await readFile(geodataDatePath, 'utf8'); + // TODO move to metadata service init const geocodingMetadata = await this.systemMetadataRepository.get(SystemMetadataKey.REVERSE_GEOCODING_STATE); - if (geocodingMetadata?.lastUpdate === geodataDate) { return; } diff --git a/server/src/services/index.ts b/server/src/services/index.ts index db3d6083e9..2305708caa 100644 --- a/server/src/services/index.ts +++ b/server/src/services/index.ts @@ -25,6 +25,7 @@ import { StorageTemplateService } from 'src/services/storage-template.service'; import { StorageService } from 'src/services/storage.service'; import { SyncService } from 'src/services/sync.service'; import { SystemConfigService } from 'src/services/system-config.service'; +import { SystemMetadataService } from 'src/services/system-metadata.service'; import { TagService } from 'src/services/tag.service'; import { TimelineService } from 'src/services/timeline.service'; import { TrashService } from 'src/services/trash.service'; @@ -58,6 +59,7 @@ export const services = [ StorageTemplateService, SyncService, SystemConfigService, + SystemMetadataService, TagService, TimelineService, TrashService, diff --git a/server/src/services/server-info.service.spec.ts b/server/src/services/server-info.service.spec.ts index 836909b74f..115ab4b6a1 100644 --- a/server/src/services/server-info.service.spec.ts +++ b/server/src/services/server-info.service.spec.ts @@ -1,5 +1,4 @@ import { serverVersion } from 'src/constants'; -import { SystemMetadataKey } from 'src/entities/system-metadata.entity'; import { IEventRepository } from 'src/interfaces/event.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IServerInfoRepository } from 'src/interfaces/server-info.interface'; @@ -207,13 +206,6 @@ describe(ServerInfoService.name, () => { }); }); - describe('setAdminOnboarding', () => { - it('should set admin onboarding to true', async () => { - await sut.setAdminOnboarding(); - expect(systemMetadataMock.set).toHaveBeenCalledWith(SystemMetadataKey.ADMIN_ONBOARDING, { isOnboarded: true }); - }); - }); - describe('getStats', () => { it('should total up usage by user', async () => { userMock.getUserStats.mockResolvedValue([ diff --git a/server/src/services/server-info.service.ts b/server/src/services/server-info.service.ts index bb092896bf..52bf8bd1d3 100644 --- a/server/src/services/server-info.service.ts +++ b/server/src/services/server-info.service.ts @@ -51,7 +51,9 @@ export class ServerInfoService { const featureFlags = await this.getFeatures(); if (featureFlags.configFile) { - await this.setAdminOnboarding(); + await this.systemMetadataRepository.set(SystemMetadataKey.ADMIN_ONBOARDING, { + isOnboarded: true, + }); } } @@ -105,10 +107,6 @@ export class ServerInfoService { }; } - setAdminOnboarding(): Promise { - return this.systemMetadataRepository.set(SystemMetadataKey.ADMIN_ONBOARDING, { isOnboarded: true }); - } - async getStatistics(): Promise { const userStats: UserStatsQueryResponse[] = await this.userRepository.getUserStats(); const serverStats = new ServerStatsResponseDto(); diff --git a/server/src/services/system-metadata.service.spec.ts b/server/src/services/system-metadata.service.spec.ts new file mode 100644 index 0000000000..9d11c1c72a --- /dev/null +++ b/server/src/services/system-metadata.service.spec.ts @@ -0,0 +1,31 @@ +import { SystemMetadataKey } from 'src/entities/system-metadata.entity'; +import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; +import { SystemMetadataService } from 'src/services/system-metadata.service'; +import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock'; +import { Mocked } from 'vitest'; + +describe(SystemMetadataService.name, () => { + let sut: SystemMetadataService; + let metadataMock: Mocked; + + beforeEach(() => { + metadataMock = newSystemMetadataRepositoryMock(); + sut = new SystemMetadataService(metadataMock); + }); + + it('should work', () => { + expect(sut).toBeDefined(); + }); + + describe('updateAdminOnboarding', () => { + it('should update isOnboarded to true', async () => { + await expect(sut.updateAdminOnboarding({ isOnboarded: true })).resolves.toBeUndefined(); + expect(metadataMock.set).toHaveBeenCalledWith(SystemMetadataKey.ADMIN_ONBOARDING, { isOnboarded: true }); + }); + + it('should update isOnboarded to false', async () => { + await expect(sut.updateAdminOnboarding({ isOnboarded: false })).resolves.toBeUndefined(); + expect(metadataMock.set).toHaveBeenCalledWith(SystemMetadataKey.ADMIN_ONBOARDING, { isOnboarded: false }); + }); + }); +}); diff --git a/server/src/services/system-metadata.service.ts b/server/src/services/system-metadata.service.ts new file mode 100644 index 0000000000..e8fddfc13c --- /dev/null +++ b/server/src/services/system-metadata.service.ts @@ -0,0 +1,29 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { + AdminOnboardingResponseDto, + AdminOnboardingUpdateDto, + ReverseGeocodingStateResponseDto, +} from 'src/dtos/system-metadata.dto'; +import { SystemMetadataKey } from 'src/entities/system-metadata.entity'; +import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; + +@Injectable() +export class SystemMetadataService { + constructor(@Inject(ISystemMetadataRepository) private repository: ISystemMetadataRepository) {} + + async getAdminOnboarding(): Promise { + const value = await this.repository.get(SystemMetadataKey.ADMIN_ONBOARDING); + return { isOnboarded: false, ...value }; + } + + async updateAdminOnboarding(dto: AdminOnboardingUpdateDto): Promise { + await this.repository.set(SystemMetadataKey.ADMIN_ONBOARDING, { + isOnboarded: dto.isOnboarded, + }); + } + + async getReverseGeocodingState(): Promise { + const value = await this.repository.get(SystemMetadataKey.REVERSE_GEOCODING_STATE); + return { lastUpdate: null, lastImportFileName: null, ...value }; + } +} diff --git a/web/src/routes/auth/onboarding/+page.svelte b/web/src/routes/auth/onboarding/+page.svelte index 09139a7f7e..4647ad8bde 100644 --- a/web/src/routes/auth/onboarding/+page.svelte +++ b/web/src/routes/auth/onboarding/+page.svelte @@ -5,7 +5,7 @@ import OnboadingStorageTemplate from '$lib/components/onboarding-page/onboarding-storage-template.svelte'; import OnboardingTheme from '$lib/components/onboarding-page/onboarding-theme.svelte'; import { AppRoute, QueryParameter } from '$lib/constants'; - import { setAdminOnboarding } from '@immich/sdk'; + import { updateAdminOnboarding } from '@immich/sdk'; let index = 0; @@ -28,7 +28,7 @@ const handleDoneClicked = async () => { if (index >= onboardingSteps.length - 1) { - await setAdminOnboarding(); + await updateAdminOnboarding({ adminOnboardingUpdateDto: { isOnboarded: true } }); await goto(AppRoute.PHOTOS); } else { index++; From 3abfe3c99e26fa59427c5710802f7a9ba798dc5f Mon Sep 17 00:00:00 2001 From: Conner <46903591+ConnerWithAnE@users.noreply.github.com> Date: Fri, 19 Apr 2024 19:19:50 -0600 Subject: [PATCH 07/27] fix(web): restore button in asset viewer (#8935) * fix(web): restore button added to trashed asset-view to restore single item * fixed the asset-viewer menu to update upon restoration * prettier formatting complete, testing passed * chore: clean up --------- Co-authored-by: Jason Rasmussen --- .../asset-viewer/asset-viewer-nav-bar.svelte | 19 +++++++++++++------ .../asset-viewer/asset-viewer.svelte | 18 ++++++++++++++++++ .../components/photos-page/asset-grid.svelte | 1 + web/src/lib/constants.ts | 2 +- 4 files changed, 33 insertions(+), 7 deletions(-) diff --git a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte index abcb248f1c..6772ff5db0 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte @@ -20,6 +20,7 @@ mdiFolderDownloadOutline, mdiHeart, mdiHeartOutline, + mdiHistory, mdiImageAlbum, mdiImageMinusOutline, mdiImageOutline, @@ -52,6 +53,7 @@ type MenuItemEvent = | 'addToAlbum' + | 'restoreAsset' | 'addToSharedAlbum' | 'asProfileImage' | 'setAsAlbumCover' @@ -70,6 +72,7 @@ delete: void; toggleArchive: void; addToAlbum: void; + restoreAsset: void; addToSharedAlbum: void; asProfileImage: void; setAsAlbumCover: void; @@ -208,12 +211,16 @@ {#if showDownloadButton} onMenuClick('download')} text="Download" /> {/if} - onMenuClick('addToAlbum')} text="Add to album" /> - onMenuClick('addToSharedAlbum')} - text="Add to shared album" - /> + {#if asset.isTrashed} + onMenuClick('restoreAsset')} text="Restore" /> + {:else} + onMenuClick('addToAlbum')} text="Add to album" /> + onMenuClick('addToSharedAlbum')} + text="Add to shared album" + /> + {/if} {#if isOwner} {#if hasStackChildren} diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index 46c95636d0..40309e511f 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -27,6 +27,7 @@ getActivityStatistics, getAllAlbums, runAssetJobs, + restoreAssets, updateAsset, updateAlbumInfo, type ActivityResponseDto, @@ -403,6 +404,22 @@ await handleGetAllAlbums(); }; + const handleRestoreAsset = async () => { + try { + await restoreAssets({ bulkIdsDto: { ids: [asset.id] } }); + asset.isTrashed = false; + + dispatch('action', { type: AssetAction.RESTORE, asset }); + + notificationController.show({ + type: NotificationType.Info, + message: `Restored asset`, + }); + } catch (error) { + handleError(error, 'Error restoring asset'); + } + }; + const toggleArchive = async () => { try { const data = await updateAsset({ @@ -556,6 +573,7 @@ on:delete={() => trashOrDelete()} on:favorite={toggleFavorite} on:addToAlbum={() => openAlbumPicker(false)} + on:restoreAsset={() => handleRestoreAsset()} on:addToSharedAlbum={() => openAlbumPicker(true)} on:playMotionPhoto={() => (shouldPlayMotionPhoto = true)} on:stopMotionPhoto={() => (shouldPlayMotionPhoto = false)} diff --git a/web/src/lib/components/photos-page/asset-grid.svelte b/web/src/lib/components/photos-page/asset-grid.svelte index a3f4c51563..8cfb0b8b16 100644 --- a/web/src/lib/components/photos-page/asset-grid.svelte +++ b/web/src/lib/components/photos-page/asset-grid.svelte @@ -169,6 +169,7 @@ switch (action) { case removeAction: case AssetAction.TRASH: + case AssetAction.RESTORE: case AssetAction.DELETE: { // find the next asset to show or close the viewer (await handleNext()) || (await handlePrevious()) || handleClose(); diff --git a/web/src/lib/constants.ts b/web/src/lib/constants.ts index af5558c261..98d6d742d2 100644 --- a/web/src/lib/constants.ts +++ b/web/src/lib/constants.ts @@ -5,7 +5,7 @@ export enum AssetAction { UNFAVORITE = 'unfavorite', TRASH = 'trash', DELETE = 'delete', - // RESTORE = 'restore', + RESTORE = 'restore', ADD = 'add', } From 71b6d8b569038e1676316f61845c91cbac58c19c Mon Sep 17 00:00:00 2001 From: devjn Date: Sat, 20 Apr 2024 16:39:04 +0300 Subject: [PATCH 08/27] feat(android) Check server is reachable before starting background backup (#8594) * Bump androidx work version to 2.9.0 * Check that server is reachable before starting backup work * Dart format * Cleanup debug logs * Fix analysis --- mobile/android/app/build.gradle | 2 +- .../example/mobile/BackgroundServicePlugin.kt | 1 + .../kotlin/com/example/mobile/BackupWorker.kt | 136 ++++++++++++++---- mobile/android/build.gradle | 4 +- .../background.service.dart | 7 +- 5 files changed, 116 insertions(+), 34 deletions(-) diff --git a/mobile/android/app/build.gradle b/mobile/android/app/build.gradle index 96d2db23f5..a6f86b8537 100644 --- a/mobile/android/app/build.gradle +++ b/mobile/android/app/build.gradle @@ -90,7 +90,7 @@ flutter { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version" - implementation "androidx.work:work-runtime-ktx:$work_version" + implementation "androidx.work:work-runtime:$work_version" implementation "androidx.concurrent:concurrent-futures:$concurrent_version" implementation "com.google.guava:guava:$guava_version" implementation "com.github.bumptech.glide:glide:$glide_version" diff --git a/mobile/android/app/src/main/kotlin/com/example/mobile/BackgroundServicePlugin.kt b/mobile/android/app/src/main/kotlin/com/example/mobile/BackgroundServicePlugin.kt index 6541ad5755..1d23c5665c 100644 --- a/mobile/android/app/src/main/kotlin/com/example/mobile/BackgroundServicePlugin.kt +++ b/mobile/android/app/src/main/kotlin/com/example/mobile/BackgroundServicePlugin.kt @@ -52,6 +52,7 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler { .putBoolean(ContentObserverWorker.SHARED_PREF_SERVICE_ENABLED, true) .putLong(BackupWorker.SHARED_PREF_CALLBACK_KEY, args.get(0) as Long) .putString(BackupWorker.SHARED_PREF_NOTIFICATION_TITLE, args.get(1) as String) + .putString(BackupWorker.SHARED_PREF_SERVER_URL, args.get(3) as String) .apply() ContentObserverWorker.enable(ctx, immediate = args.get(2) as Boolean) result.success(true) diff --git a/mobile/android/app/src/main/kotlin/com/example/mobile/BackupWorker.kt b/mobile/android/app/src/main/kotlin/com/example/mobile/BackupWorker.kt index 660e1d55ba..dc7c4a9c37 100644 --- a/mobile/android/app/src/main/kotlin/com/example/mobile/BackupWorker.kt +++ b/mobile/android/app/src/main/kotlin/com/example/mobile/BackupWorker.kt @@ -11,8 +11,8 @@ import android.os.PowerManager import android.os.SystemClock import android.util.Log import androidx.annotation.RequiresApi +import androidx.concurrent.futures.CallbackToFutureAdapter import androidx.core.app.NotificationCompat -import androidx.concurrent.futures.ResolvableFuture import androidx.work.BackoffPolicy import androidx.work.Constraints import androidx.work.ForegroundInfo @@ -30,6 +30,16 @@ import io.flutter.embedding.engine.loader.FlutterLoader import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.view.FlutterCallbackInformation +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeoutOrNull +import java.io.IOException +import java.net.HttpURLConnection +import java.net.InetAddress +import java.net.URL import java.util.concurrent.TimeUnit /** @@ -42,7 +52,6 @@ import java.util.concurrent.TimeUnit */ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ctx, params), MethodChannel.MethodCallHandler { - private val resolvableFuture = ResolvableFuture.create() private var engine: FlutterEngine? = null private lateinit var backgroundChannel: MethodChannel private val notificationManager = ctx.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager @@ -52,37 +61,82 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct private var notificationDetailBuilder: NotificationCompat.Builder? = null private var fgFuture: ListenableFuture? = null - override fun startWork(): ListenableFuture { + private val job = Job() + private lateinit var completer: CallbackToFutureAdapter.Completer + private val resolvableFuture = CallbackToFutureAdapter.getFuture { completer -> + this.completer = completer + null + } + init { + resolvableFuture.addListener( + Runnable { + if (resolvableFuture.isCancelled) { + job.cancel() + } + }, + taskExecutor.serialTaskExecutor + ) + } + + override fun startWork(): ListenableFuture { Log.d(TAG, "startWork") val ctx = applicationContext + val prefs = ctx.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE) - if (!flutterLoader.initialized()) { - flutterLoader.startInitialization(ctx) - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - // Create a Notification channel if necessary - createChannel() - } - if (isIgnoringBatteryOptimizations) { - // normal background services can only up to 10 minutes - // foreground services are allowed to run indefinitely - // requires battery optimizations to be disabled (either manually by the user - // or by the system learning that immich is important to the user) - val title = ctx.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE) - .getString(SHARED_PREF_NOTIFICATION_TITLE, NOTIFICATION_DEFAULT_TITLE)!! - showInfo(getInfoBuilder(title, indeterminate=true).build()) - } - engine = FlutterEngine(ctx) - - flutterLoader.ensureInitializationCompleteAsync(ctx, null, Handler(Looper.getMainLooper())) { - runDart() - } - + prefs.getString(SHARED_PREF_SERVER_URL, null) + ?.takeIf { it.isNotEmpty() } + ?.let { serverUrl -> doCoroutineWork(serverUrl) } + ?: doWork() return resolvableFuture } + /** + * This function is used to check if server URL is reachable before starting the backup work. + * Check must be done in a background to avoid blocking the main thread. + */ + private fun doCoroutineWork(serverUrl : String) { + CoroutineScope(Dispatchers.Default + job).launch { + val isReachable = isUrlReachableHttp(serverUrl) + withContext(Dispatchers.Main) { + if (isReachable) { + doWork() + } else { + // Fail when the URL is not reachable + completer.set(Result.failure()) + } + } + } + } + + private fun doWork() { + Log.d(TAG, "doWork") + val ctx = applicationContext + + if (!flutterLoader.initialized()) { + flutterLoader.startInitialization(ctx) + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + // Create a Notification channel if necessary + createChannel() + } + if (isIgnoringBatteryOptimizations) { + // normal background services can only up to 10 minutes + // foreground services are allowed to run indefinitely + // requires battery optimizations to be disabled (either manually by the user + // or by the system learning that immich is important to the user) + val title = ctx.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE) + .getString(SHARED_PREF_NOTIFICATION_TITLE, NOTIFICATION_DEFAULT_TITLE)!! + showInfo(getInfoBuilder(title, indeterminate=true).build()) + } + engine = FlutterEngine(ctx) + + flutterLoader.ensureInitializationCompleteAsync(ctx, null, Handler(Looper.getMainLooper())) { + runDart() + } + } + /** * Starts the Dart runtime/engine and calls `_nativeEntry` function in * `background.service.dart` to run the actual backup logic. @@ -139,7 +193,7 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct engine = null if (result != null) { Log.d(TAG, "stopEngine result=${result}") - resolvableFuture.set(result) + this.completer.set(result) } waitOnSetForegroundAsync() } @@ -270,13 +324,14 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct const val SHARED_PREF_CALLBACK_KEY = "callbackDispatcherHandle" const val SHARED_PREF_NOTIFICATION_TITLE = "notificationTitle" const val SHARED_PREF_LAST_CHANGE = "lastChange" + const val SHARED_PREF_SERVER_URL = "serverUrl" private const val TASK_NAME_BACKUP = "immich/BackupWorker" private const val NOTIFICATION_CHANNEL_ID = "immich/backgroundService" private const val NOTIFICATION_CHANNEL_ERROR_ID = "immich/backgroundServiceError" private const val NOTIFICATION_DEFAULT_TITLE = "Immich" private const val NOTIFICATION_ID = 1 - private const val NOTIFICATION_ERROR_ID = 2 + private const val NOTIFICATION_ERROR_ID = 2 private const val NOTIFICATION_DETAIL_ID = 3 private const val ONE_MINUTE = 60000L @@ -304,7 +359,7 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct val workInfoList = workInfoFuture.get(1000, TimeUnit.MILLISECONDS) if (workInfoList != null) { for (workInfo in workInfoList) { - if (workInfo.getState() == WorkInfo.State.ENQUEUED) { + if (workInfo.state == WorkInfo.State.ENQUEUED) { val workRequest = buildWorkRequest(requireWifi, requireCharging) wm.enqueueUniqueWork(TASK_NAME_BACKUP, ExistingWorkPolicy.REPLACE, workRequest) Log.d(TAG, "updateBackupWorker updated BackupWorker constraints") @@ -359,4 +414,27 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct } } -private const val TAG = "BackupWorker" \ No newline at end of file +private const val TAG = "BackupWorker" + +/** + * Check if the given URL is reachable via HTTP + */ +suspend fun isUrlReachableHttp(url: String, timeoutMillis: Long = 5000L): Boolean { + return withTimeoutOrNull(timeoutMillis) { + var httpURLConnection: HttpURLConnection? = null + try { + httpURLConnection = (URL(url).openConnection() as HttpURLConnection).apply { + requestMethod = "HEAD" + connectTimeout = timeoutMillis.toInt() + readTimeout = timeoutMillis.toInt() + } + httpURLConnection.connect() + httpURLConnection.responseCode == HttpURLConnection.HTTP_OK + } catch (e: Exception) { + Log.e(TAG, "Failed to reach server URL: $e") + false + } finally { + httpURLConnection?.disconnect() + } + } == true +} diff --git a/mobile/android/build.gradle b/mobile/android/build.gradle index 4dacde5a9d..e9c271b2c5 100644 --- a/mobile/android/build.gradle +++ b/mobile/android/build.gradle @@ -1,7 +1,7 @@ buildscript { - ext.kotlin_version = '1.8.20' + ext.kotlin_version = '1.8.22' ext.kotlin_coroutines_version = '1.7.1' - ext.work_version = '2.7.1' + ext.work_version = '2.9.0' ext.concurrent_version = '1.1.0' ext.guava_version = '33.0.0-android' ext.glide_version = '4.14.2' diff --git a/mobile/lib/modules/backup/background_service/background.service.dart b/mobile/lib/modules/backup/background_service/background.service.dart index cbee121105..8358043894 100644 --- a/mobile/lib/modules/backup/background_service/background.service.dart +++ b/mobile/lib/modules/backup/background_service/background.service.dart @@ -20,6 +20,7 @@ import 'package:immich_mobile/shared/models/store.dart'; import 'package:immich_mobile/shared/services/api.service.dart'; import 'package:immich_mobile/utils/backup_progress.dart'; import 'package:immich_mobile/utils/diff.dart'; +import 'package:immich_mobile/utils/url_helper.dart'; import 'package:isar/isar.dart'; import 'package:path_provider_ios/path_provider_ios.dart'; import 'package:photo_manager/photo_manager.dart'; @@ -68,8 +69,10 @@ class BackgroundService { final callback = PluginUtilities.getCallbackHandle(_nativeEntry)!; final String title = "backup_background_service_default_notification".tr(); - final bool ok = await _foregroundChannel - .invokeMethod('enable', [callback.toRawHandle(), title, immediate]); + final bool ok = await _foregroundChannel.invokeMethod( + 'enable', + [callback.toRawHandle(), title, immediate, getServerUrl()], + ); return ok; } catch (error) { return false; From 6eb1b825412207729b9c60a8af6d29f6ff41ae8b Mon Sep 17 00:00:00 2001 From: Alex The Bot Date: Sat, 20 Apr 2024 13:43:46 +0000 Subject: [PATCH 09/27] Version v1.102.1 --- cli/package-lock.json | 2 +- e2e/package-lock.json | 6 +++--- e2e/package.json | 2 +- machine-learning/pyproject.toml | 2 +- mobile/android/fastlane/Fastfile | 4 ++-- mobile/ios/fastlane/Fastfile | 2 +- mobile/openapi/README.md | 2 +- mobile/pubspec.yaml | 2 +- open-api/immich-openapi-specs.json | 2 +- open-api/typescript-sdk/package-lock.json | 4 ++-- open-api/typescript-sdk/package.json | 2 +- open-api/typescript-sdk/src/fetch-client.ts | 2 +- server/package-lock.json | 4 ++-- server/package.json | 2 +- web/package-lock.json | 6 +++--- web/package.json | 2 +- 16 files changed, 23 insertions(+), 23 deletions(-) diff --git a/cli/package-lock.json b/cli/package-lock.json index 899c1cd487..e17392c9d8 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -47,7 +47,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.102.0", + "version": "1.102.1", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { diff --git a/e2e/package-lock.json b/e2e/package-lock.json index bdafbadd4f..b58faf4424 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich-e2e", - "version": "1.102.0", + "version": "1.102.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-e2e", - "version": "1.102.0", + "version": "1.102.1", "license": "GNU Affero General Public License version 3", "devDependencies": { "@immich/cli": "file:../cli", @@ -81,7 +81,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.102.0", + "version": "1.102.1", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { diff --git a/e2e/package.json b/e2e/package.json index 9e5ad85fb4..ae3aa015d7 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -1,6 +1,6 @@ { "name": "immich-e2e", - "version": "1.102.0", + "version": "1.102.1", "description": "", "main": "index.js", "type": "module", diff --git a/machine-learning/pyproject.toml b/machine-learning/pyproject.toml index 5610f8438d..9a9026fdff 100644 --- a/machine-learning/pyproject.toml +++ b/machine-learning/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "machine-learning" -version = "1.102.0" +version = "1.102.1" description = "" authors = ["Hau Tran "] readme = "README.md" diff --git a/mobile/android/fastlane/Fastfile b/mobile/android/fastlane/Fastfile index 50a22b6451..5690591d50 100644 --- a/mobile/android/fastlane/Fastfile +++ b/mobile/android/fastlane/Fastfile @@ -35,8 +35,8 @@ platform :android do task: 'bundle', build_type: 'Release', properties: { - "android.injected.version.code" => 132, - "android.injected.version.name" => "1.102.0", + "android.injected.version.code" => 133, + "android.injected.version.name" => "1.102.1", } ) upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab') diff --git a/mobile/ios/fastlane/Fastfile b/mobile/ios/fastlane/Fastfile index 74ffcf4237..548fac81f7 100644 --- a/mobile/ios/fastlane/Fastfile +++ b/mobile/ios/fastlane/Fastfile @@ -19,7 +19,7 @@ platform :ios do desc "iOS Beta" lane :beta do increment_version_number( - version_number: "1.102.0" + version_number: "1.102.1" ) increment_build_number( build_number: latest_testflight_build_number + 1, diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 27d631e4fd..b5a92b6e6f 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -3,7 +3,7 @@ Immich API This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project: -- API version: 1.102.0 +- API version: 1.102.1 - Build package: org.openapitools.codegen.languages.DartClientCodegen ## Requirements diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 70c443d832..9f367a76de 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -2,7 +2,7 @@ name: immich_mobile description: Immich - selfhosted backup media file on mobile phone publish_to: 'none' -version: 1.102.0+132 +version: 1.102.1+133 environment: sdk: '>=3.0.0 <4.0.0' diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 4f666b303c..184400b140 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -7078,7 +7078,7 @@ "info": { "title": "Immich", "description": "Immich API", - "version": "1.102.0", + "version": "1.102.1", "contact": {} }, "tags": [], diff --git a/open-api/typescript-sdk/package-lock.json b/open-api/typescript-sdk/package-lock.json index e698fb97ff..acef699734 100644 --- a/open-api/typescript-sdk/package-lock.json +++ b/open-api/typescript-sdk/package-lock.json @@ -1,12 +1,12 @@ { "name": "@immich/sdk", - "version": "1.102.0", + "version": "1.102.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@immich/sdk", - "version": "1.102.0", + "version": "1.102.1", "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" diff --git a/open-api/typescript-sdk/package.json b/open-api/typescript-sdk/package.json index eab26bc4e1..a323a5a459 100644 --- a/open-api/typescript-sdk/package.json +++ b/open-api/typescript-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@immich/sdk", - "version": "1.102.0", + "version": "1.102.1", "description": "Auto-generated TypeScript SDK for the Immich API", "type": "module", "main": "./build/index.js", diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 1bf219162d..9fe74fde7b 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -1,6 +1,6 @@ /** * Immich - * 1.102.0 + * 1.102.1 * DO NOT MODIFY - This file has been generated using oazapfts. * See https://www.npmjs.com/package/oazapfts */ diff --git a/server/package-lock.json b/server/package-lock.json index c3f8e8cf79..f2362c2717 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich", - "version": "1.102.0", + "version": "1.102.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "immich", - "version": "1.102.0", + "version": "1.102.1", "license": "GNU Affero General Public License version 3", "dependencies": { "@nestjs/bullmq": "^10.0.1", diff --git a/server/package.json b/server/package.json index bdecc1362a..bb2cd8a24e 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "immich", - "version": "1.102.0", + "version": "1.102.1", "description": "", "author": "", "private": true, diff --git a/web/package-lock.json b/web/package-lock.json index d36059ca05..b6e3aea30d 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich-web", - "version": "1.102.0", + "version": "1.102.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-web", - "version": "1.102.0", + "version": "1.102.1", "license": "GNU Affero General Public License version 3", "dependencies": { "@immich/sdk": "file:../open-api/typescript-sdk", @@ -63,7 +63,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.102.0", + "version": "1.102.1", "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" diff --git a/web/package.json b/web/package.json index 5e2ec844a1..11a1ce65ae 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "immich-web", - "version": "1.102.0", + "version": "1.102.1", "license": "GNU Affero General Public License version 3", "scripts": { "dev": "vite dev --host 0.0.0.0 --port 3000", From c858b43717ccbef31d55fe8446901f9bd1f48cec Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 20 Apr 2024 09:12:11 -0500 Subject: [PATCH 10/27] chore: post release tasks --- mobile/android/fastlane/report.xml | 6 +++--- mobile/ios/Runner.xcodeproj/project.pbxproj | 6 +++--- mobile/ios/Runner/Info.plist | 4 ++-- mobile/ios/fastlane/report.xml | 15 +++++---------- 4 files changed, 13 insertions(+), 18 deletions(-) diff --git a/mobile/android/fastlane/report.xml b/mobile/android/fastlane/report.xml index 9ff4624d67..9b1b5df910 100644 --- a/mobile/android/fastlane/report.xml +++ b/mobile/android/fastlane/report.xml @@ -5,17 +5,17 @@ - + - + - + diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index 26153c05cd..9eb77da4d4 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -383,7 +383,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 148; + CURRENT_PROJECT_VERSION = 150; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -525,7 +525,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 148; + CURRENT_PROJECT_VERSION = 150; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -553,7 +553,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 148; + CURRENT_PROJECT_VERSION = 150; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist index 3e7f6a874a..70338b5725 100644 --- a/mobile/ios/Runner/Info.plist +++ b/mobile/ios/Runner/Info.plist @@ -58,11 +58,11 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.102.0 + 1.102.1 CFBundleSignature ???? CFBundleVersion - 148 + 150 FLTEnableImpeller ITSAppUsesNonExemptEncryption diff --git a/mobile/ios/fastlane/report.xml b/mobile/ios/fastlane/report.xml index 85320ab12a..b1538e60af 100644 --- a/mobile/ios/fastlane/report.xml +++ b/mobile/ios/fastlane/report.xml @@ -5,32 +5,27 @@ - + - + - + - + - - - - - - + From 6778653825e8deaa4db5e49f7f65bc0d57802975 Mon Sep 17 00:00:00 2001 From: martin <74269598+martabal@users.noreply.github.com> Date: Sat, 20 Apr 2024 16:18:31 +0200 Subject: [PATCH 11/27] fix(web): keep focus when searching people (#8950) fix: keep focus when searching people Co-authored-by: Alex --- web/src/routes/(user)/people/+page.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/routes/(user)/people/+page.svelte b/web/src/routes/(user)/people/+page.svelte index ba9db04c74..09d36890f5 100644 --- a/web/src/routes/(user)/people/+page.svelte +++ b/web/src/routes/(user)/people/+page.svelte @@ -89,7 +89,7 @@ const handleSearch = async (force: boolean) => { $page.url.searchParams.set(QueryParameter.SEARCHED_PEOPLE, searchName); - await goto($page.url); + await goto($page.url, { keepFocus: true }); await handleSearchPeople(force); }; From caf76f07130d8e9c125bb8a9329fe5b623602722 Mon Sep 17 00:00:00 2001 From: Jaryl Chng Date: Sat, 20 Apr 2024 22:36:00 +0800 Subject: [PATCH 12/27] feat(server): enable AV1 encoding for QSV (#8942) --- server/src/utils/media.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/utils/media.ts b/server/src/utils/media.ts index d0a6d4d740..7750e48873 100644 --- a/server/src/utils/media.ts +++ b/server/src/utils/media.ts @@ -566,7 +566,7 @@ export class QSVConfig extends BaseHWConfig { } getSupportedCodecs() { - return [VideoCodec.H264, VideoCodec.HEVC, VideoCodec.VP9]; + return [VideoCodec.H264, VideoCodec.HEVC, VideoCodec.VP9, VideoCodec.AV1]; } // recommended from https://github.com/intel/media-delivery/blob/master/doc/benchmarks/intel-iris-xe-max-graphics/intel-iris-xe-max-graphics.md From 7ec62f12b5e6c70629764f043a663dec5a59c674 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 20 Apr 2024 10:53:52 -0500 Subject: [PATCH 13/27] Revert "fix(mobile): random logout (#8739)" (#8954) This reverts commit 97c099e26dd35dc89a7a28b84e032e7aa11cba55. --- mobile/lib/shared/views/splash_screen.dart | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/mobile/lib/shared/views/splash_screen.dart b/mobile/lib/shared/views/splash_screen.dart index 64bc1ec081..47b550f9d0 100644 --- a/mobile/lib/shared/views/splash_screen.dart +++ b/mobile/lib/shared/views/splash_screen.dart @@ -25,7 +25,6 @@ class SplashScreenPage extends HookConsumerWidget { void performLoggingIn() async { bool isSuccess = false; bool deviceIsOffline = false; - if (accessToken != null && serverUrl != null) { try { // Resolve API server endpoint from user provided serverUrl @@ -51,11 +50,15 @@ class SplashScreenPage extends HookConsumerWidget { offlineLogin: deviceIsOffline, ); } catch (error, stackTrace) { + ref.read(authenticationProvider.notifier).logout(); + log.severe( 'Cannot set success login info', error, stackTrace, ); + + context.pushRoute(const LoginRoute()); } } @@ -73,11 +76,6 @@ class SplashScreenPage extends HookConsumerWidget { } context.replaceRoute(const TabControllerRoute()); } else { - log.severe( - 'Unable to login through offline or online methods - logging out completely', - ); - - ref.read(authenticationProvider.notifier).logout(); // User was unable to login through either offline or online methods context.replaceRoute(const LoginRoute()); } From 25549b87c924f65a43e8fa5d612cd6cf014c38f3 Mon Sep 17 00:00:00 2001 From: Alex The Bot Date: Sat, 20 Apr 2024 15:55:32 +0000 Subject: [PATCH 14/27] Version v1.102.2 --- cli/package-lock.json | 2 +- e2e/package-lock.json | 6 +++--- e2e/package.json | 2 +- machine-learning/pyproject.toml | 2 +- mobile/android/fastlane/Fastfile | 4 ++-- mobile/ios/fastlane/Fastfile | 2 +- mobile/openapi/README.md | 2 +- mobile/pubspec.yaml | 2 +- open-api/immich-openapi-specs.json | 2 +- open-api/typescript-sdk/package-lock.json | 4 ++-- open-api/typescript-sdk/package.json | 2 +- open-api/typescript-sdk/src/fetch-client.ts | 2 +- server/package-lock.json | 4 ++-- server/package.json | 2 +- web/package-lock.json | 6 +++--- web/package.json | 2 +- 16 files changed, 23 insertions(+), 23 deletions(-) diff --git a/cli/package-lock.json b/cli/package-lock.json index e17392c9d8..68a7a29280 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -47,7 +47,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.102.1", + "version": "1.102.2", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { diff --git a/e2e/package-lock.json b/e2e/package-lock.json index b58faf4424..b9933aee96 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich-e2e", - "version": "1.102.1", + "version": "1.102.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-e2e", - "version": "1.102.1", + "version": "1.102.2", "license": "GNU Affero General Public License version 3", "devDependencies": { "@immich/cli": "file:../cli", @@ -81,7 +81,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.102.1", + "version": "1.102.2", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { diff --git a/e2e/package.json b/e2e/package.json index ae3aa015d7..b71950fd18 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -1,6 +1,6 @@ { "name": "immich-e2e", - "version": "1.102.1", + "version": "1.102.2", "description": "", "main": "index.js", "type": "module", diff --git a/machine-learning/pyproject.toml b/machine-learning/pyproject.toml index 9a9026fdff..4d06858b78 100644 --- a/machine-learning/pyproject.toml +++ b/machine-learning/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "machine-learning" -version = "1.102.1" +version = "1.102.2" description = "" authors = ["Hau Tran "] readme = "README.md" diff --git a/mobile/android/fastlane/Fastfile b/mobile/android/fastlane/Fastfile index 5690591d50..4dd9c9c60f 100644 --- a/mobile/android/fastlane/Fastfile +++ b/mobile/android/fastlane/Fastfile @@ -35,8 +35,8 @@ platform :android do task: 'bundle', build_type: 'Release', properties: { - "android.injected.version.code" => 133, - "android.injected.version.name" => "1.102.1", + "android.injected.version.code" => 134, + "android.injected.version.name" => "1.102.2", } ) upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab') diff --git a/mobile/ios/fastlane/Fastfile b/mobile/ios/fastlane/Fastfile index 548fac81f7..2d1c1129f7 100644 --- a/mobile/ios/fastlane/Fastfile +++ b/mobile/ios/fastlane/Fastfile @@ -19,7 +19,7 @@ platform :ios do desc "iOS Beta" lane :beta do increment_version_number( - version_number: "1.102.1" + version_number: "1.102.2" ) increment_build_number( build_number: latest_testflight_build_number + 1, diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index b5a92b6e6f..24e4e993b6 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -3,7 +3,7 @@ Immich API This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project: -- API version: 1.102.1 +- API version: 1.102.2 - Build package: org.openapitools.codegen.languages.DartClientCodegen ## Requirements diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 9f367a76de..f3d821c415 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -2,7 +2,7 @@ name: immich_mobile description: Immich - selfhosted backup media file on mobile phone publish_to: 'none' -version: 1.102.1+133 +version: 1.102.2+134 environment: sdk: '>=3.0.0 <4.0.0' diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 184400b140..c21b16e93f 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -7078,7 +7078,7 @@ "info": { "title": "Immich", "description": "Immich API", - "version": "1.102.1", + "version": "1.102.2", "contact": {} }, "tags": [], diff --git a/open-api/typescript-sdk/package-lock.json b/open-api/typescript-sdk/package-lock.json index acef699734..a3056fe71d 100644 --- a/open-api/typescript-sdk/package-lock.json +++ b/open-api/typescript-sdk/package-lock.json @@ -1,12 +1,12 @@ { "name": "@immich/sdk", - "version": "1.102.1", + "version": "1.102.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@immich/sdk", - "version": "1.102.1", + "version": "1.102.2", "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" diff --git a/open-api/typescript-sdk/package.json b/open-api/typescript-sdk/package.json index a323a5a459..3c2a0e1f7c 100644 --- a/open-api/typescript-sdk/package.json +++ b/open-api/typescript-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@immich/sdk", - "version": "1.102.1", + "version": "1.102.2", "description": "Auto-generated TypeScript SDK for the Immich API", "type": "module", "main": "./build/index.js", diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 9fe74fde7b..d384986e0c 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -1,6 +1,6 @@ /** * Immich - * 1.102.1 + * 1.102.2 * DO NOT MODIFY - This file has been generated using oazapfts. * See https://www.npmjs.com/package/oazapfts */ diff --git a/server/package-lock.json b/server/package-lock.json index f2362c2717..a70fc65659 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich", - "version": "1.102.1", + "version": "1.102.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "immich", - "version": "1.102.1", + "version": "1.102.2", "license": "GNU Affero General Public License version 3", "dependencies": { "@nestjs/bullmq": "^10.0.1", diff --git a/server/package.json b/server/package.json index bb2cd8a24e..8ac141c24d 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "immich", - "version": "1.102.1", + "version": "1.102.2", "description": "", "author": "", "private": true, diff --git a/web/package-lock.json b/web/package-lock.json index b6e3aea30d..441ed46442 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich-web", - "version": "1.102.1", + "version": "1.102.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-web", - "version": "1.102.1", + "version": "1.102.2", "license": "GNU Affero General Public License version 3", "dependencies": { "@immich/sdk": "file:../open-api/typescript-sdk", @@ -63,7 +63,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.102.1", + "version": "1.102.2", "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" diff --git a/web/package.json b/web/package.json index 11a1ce65ae..a891cfaa33 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "immich-web", - "version": "1.102.1", + "version": "1.102.2", "license": "GNU Affero General Public License version 3", "scripts": { "dev": "vite dev --host 0.0.0.0 --port 3000", From 40931b5668be18cfd34bb0520513b58f3445d271 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 20 Apr 2024 11:15:41 -0500 Subject: [PATCH 15/27] chore: post release tasks --- mobile/android/fastlane/report.xml | 6 +++--- mobile/ios/Runner/Info.plist | 2 +- mobile/ios/fastlane/report.xml | 15 ++++++++++----- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/mobile/android/fastlane/report.xml b/mobile/android/fastlane/report.xml index 9b1b5df910..94f1ff73e9 100644 --- a/mobile/android/fastlane/report.xml +++ b/mobile/android/fastlane/report.xml @@ -5,17 +5,17 @@ - + - + - + diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist index 70338b5725..517c5b29ef 100644 --- a/mobile/ios/Runner/Info.plist +++ b/mobile/ios/Runner/Info.plist @@ -58,7 +58,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.102.1 + 1.102.2 CFBundleSignature ???? CFBundleVersion diff --git a/mobile/ios/fastlane/report.xml b/mobile/ios/fastlane/report.xml index b1538e60af..c3a94c1c23 100644 --- a/mobile/ios/fastlane/report.xml +++ b/mobile/ios/fastlane/report.xml @@ -5,27 +5,32 @@ - + - + - + - + - + + + + + + From 2dd7c13b8871e5cc3e546e3f9244f0298c425643 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 20 Apr 2024 12:15:26 -0500 Subject: [PATCH 16/27] Revert "feat(android) Check server is reachable before starting background backup (#8594)" (#8958) This reverts commit 71b6d8b569038e1676316f61845c91cbac58c19c. --- mobile/android/app/build.gradle | 2 +- .../example/mobile/BackgroundServicePlugin.kt | 1 - .../kotlin/com/example/mobile/BackupWorker.kt | 132 ++++-------------- mobile/android/build.gradle | 4 +- .../background.service.dart | 7 +- 5 files changed, 32 insertions(+), 114 deletions(-) diff --git a/mobile/android/app/build.gradle b/mobile/android/app/build.gradle index a6f86b8537..96d2db23f5 100644 --- a/mobile/android/app/build.gradle +++ b/mobile/android/app/build.gradle @@ -90,7 +90,7 @@ flutter { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version" - implementation "androidx.work:work-runtime:$work_version" + implementation "androidx.work:work-runtime-ktx:$work_version" implementation "androidx.concurrent:concurrent-futures:$concurrent_version" implementation "com.google.guava:guava:$guava_version" implementation "com.github.bumptech.glide:glide:$glide_version" diff --git a/mobile/android/app/src/main/kotlin/com/example/mobile/BackgroundServicePlugin.kt b/mobile/android/app/src/main/kotlin/com/example/mobile/BackgroundServicePlugin.kt index 1d23c5665c..6541ad5755 100644 --- a/mobile/android/app/src/main/kotlin/com/example/mobile/BackgroundServicePlugin.kt +++ b/mobile/android/app/src/main/kotlin/com/example/mobile/BackgroundServicePlugin.kt @@ -52,7 +52,6 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler { .putBoolean(ContentObserverWorker.SHARED_PREF_SERVICE_ENABLED, true) .putLong(BackupWorker.SHARED_PREF_CALLBACK_KEY, args.get(0) as Long) .putString(BackupWorker.SHARED_PREF_NOTIFICATION_TITLE, args.get(1) as String) - .putString(BackupWorker.SHARED_PREF_SERVER_URL, args.get(3) as String) .apply() ContentObserverWorker.enable(ctx, immediate = args.get(2) as Boolean) result.success(true) diff --git a/mobile/android/app/src/main/kotlin/com/example/mobile/BackupWorker.kt b/mobile/android/app/src/main/kotlin/com/example/mobile/BackupWorker.kt index dc7c4a9c37..660e1d55ba 100644 --- a/mobile/android/app/src/main/kotlin/com/example/mobile/BackupWorker.kt +++ b/mobile/android/app/src/main/kotlin/com/example/mobile/BackupWorker.kt @@ -11,8 +11,8 @@ import android.os.PowerManager import android.os.SystemClock import android.util.Log import androidx.annotation.RequiresApi -import androidx.concurrent.futures.CallbackToFutureAdapter import androidx.core.app.NotificationCompat +import androidx.concurrent.futures.ResolvableFuture import androidx.work.BackoffPolicy import androidx.work.Constraints import androidx.work.ForegroundInfo @@ -30,16 +30,6 @@ import io.flutter.embedding.engine.loader.FlutterLoader import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.view.FlutterCallbackInformation -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import kotlinx.coroutines.withTimeoutOrNull -import java.io.IOException -import java.net.HttpURLConnection -import java.net.InetAddress -import java.net.URL import java.util.concurrent.TimeUnit /** @@ -52,6 +42,7 @@ import java.util.concurrent.TimeUnit */ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ctx, params), MethodChannel.MethodCallHandler { + private val resolvableFuture = ResolvableFuture.create() private var engine: FlutterEngine? = null private lateinit var backgroundChannel: MethodChannel private val notificationManager = ctx.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager @@ -61,80 +52,35 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct private var notificationDetailBuilder: NotificationCompat.Builder? = null private var fgFuture: ListenableFuture? = null - private val job = Job() - private lateinit var completer: CallbackToFutureAdapter.Completer - private val resolvableFuture = CallbackToFutureAdapter.getFuture { completer -> - this.completer = completer - null - } - - init { - resolvableFuture.addListener( - Runnable { - if (resolvableFuture.isCancelled) { - job.cancel() - } - }, - taskExecutor.serialTaskExecutor - ) - } - override fun startWork(): ListenableFuture { + Log.d(TAG, "startWork") val ctx = applicationContext - val prefs = ctx.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE) - prefs.getString(SHARED_PREF_SERVER_URL, null) - ?.takeIf { it.isNotEmpty() } - ?.let { serverUrl -> doCoroutineWork(serverUrl) } - ?: doWork() - return resolvableFuture - } - - /** - * This function is used to check if server URL is reachable before starting the backup work. - * Check must be done in a background to avoid blocking the main thread. - */ - private fun doCoroutineWork(serverUrl : String) { - CoroutineScope(Dispatchers.Default + job).launch { - val isReachable = isUrlReachableHttp(serverUrl) - withContext(Dispatchers.Main) { - if (isReachable) { - doWork() - } else { - // Fail when the URL is not reachable - completer.set(Result.failure()) - } - } + if (!flutterLoader.initialized()) { + flutterLoader.startInitialization(ctx) } - } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + // Create a Notification channel if necessary + createChannel() + } + if (isIgnoringBatteryOptimizations) { + // normal background services can only up to 10 minutes + // foreground services are allowed to run indefinitely + // requires battery optimizations to be disabled (either manually by the user + // or by the system learning that immich is important to the user) + val title = ctx.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE) + .getString(SHARED_PREF_NOTIFICATION_TITLE, NOTIFICATION_DEFAULT_TITLE)!! + showInfo(getInfoBuilder(title, indeterminate=true).build()) + } + engine = FlutterEngine(ctx) - private fun doWork() { - Log.d(TAG, "doWork") - val ctx = applicationContext + flutterLoader.ensureInitializationCompleteAsync(ctx, null, Handler(Looper.getMainLooper())) { + runDart() + } - if (!flutterLoader.initialized()) { - flutterLoader.startInitialization(ctx) - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - // Create a Notification channel if necessary - createChannel() - } - if (isIgnoringBatteryOptimizations) { - // normal background services can only up to 10 minutes - // foreground services are allowed to run indefinitely - // requires battery optimizations to be disabled (either manually by the user - // or by the system learning that immich is important to the user) - val title = ctx.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE) - .getString(SHARED_PREF_NOTIFICATION_TITLE, NOTIFICATION_DEFAULT_TITLE)!! - showInfo(getInfoBuilder(title, indeterminate=true).build()) - } - engine = FlutterEngine(ctx) - - flutterLoader.ensureInitializationCompleteAsync(ctx, null, Handler(Looper.getMainLooper())) { - runDart() - } + return resolvableFuture } /** @@ -193,7 +139,7 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct engine = null if (result != null) { Log.d(TAG, "stopEngine result=${result}") - this.completer.set(result) + resolvableFuture.set(result) } waitOnSetForegroundAsync() } @@ -324,14 +270,13 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct const val SHARED_PREF_CALLBACK_KEY = "callbackDispatcherHandle" const val SHARED_PREF_NOTIFICATION_TITLE = "notificationTitle" const val SHARED_PREF_LAST_CHANGE = "lastChange" - const val SHARED_PREF_SERVER_URL = "serverUrl" private const val TASK_NAME_BACKUP = "immich/BackupWorker" private const val NOTIFICATION_CHANNEL_ID = "immich/backgroundService" private const val NOTIFICATION_CHANNEL_ERROR_ID = "immich/backgroundServiceError" private const val NOTIFICATION_DEFAULT_TITLE = "Immich" private const val NOTIFICATION_ID = 1 - private const val NOTIFICATION_ERROR_ID = 2 + private const val NOTIFICATION_ERROR_ID = 2 private const val NOTIFICATION_DETAIL_ID = 3 private const val ONE_MINUTE = 60000L @@ -359,7 +304,7 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct val workInfoList = workInfoFuture.get(1000, TimeUnit.MILLISECONDS) if (workInfoList != null) { for (workInfo in workInfoList) { - if (workInfo.state == WorkInfo.State.ENQUEUED) { + if (workInfo.getState() == WorkInfo.State.ENQUEUED) { val workRequest = buildWorkRequest(requireWifi, requireCharging) wm.enqueueUniqueWork(TASK_NAME_BACKUP, ExistingWorkPolicy.REPLACE, workRequest) Log.d(TAG, "updateBackupWorker updated BackupWorker constraints") @@ -414,27 +359,4 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct } } -private const val TAG = "BackupWorker" - -/** - * Check if the given URL is reachable via HTTP - */ -suspend fun isUrlReachableHttp(url: String, timeoutMillis: Long = 5000L): Boolean { - return withTimeoutOrNull(timeoutMillis) { - var httpURLConnection: HttpURLConnection? = null - try { - httpURLConnection = (URL(url).openConnection() as HttpURLConnection).apply { - requestMethod = "HEAD" - connectTimeout = timeoutMillis.toInt() - readTimeout = timeoutMillis.toInt() - } - httpURLConnection.connect() - httpURLConnection.responseCode == HttpURLConnection.HTTP_OK - } catch (e: Exception) { - Log.e(TAG, "Failed to reach server URL: $e") - false - } finally { - httpURLConnection?.disconnect() - } - } == true -} +private const val TAG = "BackupWorker" \ No newline at end of file diff --git a/mobile/android/build.gradle b/mobile/android/build.gradle index e9c271b2c5..4dacde5a9d 100644 --- a/mobile/android/build.gradle +++ b/mobile/android/build.gradle @@ -1,7 +1,7 @@ buildscript { - ext.kotlin_version = '1.8.22' + ext.kotlin_version = '1.8.20' ext.kotlin_coroutines_version = '1.7.1' - ext.work_version = '2.9.0' + ext.work_version = '2.7.1' ext.concurrent_version = '1.1.0' ext.guava_version = '33.0.0-android' ext.glide_version = '4.14.2' diff --git a/mobile/lib/modules/backup/background_service/background.service.dart b/mobile/lib/modules/backup/background_service/background.service.dart index 8358043894..cbee121105 100644 --- a/mobile/lib/modules/backup/background_service/background.service.dart +++ b/mobile/lib/modules/backup/background_service/background.service.dart @@ -20,7 +20,6 @@ import 'package:immich_mobile/shared/models/store.dart'; import 'package:immich_mobile/shared/services/api.service.dart'; import 'package:immich_mobile/utils/backup_progress.dart'; import 'package:immich_mobile/utils/diff.dart'; -import 'package:immich_mobile/utils/url_helper.dart'; import 'package:isar/isar.dart'; import 'package:path_provider_ios/path_provider_ios.dart'; import 'package:photo_manager/photo_manager.dart'; @@ -69,10 +68,8 @@ class BackgroundService { final callback = PluginUtilities.getCallbackHandle(_nativeEntry)!; final String title = "backup_background_service_default_notification".tr(); - final bool ok = await _foregroundChannel.invokeMethod( - 'enable', - [callback.toRawHandle(), title, immediate, getServerUrl()], - ); + final bool ok = await _foregroundChannel + .invokeMethod('enable', [callback.toRawHandle(), title, immediate]); return ok; } catch (error) { return false; From fd4514711f03a236b508f84b7aedec70de27bd4c Mon Sep 17 00:00:00 2001 From: Mert <101130780+mertalev@users.noreply.github.com> Date: Sat, 20 Apr 2024 14:52:50 -0400 Subject: [PATCH 17/27] feat(server): enable AV1 encoding for NVENC (#8959) allow av1 for nvenc --- server/src/utils/media.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/utils/media.ts b/server/src/utils/media.ts index 7750e48873..ff38ded631 100644 --- a/server/src/utils/media.ts +++ b/server/src/utils/media.ts @@ -436,7 +436,7 @@ export class AV1Config extends BaseConfig { export class NVENCConfig extends BaseHWConfig { getSupportedCodecs() { - return [VideoCodec.H264, VideoCodec.HEVC]; + return [VideoCodec.H264, VideoCodec.HEVC, VideoCodec.AV1]; } getBaseInputOptions() { From 1e3dceea4d0ce4a1be4ea27385c8fb8ef4ca16a7 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Sat, 20 Apr 2024 16:15:25 -0400 Subject: [PATCH 18/27] fix(server): session refresh (#8974) --- server/src/services/auth.service.spec.ts | 5 +---- server/src/services/auth.service.ts | 4 ++-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/server/src/services/auth.service.spec.ts b/server/src/services/auth.service.spec.ts index cbee9faddf..f00e10b13c 100644 --- a/server/src/services/auth.service.spec.ts +++ b/server/src/services/auth.service.spec.ts @@ -340,10 +340,7 @@ describe('AuthService', () => { sessionMock.getByToken.mockResolvedValue(sessionStub.inactive); sessionMock.update.mockResolvedValue(sessionStub.valid); const headers: IncomingHttpHeaders = { cookie: 'immich_access_token=auth_token' }; - await expect(sut.validate(headers, {})).resolves.toEqual({ - user: userStub.user1, - session: sessionStub.valid, - }); + await expect(sut.validate(headers, {})).resolves.toBeDefined(); expect(sessionMock.update.mock.calls[0][0]).toMatchObject({ id: 'not_active', updatedAt: expect.any(Date) }); }); }); diff --git a/server/src/services/auth.service.ts b/server/src/services/auth.service.ts index bea7366555..72fee12f45 100644 --- a/server/src/services/auth.service.ts +++ b/server/src/services/auth.service.ts @@ -374,14 +374,14 @@ export class AuthService { private async validateSession(tokenValue: string): Promise { const hashedToken = this.cryptoRepository.hashSha256(tokenValue); - let session = await this.sessionRepository.getByToken(hashedToken); + const session = await this.sessionRepository.getByToken(hashedToken); if (session?.user) { const now = DateTime.now(); const updatedAt = DateTime.fromJSDate(session.updatedAt); const diff = now.diff(updatedAt, ['hours']); if (diff.hours > 1) { - session = await this.sessionRepository.update({ id: session.id, updatedAt: new Date() }); + await this.sessionRepository.update({ id: session.id, updatedAt: new Date() }); } return { user: session.user, session: session }; From a2180a467d09c01ec41c1481e51c7485d5a82046 Mon Sep 17 00:00:00 2001 From: Alex The Bot Date: Sat, 20 Apr 2024 20:17:39 +0000 Subject: [PATCH 19/27] Version v1.102.3 --- cli/package-lock.json | 2 +- e2e/package-lock.json | 6 +++--- e2e/package.json | 2 +- machine-learning/pyproject.toml | 2 +- mobile/android/fastlane/Fastfile | 2 +- mobile/ios/fastlane/Fastfile | 2 +- mobile/openapi/README.md | 2 +- mobile/pubspec.yaml | 2 +- open-api/immich-openapi-specs.json | 2 +- open-api/typescript-sdk/package-lock.json | 4 ++-- open-api/typescript-sdk/package.json | 2 +- open-api/typescript-sdk/src/fetch-client.ts | 2 +- server/package-lock.json | 4 ++-- server/package.json | 2 +- web/package-lock.json | 6 +++--- web/package.json | 2 +- 16 files changed, 22 insertions(+), 22 deletions(-) diff --git a/cli/package-lock.json b/cli/package-lock.json index 68a7a29280..5075514973 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -47,7 +47,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.102.2", + "version": "1.102.3", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { diff --git a/e2e/package-lock.json b/e2e/package-lock.json index b9933aee96..4643b8b01f 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich-e2e", - "version": "1.102.2", + "version": "1.102.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-e2e", - "version": "1.102.2", + "version": "1.102.3", "license": "GNU Affero General Public License version 3", "devDependencies": { "@immich/cli": "file:../cli", @@ -81,7 +81,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.102.2", + "version": "1.102.3", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { diff --git a/e2e/package.json b/e2e/package.json index b71950fd18..34ef229a2a 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -1,6 +1,6 @@ { "name": "immich-e2e", - "version": "1.102.2", + "version": "1.102.3", "description": "", "main": "index.js", "type": "module", diff --git a/machine-learning/pyproject.toml b/machine-learning/pyproject.toml index 4d06858b78..76f51d964e 100644 --- a/machine-learning/pyproject.toml +++ b/machine-learning/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "machine-learning" -version = "1.102.2" +version = "1.102.3" description = "" authors = ["Hau Tran "] readme = "README.md" diff --git a/mobile/android/fastlane/Fastfile b/mobile/android/fastlane/Fastfile index 4dd9c9c60f..024cd6436f 100644 --- a/mobile/android/fastlane/Fastfile +++ b/mobile/android/fastlane/Fastfile @@ -36,7 +36,7 @@ platform :android do build_type: 'Release', properties: { "android.injected.version.code" => 134, - "android.injected.version.name" => "1.102.2", + "android.injected.version.name" => "1.102.3", } ) upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab') diff --git a/mobile/ios/fastlane/Fastfile b/mobile/ios/fastlane/Fastfile index 2d1c1129f7..dd62415ff5 100644 --- a/mobile/ios/fastlane/Fastfile +++ b/mobile/ios/fastlane/Fastfile @@ -19,7 +19,7 @@ platform :ios do desc "iOS Beta" lane :beta do increment_version_number( - version_number: "1.102.2" + version_number: "1.102.3" ) increment_build_number( build_number: latest_testflight_build_number + 1, diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 24e4e993b6..5439d48208 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -3,7 +3,7 @@ Immich API This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project: -- API version: 1.102.2 +- API version: 1.102.3 - Build package: org.openapitools.codegen.languages.DartClientCodegen ## Requirements diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index f3d821c415..16fd92d821 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -2,7 +2,7 @@ name: immich_mobile description: Immich - selfhosted backup media file on mobile phone publish_to: 'none' -version: 1.102.2+134 +version: 1.102.3+134 environment: sdk: '>=3.0.0 <4.0.0' diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index c21b16e93f..b5bf2c9f4d 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -7078,7 +7078,7 @@ "info": { "title": "Immich", "description": "Immich API", - "version": "1.102.2", + "version": "1.102.3", "contact": {} }, "tags": [], diff --git a/open-api/typescript-sdk/package-lock.json b/open-api/typescript-sdk/package-lock.json index a3056fe71d..a6a751ccfa 100644 --- a/open-api/typescript-sdk/package-lock.json +++ b/open-api/typescript-sdk/package-lock.json @@ -1,12 +1,12 @@ { "name": "@immich/sdk", - "version": "1.102.2", + "version": "1.102.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@immich/sdk", - "version": "1.102.2", + "version": "1.102.3", "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" diff --git a/open-api/typescript-sdk/package.json b/open-api/typescript-sdk/package.json index 3c2a0e1f7c..dd2360cede 100644 --- a/open-api/typescript-sdk/package.json +++ b/open-api/typescript-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@immich/sdk", - "version": "1.102.2", + "version": "1.102.3", "description": "Auto-generated TypeScript SDK for the Immich API", "type": "module", "main": "./build/index.js", diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index d384986e0c..41603bc0e8 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -1,6 +1,6 @@ /** * Immich - * 1.102.2 + * 1.102.3 * DO NOT MODIFY - This file has been generated using oazapfts. * See https://www.npmjs.com/package/oazapfts */ diff --git a/server/package-lock.json b/server/package-lock.json index a70fc65659..eb062059f3 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich", - "version": "1.102.2", + "version": "1.102.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "immich", - "version": "1.102.2", + "version": "1.102.3", "license": "GNU Affero General Public License version 3", "dependencies": { "@nestjs/bullmq": "^10.0.1", diff --git a/server/package.json b/server/package.json index 8ac141c24d..274eddd304 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "immich", - "version": "1.102.2", + "version": "1.102.3", "description": "", "author": "", "private": true, diff --git a/web/package-lock.json b/web/package-lock.json index 441ed46442..40f1d937c2 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich-web", - "version": "1.102.2", + "version": "1.102.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-web", - "version": "1.102.2", + "version": "1.102.3", "license": "GNU Affero General Public License version 3", "dependencies": { "@immich/sdk": "file:../open-api/typescript-sdk", @@ -63,7 +63,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.102.2", + "version": "1.102.3", "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" diff --git a/web/package.json b/web/package.json index a891cfaa33..b2bdd5afba 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "immich-web", - "version": "1.102.2", + "version": "1.102.3", "license": "GNU Affero General Public License version 3", "scripts": { "dev": "vite dev --host 0.0.0.0 --port 3000", From cef84f6cedef1c7f83027327474681d92d685845 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 20 Apr 2024 19:56:03 -0500 Subject: [PATCH 20/27] chore(mobile): override appbundle on PlayStore before getting released (#8960) --- mobile/android/fastlane/Fastfile | 2 +- mobile/android/fastlane/report.xml | 6 +++--- mobile/pubspec.yaml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/mobile/android/fastlane/Fastfile b/mobile/android/fastlane/Fastfile index 024cd6436f..94a9a7e0bd 100644 --- a/mobile/android/fastlane/Fastfile +++ b/mobile/android/fastlane/Fastfile @@ -35,7 +35,7 @@ platform :android do task: 'bundle', build_type: 'Release', properties: { - "android.injected.version.code" => 134, + "android.injected.version.code" => 136, "android.injected.version.name" => "1.102.3", } ) diff --git a/mobile/android/fastlane/report.xml b/mobile/android/fastlane/report.xml index 94f1ff73e9..358fb9618c 100644 --- a/mobile/android/fastlane/report.xml +++ b/mobile/android/fastlane/report.xml @@ -5,17 +5,17 @@ - + - + - + diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 16fd92d821..828c9a63b7 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -2,7 +2,7 @@ name: immich_mobile description: Immich - selfhosted backup media file on mobile phone publish_to: 'none' -version: 1.102.3+134 +version: 1.102.3+136 environment: sdk: '>=3.0.0 <4.0.0' From a93534fc3c123abf756bcd22fd11b17e68c0f08d Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Sat, 20 Apr 2024 23:45:55 -0400 Subject: [PATCH 21/27] refactor(server): session interface types (#8977) --- server/src/interfaces/session.interface.ts | 10 ++++++---- server/src/repositories/session.repository.ts | 8 ++++---- server/test/repositories/session.repository.mock.ts | 4 ++-- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/server/src/interfaces/session.interface.ts b/server/src/interfaces/session.interface.ts index 3e2c9574a4..62c2ecec7b 100644 --- a/server/src/interfaces/session.interface.ts +++ b/server/src/interfaces/session.interface.ts @@ -2,10 +2,12 @@ import { SessionEntity } from 'src/entities/session.entity'; export const ISessionRepository = 'ISessionRepository'; +type E = SessionEntity; + export interface ISessionRepository { - create(dto: Partial): Promise; - update(dto: Partial): Promise; + create>(dto: T): Promise; + update>(dto: T): Promise; delete(id: string): Promise; - getByToken(token: string): Promise; - getByUserId(userId: string): Promise; + getByToken(token: string): Promise; + getByUserId(userId: string): Promise; } diff --git a/server/src/repositories/session.repository.ts b/server/src/repositories/session.repository.ts index 5e42039bc6..ed2da7a05f 100644 --- a/server/src/repositories/session.repository.ts +++ b/server/src/repositories/session.repository.ts @@ -31,12 +31,12 @@ export class SessionRepository implements ISessionRepository { }); } - create(session: Partial): Promise { - return this.repository.save(session); + create>(dto: T): Promise { + return this.repository.save(dto); } - update(session: Partial): Promise { - return this.repository.save(session); + update>(dto: T): Promise { + return this.repository.save(dto); } @GenerateSql({ params: [DummyValue.UUID] }) diff --git a/server/test/repositories/session.repository.mock.ts b/server/test/repositories/session.repository.mock.ts index 1a034e79f0..d510eb53f7 100644 --- a/server/test/repositories/session.repository.mock.ts +++ b/server/test/repositories/session.repository.mock.ts @@ -3,8 +3,8 @@ import { Mocked, vitest } from 'vitest'; export const newSessionRepositoryMock = (): Mocked => { return { - create: vitest.fn(), - update: vitest.fn(), + create: vitest.fn() as any, + update: vitest.fn() as any, delete: vitest.fn(), getByToken: vitest.fn(), getByUserId: vitest.fn(), From 7d4187962ad0b4ebc131390d39422b219a8e85d1 Mon Sep 17 00:00:00 2001 From: martin <74269598+martabal@users.noreply.github.com> Date: Sun, 21 Apr 2024 06:06:49 +0200 Subject: [PATCH 22/27] feat(web): new look option for slideshow (#8924) feat: new look option for slideshow --- .../asset-viewer/photo-viewer.svelte | 19 ++++++++++++++----- .../lib/components/slideshow-settings.svelte | 10 +++++++++- web/src/lib/stores/slideshow.store.ts | 2 ++ 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/web/src/lib/components/asset-viewer/photo-viewer.svelte b/web/src/lib/components/asset-viewer/photo-viewer.svelte index 6285a63006..2bf4ba4801 100644 --- a/web/src/lib/components/asset-viewer/photo-viewer.svelte +++ b/web/src/lib/components/asset-viewer/photo-viewer.svelte @@ -14,7 +14,7 @@ import LoadingSpinner from '../shared-components/loading-spinner.svelte'; import { NotificationType, notificationController } from '../shared-components/notification/notification'; import { getAltText } from '$lib/utils/thumbnail-util'; - import { slideshowLookCssMapping, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store'; + import { SlideshowLook, slideshowLookCssMapping, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store'; const { slideshowState, slideshowLook } = slideshowStore; @@ -150,15 +150,24 @@
{#if !imageLoaded} - +
+ +
{:else} -
+
+ {#if $slideshowState !== SlideshowState.None && $slideshowLook === SlideshowLook.BlurredBackground} + {getAltText(asset)} + {/if} {getAltText(asset)} - localProperties.load(reader) - } -} - -def flutterRoot = localProperties.getProperty('flutter.sdk') -if (flutterRoot == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") + localPropertiesFile.withInputStream { localProperties.load(it) } } def flutterVersionCode = localProperties.getProperty('flutter.versionCode') @@ -21,18 +21,12 @@ if (flutterVersionName == null) { flutterVersionName = '1.0' } -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' -apply plugin: 'kotlin-kapt' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" - def keystoreProperties = new Properties() def keystorePropertiesFile = rootProject.file('key.properties') if (keystorePropertiesFile.exists()) { - keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) + keystorePropertiesFile.withInputStream { keystoreProperties.load(it) } } - android { compileSdkVersion 34 @@ -50,7 +44,6 @@ android { } defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "app.alextran.immich" minSdkVersion 26 targetSdkVersion 33 @@ -88,6 +81,13 @@ flutter { } dependencies { + def kotlin_version = '1.9.23' + def kotlin_coroutines_version = '1.8.0' + def work_version = '2.9.0' + def concurrent_version = '1.1.0' + def guava_version = '33.1.0-android' + def glide_version = '4.16.0' + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version" implementation "androidx.work:work-runtime-ktx:$work_version" diff --git a/mobile/android/app/src/main/kotlin/com/example/mobile/BackupWorker.kt b/mobile/android/app/src/main/kotlin/com/example/mobile/BackupWorker.kt index 660e1d55ba..b6b78c2cba 100644 --- a/mobile/android/app/src/main/kotlin/com/example/mobile/BackupWorker.kt +++ b/mobile/android/app/src/main/kotlin/com/example/mobile/BackupWorker.kt @@ -276,7 +276,7 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct private const val NOTIFICATION_CHANNEL_ERROR_ID = "immich/backgroundServiceError" private const val NOTIFICATION_DEFAULT_TITLE = "Immich" private const val NOTIFICATION_ID = 1 - private const val NOTIFICATION_ERROR_ID = 2 + private const val NOTIFICATION_ERROR_ID = 2 private const val NOTIFICATION_DETAIL_ID = 3 private const val ONE_MINUTE = 60000L @@ -304,7 +304,7 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct val workInfoList = workInfoFuture.get(1000, TimeUnit.MILLISECONDS) if (workInfoList != null) { for (workInfo in workInfoList) { - if (workInfo.getState() == WorkInfo.State.ENQUEUED) { + if (workInfo.state == WorkInfo.State.ENQUEUED) { val workRequest = buildWorkRequest(requireWifi, requireCharging) wm.enqueueUniqueWork(TASK_NAME_BACKUP, ExistingWorkPolicy.REPLACE, workRequest) Log.d(TAG, "updateBackupWorker updated BackupWorker constraints") @@ -346,7 +346,7 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct .setRequiresBatteryNotLow(true) .setRequiresCharging(requireCharging) .build(); - + val work = OneTimeWorkRequest.Builder(BackupWorker::class.java) .setConstraints(constraints) .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, ONE_MINUTE, TimeUnit.MILLISECONDS) @@ -359,4 +359,4 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct } } -private const val TAG = "BackupWorker" \ No newline at end of file +private const val TAG = "BackupWorker" diff --git a/mobile/android/build.gradle b/mobile/android/build.gradle index 4dacde5a9d..5e374c9f64 100644 --- a/mobile/android/build.gradle +++ b/mobile/android/build.gradle @@ -1,21 +1,3 @@ -buildscript { - ext.kotlin_version = '1.8.20' - ext.kotlin_coroutines_version = '1.7.1' - ext.work_version = '2.7.1' - ext.concurrent_version = '1.1.0' - ext.guava_version = '33.0.0-android' - ext.glide_version = '4.14.2' - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:7.4.2' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} - allprojects { repositories { google() @@ -34,3 +16,7 @@ subprojects { tasks.register("clean", Delete) { delete rootProject.buildDir } + +tasks.named('wrapper') { + distributionType = Wrapper.DistributionType.ALL +} \ No newline at end of file diff --git a/mobile/android/gradle/wrapper/gradle-wrapper.properties b/mobile/android/gradle/wrapper/gradle-wrapper.properties index 7787882b74..6357330c9e 100644 --- a/mobile/android/gradle/wrapper/gradle-wrapper.properties +++ b/mobile/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip -distributionSha256Sum=6001aba9b2204d26fa25a5800bb9382cf3ee01ccb78fe77317b2872336eb2f80 \ No newline at end of file +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.4-all.zip +distributionSha256Sum=fe696c020f241a5f69c30f763c5a7f38eec54b490db19cd2b0962dda420d7d12 \ No newline at end of file diff --git a/mobile/android/settings.gradle b/mobile/android/settings.gradle index 44e62bcf06..7ea6533b65 100644 --- a/mobile/android/settings.gradle +++ b/mobile/android/settings.gradle @@ -1,11 +1,26 @@ -include ':app' +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + }() -def localPropertiesFile = new File(rootProject.projectDir, "local.properties") -def properties = new Properties() + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") -assert localPropertiesFile.exists() -localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} -def flutterSdkPath = properties.getProperty("flutter.sdk") -assert flutterSdkPath != null, "flutter.sdk not set in local.properties" -apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "7.4.2" apply false + id "org.jetbrains.kotlin.android" version "1.9.23" apply false + id "org.jetbrains.kotlin.kapt" version "1.9.23" apply false +} + +include ":app" diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 6bf2b09026..54648fd20b 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -1804,5 +1804,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.2.0 <4.0.0" + dart: ">=3.3.0 <4.0.0" flutter: ">=3.16.0" diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 828c9a63b7..8ae3a21318 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -5,7 +5,7 @@ publish_to: 'none' version: 1.102.3+136 environment: - sdk: '>=3.0.0 <4.0.0' + sdk: '>=3.3.0 <4.0.0' dependencies: flutter: From a99862120d5e3f6bf9b4fe19b571e5fa5b58ad80 Mon Sep 17 00:00:00 2001 From: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Sun, 21 Apr 2024 20:11:03 +0200 Subject: [PATCH 24/27] feat: mobile label for renovate pull requests (#8991) mobile lable for renovate pull requests --- renovate.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/renovate.json b/renovate.json index afa68011d0..3e2c50d7f1 100644 --- a/renovate.json +++ b/renovate.json @@ -27,7 +27,8 @@ "matchFileNames": ["mobile/**"], "groupName": "mobile", "matchUpdateTypes": ["minor", "patch"], - "schedule": "on tuesday" + "schedule": "on tuesday", + "addLabels": ["📱mobile"] }, { "groupName": "exiftool", From 21231d53a589f10acea804620c3219d47e6c063a Mon Sep 17 00:00:00 2001 From: clementdelestre <56797425+clementdelestre@users.noreply.github.com> Date: Sun, 21 Apr 2024 21:26:19 +0300 Subject: [PATCH 25/27] feat(mobile): add i18n in multiselect-grid and update translation (en and fr) (#8993) * add i18n in multiselect grid (en-fr) * add FR translations from (haptic feedback) * revert settings --- mobile/assets/i18n/en-US.json | 1 + mobile/assets/i18n/fr-FR.json | 5 ++++- mobile/lib/shared/ui/asset_grid/multiselect_grid.dart | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json index 1a6ca76757..46155d0c53 100644 --- a/mobile/assets/i18n/en-US.json +++ b/mobile/assets/i18n/en-US.json @@ -296,6 +296,7 @@ "motion_photos_page_title": "Motion Photos", "multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping", "multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping", + "no_assets_to_show" : "No assets to show", "notification_permission_dialog_cancel": "Cancel", "notification_permission_dialog_content": "To enable notifications, go to Settings and select allow.", "notification_permission_dialog_settings": "Settings", diff --git a/mobile/assets/i18n/fr-FR.json b/mobile/assets/i18n/fr-FR.json index eaa4fc06ed..f9ce46c4d5 100644 --- a/mobile/assets/i18n/fr-FR.json +++ b/mobile/assets/i18n/fr-FR.json @@ -296,6 +296,7 @@ "motion_photos_page_title": "Photos avec mouvement", "multiselect_grid_edit_date_time_err_read_only": "Impossible de modifier la date d'un élément d'actif en lecture seule.", "multiselect_grid_edit_gps_err_read_only": "Impossible de modifier l'emplacement d'un élément en lecture seule.", + "no_assets_to_show" : "Aucun élément à afficher", "notification_permission_dialog_cancel": "Annuler", "notification_permission_dialog_content": "Pour activer les notifications, allez dans Paramètres et sélectionnez Autoriser.", "notification_permission_dialog_settings": "Paramètres", @@ -509,5 +510,7 @@ "version_announcement_overlay_title": "Nouvelle version serveur disponible \uD83C\uDF89", "viewer_remove_from_stack": "Retirer de la pile", "viewer_stack_use_as_main_asset": "Utiliser comme élément principal", - "viewer_unstack": "Désempiler" + "viewer_unstack": "Désempiler", + "haptic_feedback_title": "Retour haptique", + "haptic_feedback_switch": "Activer le retour haptique" } \ No newline at end of file diff --git a/mobile/lib/shared/ui/asset_grid/multiselect_grid.dart b/mobile/lib/shared/ui/asset_grid/multiselect_grid.dart index c9cc6c04a9..482f1efc4f 100644 --- a/mobile/lib/shared/ui/asset_grid/multiselect_grid.dart +++ b/mobile/lib/shared/ui/asset_grid/multiselect_grid.dart @@ -63,7 +63,7 @@ class MultiselectGrid extends HookConsumerWidget { const Center(child: ImmichLoadingIndicator()); Widget buildEmptyIndicator() => - emptyIndicator ?? const Center(child: Text("No assets to show")); + emptyIndicator ?? Center(child: const Text("no_assets_to_show").tr()); @override Widget build(BuildContext context, WidgetRef ref) { From f004487be0d68d7c14891ee21d941607ee574d92 Mon Sep 17 00:00:00 2001 From: Conner Hnatiuk <46903591+ConnerWithAnE@users.noreply.github.com> Date: Sun, 21 Apr 2024 13:07:17 -0600 Subject: [PATCH 26/27] fix(web): trash page now auto refreshes (#8978) * fix(web): the trash page now auto refreshes when restore all or empty trash is clicked. Also shows number of assets affected. * formatting --- web/src/routes/(user)/trash/+page.svelte | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/web/src/routes/(user)/trash/+page.svelte b/web/src/routes/(user)/trash/+page.svelte index 5be97f2821..2728f25b02 100644 --- a/web/src/routes/(user)/trash/+page.svelte +++ b/web/src/routes/(user)/trash/+page.svelte @@ -39,8 +39,12 @@ try { await emptyTrash(); + const deletedAssetIds = assetStore.assets.map((a) => a.id); + const numberOfAssets = deletedAssetIds.length; + assetStore.removeAssets(deletedAssetIds); + notificationController.show({ - message: `Empty trash initiated. Refresh the page to see the changes`, + message: `Permanently deleted ${numberOfAssets} ${numberOfAssets == 1 ? 'asset' : 'assets'}`, type: NotificationType.Info, }); } catch (error) { @@ -52,8 +56,12 @@ try { await restoreTrash(); + const restoredAssetIds = assetStore.assets.map((a) => a.id); + const numberOfAssets = restoredAssetIds.length; + assetStore.removeAssets(restoredAssetIds); + notificationController.show({ - message: `Restore trash initiated. Refresh the page to see the changes`, + message: `Restored ${numberOfAssets} ${numberOfAssets == 1 ? 'asset' : 'assets'}`, type: NotificationType.Info, }); } catch (error) { From 0d3cc28f45607ebf62069fe898f8e1df6b7a586c Mon Sep 17 00:00:00 2001 From: TruongSinh Tran-Nguyen Date: Sun, 21 Apr 2024 12:14:54 -0700 Subject: [PATCH 27/27] feat(web): support 360 video (equirectangular) (#8762) * [web]: support 360 video * lint * lint * fix typing --------- Co-authored-by: Alex --- web/package-lock.json | 18 ++++++++++++ web/package.json | 2 ++ .../asset-viewer/asset-viewer.svelte | 5 +++- .../asset-viewer/panorama-viewer.svelte | 29 +++++++++++++++---- .../photo-sphere-viewer-adapter.svelte | 21 ++++++++++++-- ...ewer.svelte => video-native-viewer.svelte} | 0 .../asset-viewer/video-wrapper-viewer.svelte | 15 ++++++++++ 7 files changed, 80 insertions(+), 10 deletions(-) rename web/src/lib/components/asset-viewer/{video-viewer.svelte => video-native-viewer.svelte} (100%) create mode 100644 web/src/lib/components/asset-viewer/video-wrapper-viewer.svelte diff --git a/web/package-lock.json b/web/package-lock.json index 40f1d937c2..89b02cc4cd 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -12,6 +12,8 @@ "@immich/sdk": "file:../open-api/typescript-sdk", "@mdi/js": "^7.4.47", "@photo-sphere-viewer/core": "^5.7.1", + "@photo-sphere-viewer/equirectangular-video-adapter": "^5.7.2", + "@photo-sphere-viewer/video-plugin": "^5.7.2", "@zoom-image/svelte": "^0.2.6", "buffer": "^6.0.3", "copy-image-clipboard": "^2.1.2", @@ -1590,6 +1592,22 @@ "three": "^0.161.0" } }, + "node_modules/@photo-sphere-viewer/equirectangular-video-adapter": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/equirectangular-video-adapter/-/equirectangular-video-adapter-5.7.2.tgz", + "integrity": "sha512-cAaot52nPqa2p77Xp1humRvuxRIa8cqbZ/XRhA8kBToFLT1Ugh9YBcDD7pM/358JtAjicUbLpT7Ioap9iEigxQ==", + "peerDependencies": { + "@photo-sphere-viewer/core": "5.7.2" + } + }, + "node_modules/@photo-sphere-viewer/video-plugin": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/video-plugin/-/video-plugin-5.7.2.tgz", + "integrity": "sha512-vrPV9RCr4HsYiORkto1unDPeUkbN2kbyogvNUoLiQ78M4xkPOqoKxtfxCxTYoM+7gECwNL9VTF81+okck498qA==", + "peerDependencies": { + "@photo-sphere-viewer/core": "5.7.2" + } + }, "node_modules/@polka/url": { "version": "1.0.0-next.24", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.24.tgz", diff --git a/web/package.json b/web/package.json index b2bdd5afba..34c2ee83a3 100644 --- a/web/package.json +++ b/web/package.json @@ -61,6 +61,8 @@ "@immich/sdk": "file:../open-api/typescript-sdk", "@mdi/js": "^7.4.47", "@photo-sphere-viewer/core": "^5.7.1", + "@photo-sphere-viewer/equirectangular-video-adapter": "^5.7.2", + "@photo-sphere-viewer/video-plugin": "^5.7.2", "@zoom-image/svelte": "^0.2.6", "buffer": "^6.0.3", "copy-image-clipboard": "^2.1.2", diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index 40309e511f..28899a7525 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -50,7 +50,7 @@ import PanoramaViewer from './panorama-viewer.svelte'; import PhotoViewer from './photo-viewer.svelte'; import SlideshowBar from './slideshow-bar.svelte'; - import VideoViewer from './video-viewer.svelte'; + import VideoViewer from './video-wrapper-viewer.svelte'; export let assetStore: AssetStore | null = null; export let asset: AssetResponseDto; @@ -622,6 +622,7 @@ {:else} navigateAsset()} on:onVideoStarted={handleVideoStarted} @@ -642,6 +643,7 @@ {#if shouldPlayMotionPhoto && asset.livePhotoVideoId} (shouldPlayMotionPhoto = false)} /> @@ -655,6 +657,7 @@ {:else} navigateAsset()} on:onVideoStarted={handleVideoStarted} diff --git a/web/src/lib/components/asset-viewer/panorama-viewer.svelte b/web/src/lib/components/asset-viewer/panorama-viewer.svelte index 66d8f63099..592053e5b8 100644 --- a/web/src/lib/components/asset-viewer/panorama-viewer.svelte +++ b/web/src/lib/components/asset-viewer/panorama-viewer.svelte @@ -1,22 +1,39 @@
- {#await Promise.all([loadAssetData(), import('./photo-sphere-viewer-adapter.svelte')])} + {#await Promise.all([loadAssetData(), import('./photo-sphere-viewer-adapter.svelte'), ...photoSphereConfigs])} - {:then [data, module]} - + {:then [data, module, adapter, plugins, navbar]} + {:catch} Failed to load asset {/await} diff --git a/web/src/lib/components/asset-viewer/photo-sphere-viewer-adapter.svelte b/web/src/lib/components/asset-viewer/photo-sphere-viewer-adapter.svelte index 796622e7fe..0c0e707693 100644 --- a/web/src/lib/components/asset-viewer/photo-sphere-viewer-adapter.svelte +++ b/web/src/lib/components/asset-viewer/photo-sphere-viewer-adapter.svelte @@ -1,17 +1,32 @@ + +{#if projectionType === ProjectionType.EQUIRECTANGULAR} + +{:else} + +{/if}