Compare commits

...

6 Commits

Author SHA1 Message Date
Christopher Makarem
ccb23c2a5a
Merge 6f0a7d0638f8e90c6287d84c5aa1cb02b86854b4 into b0bcc6c03ecedff0d756a0e54352586672cea761 2024-10-02 08:55:49 +07:00
renovate[bot]
b0bcc6c03e
chore(deps): update typescript-projects (#13099)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-01 19:29:48 -04:00
Jason Rasmussen
63437529e1
refactor(server): config file env (#13100) 2024-10-01 16:03:55 -04:00
Christopher Makarem
6f0a7d0638 Add user avatars to web timelines 2024-09-06 15:25:18 -07:00
Christopher Makarem
8ab33b40da
Merge branch 'immich-app:main' into feat/user-avatars-web 2024-09-06 09:45:54 -07:00
Christopher Makarem
4ea652aeb8 inital proof of concept icons 2024-08-28 17:13:53 -07:00
56 changed files with 385 additions and 106 deletions

7
cli/package-lock.json generated
View File

@ -3148,10 +3148,11 @@
}
},
"node_modules/mock-fs": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/mock-fs/-/mock-fs-5.2.0.tgz",
"integrity": "sha512-2dF2R6YMSZbpip1V1WHKGLNjr/k48uQClqMVb5H3MOvwc9qhYis3/IWbj02qIg/Y8MDXKFF4c5v0rxx2o6xTZw==",
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/mock-fs/-/mock-fs-5.3.0.tgz",
"integrity": "sha512-IMvz1X+RF7vf+ur7qUenXMR7/FSKSIqS3HqFHXcyNI7G0FbpFO8L5lfsUJhl+bhK1AiulVHWKUSxebWauPA+xQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12.0.0"
}

View File

@ -10478,9 +10478,9 @@
"dev": true
},
"node_modules/mock-fs": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/mock-fs/-/mock-fs-5.2.0.tgz",
"integrity": "sha512-2dF2R6YMSZbpip1V1WHKGLNjr/k48uQClqMVb5H3MOvwc9qhYis3/IWbj02qIg/Y8MDXKFF4c5v0rxx2o6xTZw==",
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/mock-fs/-/mock-fs-5.3.0.tgz",
"integrity": "sha512-IMvz1X+RF7vf+ur7qUenXMR7/FSKSIqS3HqFHXcyNI7G0FbpFO8L5lfsUJhl+bhK1AiulVHWKUSxebWauPA+xQ==",
"dev": true,
"engines": {
"node": ">=12.0.0"
@ -22506,9 +22506,9 @@
"dev": true
},
"mock-fs": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/mock-fs/-/mock-fs-5.2.0.tgz",
"integrity": "sha512-2dF2R6YMSZbpip1V1WHKGLNjr/k48uQClqMVb5H3MOvwc9qhYis3/IWbj02qIg/Y8MDXKFF4c5v0rxx2o6xTZw==",
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/mock-fs/-/mock-fs-5.3.0.tgz",
"integrity": "sha512-IMvz1X+RF7vf+ur7qUenXMR7/FSKSIqS3HqFHXcyNI7G0FbpFO8L5lfsUJhl+bhK1AiulVHWKUSxebWauPA+xQ==",
"dev": true
},
"module-details-from-path": {

View File

@ -5,6 +5,7 @@ import { AssetEntity } from 'src/entities/asset.entity';
import { PersonEntity } from 'src/entities/person.entity';
import { AssetFileType, AssetPathType, ImageFormat, PathType, PersonPathType, StorageFolder } from 'src/enum';
import { IAssetRepository } from 'src/interfaces/asset.interface';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { IMoveRepository } from 'src/interfaces/move.interface';
@ -36,6 +37,7 @@ let instance: StorageCore | null;
export class StorageCore {
private constructor(
private assetRepository: IAssetRepository,
private configRepository: IConfigRepository,
private cryptoRepository: ICryptoRepository,
private moveRepository: IMoveRepository,
private personRepository: IPersonRepository,
@ -46,6 +48,7 @@ export class StorageCore {
static create(
assetRepository: IAssetRepository,
configRepository: IConfigRepository,
cryptoRepository: ICryptoRepository,
moveRepository: IMoveRepository,
personRepository: IPersonRepository,
@ -56,6 +59,7 @@ export class StorageCore {
if (!instance) {
instance = new StorageCore(
assetRepository,
configRepository,
cryptoRepository,
moveRepository,
personRepository,
@ -245,7 +249,11 @@ export class StorageCore {
this.logger.warn(`Unable to complete move. File size mismatch: ${newPathSize} !== ${oldPathSize}`);
return false;
}
const repos = { metadataRepo: this.systemMetadataRepository, logger: this.logger };
const repos = {
configRepo: this.configRepository,
metadataRepo: this.systemMetadataRepository,
logger: this.logger,
};
const config = await getConfig(repos, { withCache: true });
if (assetInfo && config.storageTemplate.hashVerificationEnabled) {
const { checksum } = assetInfo;

View File

@ -3,6 +3,7 @@ import { VectorExtension } from 'src/interfaces/database.interface';
export const IConfigRepository = 'IConfigRepository';
export interface EnvData {
configFile?: string;
database: {
skipMigrations: boolean;
vectorExtension: VectorExtension;

View File

@ -6,6 +6,7 @@ import { EnvData, IConfigRepository } from 'src/interfaces/config.interface';
export class ConfigRepository implements IConfigRepository {
getEnv(): EnvData {
return {
configFile: process.env.IMMICH_CONFIG_FILE,
database: {
skipMigrations: process.env.DB_SKIP_MIGRATIONS === 'true',
vectorExtension: getVectorExtension(),

View File

@ -4,6 +4,7 @@ import { AssetJobName, AssetStatsResponseDto } from 'src/dtos/asset.dto';
import { AssetEntity } from 'src/entities/asset.entity';
import { AssetStatus, AssetType } from 'src/enum';
import { AssetStats, IAssetRepository } from 'src/interfaces/asset.interface';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { IEventRepository } from 'src/interfaces/event.interface';
import { IJobRepository, JobName } from 'src/interfaces/job.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
@ -19,6 +20,7 @@ import { partnerStub } from 'test/fixtures/partner.stub';
import { userStub } from 'test/fixtures/user.stub';
import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock';
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
import { newEventRepositoryMock } from 'test/repositories/event.repository.mock';
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
@ -45,6 +47,7 @@ describe(AssetService.name, () => {
let sut: AssetService;
let accessMock: IAccessRepositoryMock;
let assetMock: Mocked<IAssetRepository>;
let configMock: Mocked<IConfigRepository>;
let jobMock: Mocked<IJobRepository>;
let userMock: Mocked<IUserRepository>;
let eventMock: Mocked<IEventRepository>;
@ -66,6 +69,7 @@ describe(AssetService.name, () => {
beforeEach(() => {
accessMock = newAccessRepositoryMock();
assetMock = newAssetRepositoryMock();
configMock = newConfigRepositoryMock();
eventMock = newEventRepositoryMock();
jobMock = newJobRepositoryMock();
userMock = newUserRepositoryMock();
@ -77,6 +81,7 @@ describe(AssetService.name, () => {
sut = new AssetService(
accessMock,
assetMock,
configMock,
jobMock,
systemMock,
userMock,

View File

@ -22,6 +22,7 @@ import { AssetEntity } from 'src/entities/asset.entity';
import { AssetStatus, Permission } from 'src/enum';
import { IAccessRepository } from 'src/interfaces/access.interface';
import { IAssetRepository } from 'src/interfaces/asset.interface';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { IEventRepository } from 'src/interfaces/event.interface';
import {
IAssetDeleteJob,
@ -46,6 +47,7 @@ export class AssetService extends BaseService {
constructor(
@Inject(IAccessRepository) private access: IAccessRepository,
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
@Inject(IConfigRepository) configRepository: IConfigRepository,
@Inject(IJobRepository) private jobRepository: IJobRepository,
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
@Inject(IUserRepository) private userRepository: IUserRepository,
@ -54,7 +56,7 @@ export class AssetService extends BaseService {
@Inject(IStackRepository) private stackRepository: IStackRepository,
@Inject(ILoggerRepository) logger: ILoggerRepository,
) {
super(systemMetadataRepository, logger);
super(configRepository, systemMetadataRepository, logger);
this.logger.setContext(AssetService.name);
}

View File

@ -5,6 +5,7 @@ import { UserMetadataEntity } from 'src/entities/user-metadata.entity';
import { UserEntity } from 'src/entities/user.entity';
import { AuthType } from 'src/enum';
import { IKeyRepository } from 'src/interfaces/api-key.interface';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { IEventRepository } from 'src/interfaces/event.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
@ -20,6 +21,7 @@ import { sharedLinkStub } from 'test/fixtures/shared-link.stub';
import { systemConfigStub } from 'test/fixtures/system-config.stub';
import { userStub } from 'test/fixtures/user.stub';
import { newKeyRepositoryMock } from 'test/repositories/api-key.repository.mock';
import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
import { newEventRepositoryMock } from 'test/repositories/event.repository.mock';
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
@ -57,6 +59,7 @@ const oauthUserWithDefaultQuota = {
describe('AuthService', () => {
let sut: AuthService;
let configMock: Mocked<IConfigRepository>;
let cryptoMock: Mocked<ICryptoRepository>;
let eventMock: Mocked<IEventRepository>;
let userMock: Mocked<IUserRepository>;
@ -89,6 +92,7 @@ describe('AuthService', () => {
}),
} as any);
configMock = newConfigRepositoryMock();
cryptoMock = newCryptoRepositoryMock();
eventMock = newEventRepositoryMock();
userMock = newUserRepositoryMock();
@ -98,7 +102,17 @@ describe('AuthService', () => {
shareMock = newSharedLinkRepositoryMock();
keyMock = newKeyRepositoryMock();
sut = new AuthService(cryptoMock, eventMock, systemMock, loggerMock, userMock, sessionMock, shareMock, keyMock);
sut = new AuthService(
configMock,
cryptoMock,
eventMock,
systemMock,
loggerMock,
userMock,
sessionMock,
shareMock,
keyMock,
);
});
it('should be defined', () => {

View File

@ -31,6 +31,7 @@ import { UserAdminResponseDto, mapUserAdmin } from 'src/dtos/user.dto';
import { UserEntity } from 'src/entities/user.entity';
import { AuthType, Permission } from 'src/enum';
import { IKeyRepository } from 'src/interfaces/api-key.interface';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { IEventRepository } from 'src/interfaces/event.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
@ -72,6 +73,7 @@ export type ValidateRequest = {
@Injectable()
export class AuthService extends BaseService {
constructor(
@Inject(IConfigRepository) configRepository: IConfigRepository,
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
@Inject(IEventRepository) private eventRepository: IEventRepository,
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
@ -81,7 +83,7 @@ export class AuthService extends BaseService {
@Inject(ISharedLinkRepository) private sharedLinkRepository: ISharedLinkRepository,
@Inject(IKeyRepository) private keyRepository: IKeyRepository,
) {
super(systemMetadataRepository, logger);
super(configRepository, systemMetadataRepository, logger);
this.logger.setContext(AuthService.name);
custom.setHttpOptionsDefaults({ timeout: 30_000 });

View File

@ -1,32 +1,30 @@
import { Inject } from '@nestjs/common';
import { SystemConfig } from 'src/config';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { getConfig, updateConfig } from 'src/utils/config';
export class BaseService {
constructor(
@Inject(IConfigRepository) protected configRepository: IConfigRepository,
@Inject(ISystemMetadataRepository) protected systemMetadataRepository: ISystemMetadataRepository,
@Inject(ILoggerRepository) protected logger: ILoggerRepository,
) {}
private get repos() {
return {
configRepo: this.configRepository,
metadataRepo: this.systemMetadataRepository,
logger: this.logger,
};
}
getConfig(options: { withCache: boolean }) {
return getConfig(
{
metadataRepo: this.systemMetadataRepository,
logger: this.logger,
},
options,
);
return getConfig(this.repos, options);
}
updateConfig(newConfig: SystemConfig) {
return updateConfig(
{
metadataRepo: this.systemMetadataRepository,
logger: this.logger,
},
newConfig,
);
return updateConfig(this.repos, newConfig);
}
}

View File

@ -1,9 +1,11 @@
import { IConfigRepository } from 'src/interfaces/config.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { IUserRepository } from 'src/interfaces/user.interface';
import { CliService } from 'src/services/cli.service';
import { userStub } from 'test/fixtures/user.stub';
import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
@ -13,18 +15,20 @@ import { Mocked, describe, it } from 'vitest';
describe(CliService.name, () => {
let sut: CliService;
let userMock: Mocked<IUserRepository>;
let configMock: Mocked<IConfigRepository>;
let cryptoMock: Mocked<ICryptoRepository>;
let userMock: Mocked<IUserRepository>;
let systemMock: Mocked<ISystemMetadataRepository>;
let loggerMock: Mocked<ILoggerRepository>;
beforeEach(() => {
configMock = newConfigRepositoryMock();
cryptoMock = newCryptoRepositoryMock();
systemMock = newSystemMetadataRepositoryMock();
userMock = newUserRepositoryMock();
loggerMock = newLoggerRepositoryMock();
sut = new CliService(cryptoMock, systemMock, userMock, loggerMock);
sut = new CliService(configMock, cryptoMock, systemMock, userMock, loggerMock);
});
describe('resetAdminPassword', () => {

View File

@ -1,6 +1,7 @@
import { Inject, Injectable } from '@nestjs/common';
import { SALT_ROUNDS } from 'src/constants';
import { UserAdminResponseDto, mapUserAdmin } from 'src/dtos/user.dto';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
@ -10,12 +11,13 @@ import { BaseService } from 'src/services/base.service';
@Injectable()
export class CliService extends BaseService {
constructor(
@Inject(IConfigRepository) configRepository: IConfigRepository,
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
@Inject(IUserRepository) private userRepository: IUserRepository,
@Inject(ILoggerRepository) logger: ILoggerRepository,
) {
super(systemMetadataRepository, logger);
super(configRepository, systemMetadataRepository, logger);
this.logger.setContext(CliService.name);
}

View File

@ -1,4 +1,5 @@
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
@ -8,6 +9,7 @@ import { DuplicateService } from 'src/services/duplicate.service';
import { SearchService } from 'src/services/search.service';
import { assetStub } from 'test/fixtures/asset.stub';
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
@ -20,6 +22,7 @@ vitest.useFakeTimers();
describe(SearchService.name, () => {
let sut: DuplicateService;
let assetMock: Mocked<IAssetRepository>;
let configMock: Mocked<IConfigRepository>;
let systemMock: Mocked<ISystemMetadataRepository>;
let searchMock: Mocked<ISearchRepository>;
let loggerMock: Mocked<ILoggerRepository>;
@ -28,13 +31,14 @@ describe(SearchService.name, () => {
beforeEach(() => {
assetMock = newAssetRepositoryMock();
configMock = newConfigRepositoryMock();
systemMock = newSystemMetadataRepositoryMock();
searchMock = newSearchRepositoryMock();
loggerMock = newLoggerRepositoryMock();
cryptoMock = newCryptoRepositoryMock();
jobMock = newJobRepositoryMock();
sut = new DuplicateService(systemMock, searchMock, assetMock, loggerMock, cryptoMock, jobMock);
sut = new DuplicateService(configMock, systemMock, searchMock, assetMock, loggerMock, cryptoMock, jobMock);
});
it('should work', () => {

View File

@ -4,6 +4,7 @@ import { AuthDto } from 'src/dtos/auth.dto';
import { DuplicateResponseDto, mapDuplicateResponse } from 'src/dtos/duplicate.dto';
import { AssetEntity } from 'src/entities/asset.entity';
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import {
IBaseJob,
@ -24,6 +25,7 @@ import { usePagination } from 'src/utils/pagination';
@Injectable()
export class DuplicateService extends BaseService {
constructor(
@Inject(IConfigRepository) configRepository: IConfigRepository,
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
@Inject(ISearchRepository) private searchRepository: ISearchRepository,
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
@ -31,7 +33,7 @@ export class DuplicateService extends BaseService {
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
@Inject(IJobRepository) private jobRepository: IJobRepository,
) {
super(systemMetadataRepository, logger);
super(configRepository, systemMetadataRepository, logger);
this.logger.setContext(DuplicateService.name);
}

View File

@ -1,6 +1,7 @@
import { BadRequestException } from '@nestjs/common';
import { defaults } from 'src/config';
import { IAssetRepository } from 'src/interfaces/asset.interface';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { IEventRepository } from 'src/interfaces/event.interface';
import {
IJobRepository,
@ -18,6 +19,7 @@ import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interf
import { JobService } from 'src/services/job.service';
import { assetStub } from 'test/fixtures/asset.stub';
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
import { newEventRepositoryMock } from 'test/repositories/event.repository.mock';
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
@ -37,6 +39,7 @@ const makeMockHandlers = (status: JobStatus) => {
describe(JobService.name, () => {
let sut: JobService;
let assetMock: Mocked<IAssetRepository>;
let configMock: Mocked<IConfigRepository>;
let eventMock: Mocked<IEventRepository>;
let jobMock: Mocked<IJobRepository>;
let personMock: Mocked<IPersonRepository>;
@ -46,13 +49,14 @@ describe(JobService.name, () => {
beforeEach(() => {
assetMock = newAssetRepositoryMock();
configMock = newConfigRepositoryMock();
systemMock = newSystemMetadataRepositoryMock();
eventMock = newEventRepositoryMock();
jobMock = newJobRepositoryMock();
personMock = newPersonRepositoryMock();
metricMock = newMetricRepositoryMock();
loggerMock = newLoggerRepositoryMock();
sut = new JobService(assetMock, eventMock, jobMock, systemMock, personMock, metricMock, loggerMock);
sut = new JobService(assetMock, configMock, eventMock, jobMock, systemMock, personMock, metricMock, loggerMock);
});
it('should work', () => {

View File

@ -5,6 +5,7 @@ import { mapAsset } from 'src/dtos/asset-response.dto';
import { AllJobStatusResponseDto, JobCommandDto, JobCreateDto, JobStatusDto } from 'src/dtos/job.dto';
import { AssetType, ManualJobName } from 'src/enum';
import { IAssetRepository } from 'src/interfaces/asset.interface';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { ArgOf, IEventRepository } from 'src/interfaces/event.interface';
import {
ConcurrentQueueName,
@ -49,6 +50,7 @@ export class JobService extends BaseService {
constructor(
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
@Inject(IConfigRepository) configRepository: IConfigRepository,
@Inject(IEventRepository) private eventRepository: IEventRepository,
@Inject(IJobRepository) private jobRepository: IJobRepository,
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
@ -56,7 +58,7 @@ export class JobService extends BaseService {
@Inject(IMetricRepository) private metricRepository: IMetricRepository,
@Inject(ILoggerRepository) logger: ILoggerRepository,
) {
super(systemMetadataRepository, logger);
super(configRepository, systemMetadataRepository, logger);
this.logger.setContext(JobService.name);
}

View File

@ -5,6 +5,7 @@ import { mapLibrary } from 'src/dtos/library.dto';
import { UserEntity } from 'src/entities/user.entity';
import { AssetType } from 'src/enum';
import { IAssetRepository } from 'src/interfaces/asset.interface';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { IDatabaseRepository } from 'src/interfaces/database.interface';
import {
@ -26,6 +27,7 @@ import { libraryStub } from 'test/fixtures/library.stub';
import { systemConfigStub } from 'test/fixtures/system-config.stub';
import { userStub } from 'test/fixtures/user.stub';
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock';
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
@ -43,15 +45,17 @@ describe(LibraryService.name, () => {
let sut: LibraryService;
let assetMock: Mocked<IAssetRepository>;
let systemMock: Mocked<ISystemMetadataRepository>;
let configMock: Mocked<IConfigRepository>;
let cryptoMock: Mocked<ICryptoRepository>;
let databaseMock: Mocked<IDatabaseRepository>;
let jobMock: Mocked<IJobRepository>;
let libraryMock: Mocked<ILibraryRepository>;
let storageMock: Mocked<IStorageRepository>;
let databaseMock: Mocked<IDatabaseRepository>;
let systemMock: Mocked<ISystemMetadataRepository>;
let loggerMock: Mocked<ILoggerRepository>;
beforeEach(() => {
configMock = newConfigRepositoryMock();
systemMock = newSystemMetadataRepositoryMock();
libraryMock = newLibraryRepositoryMock();
assetMock = newAssetRepositoryMock();
@ -63,12 +67,13 @@ describe(LibraryService.name, () => {
sut = new LibraryService(
assetMock,
systemMock,
configMock,
cryptoMock,
databaseMock,
jobMock,
libraryMock,
storageMock,
databaseMock,
systemMock,
loggerMock,
);

View File

@ -18,6 +18,7 @@ import { AssetEntity } from 'src/entities/asset.entity';
import { LibraryEntity } from 'src/entities/library.entity';
import { AssetType } from 'src/enum';
import { IAssetRepository } from 'src/interfaces/asset.interface';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface';
import { ArgOf } from 'src/interfaces/event.interface';
@ -48,15 +49,16 @@ export class LibraryService extends BaseService {
constructor(
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
@Inject(IConfigRepository) configRepository: IConfigRepository,
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
@Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository,
@Inject(IJobRepository) private jobRepository: IJobRepository,
@Inject(ILibraryRepository) private repository: ILibraryRepository,
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
@Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository,
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
@Inject(ILoggerRepository) logger: ILoggerRepository,
) {
super(systemMetadataRepository, logger);
super(configRepository, systemMetadataRepository, logger);
this.logger.setContext(LibraryService.name);
}

View File

@ -12,6 +12,7 @@ import {
VideoCodec,
} from 'src/enum';
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
@ -26,6 +27,7 @@ import { faceStub } from 'test/fixtures/face.stub';
import { probeStub } from 'test/fixtures/media.stub';
import { personStub } from 'test/fixtures/person.stub';
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
@ -39,6 +41,7 @@ import { Mocked } from 'vitest';
describe(MediaService.name, () => {
let sut: MediaService;
let assetMock: Mocked<IAssetRepository>;
let configMock: Mocked<IConfigRepository>;
let jobMock: Mocked<IJobRepository>;
let mediaMock: Mocked<IMediaRepository>;
let moveMock: Mocked<IMoveRepository>;
@ -50,6 +53,7 @@ describe(MediaService.name, () => {
beforeEach(() => {
assetMock = newAssetRepositoryMock();
configMock = newConfigRepositoryMock();
systemMock = newSystemMetadataRepositoryMock();
jobMock = newJobRepositoryMock();
mediaMock = newMediaRepositoryMock();
@ -61,6 +65,7 @@ describe(MediaService.name, () => {
sut = new MediaService(
assetMock,
configMock,
personMock,
jobMock,
mediaMock,

View File

@ -18,6 +18,7 @@ import {
VideoContainer,
} from 'src/enum';
import { IAssetRepository, UpsertFileOptions, WithoutProperty } from 'src/interfaces/asset.interface';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import {
IBaseJob,
@ -55,6 +56,7 @@ export class MediaService extends BaseService {
constructor(
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
@Inject(IConfigRepository) configRepository: IConfigRepository,
@Inject(IPersonRepository) private personRepository: IPersonRepository,
@Inject(IJobRepository) private jobRepository: IJobRepository,
@Inject(IMediaRepository) private mediaRepository: IMediaRepository,
@ -64,10 +66,11 @@ export class MediaService extends BaseService {
@Inject(ICryptoRepository) cryptoRepository: ICryptoRepository,
@Inject(ILoggerRepository) logger: ILoggerRepository,
) {
super(systemMetadataRepository, logger);
super(configRepository, systemMetadataRepository, logger);
this.logger.setContext(MediaService.name);
this.storageCore = StorageCore.create(
assetRepository,
configRepository,
cryptoRepository,
moveRepository,
personRepository,

View File

@ -6,6 +6,7 @@ import { ExifEntity } from 'src/entities/exif.entity';
import { AssetType, SourceType } from 'src/enum';
import { IAlbumRepository } from 'src/interfaces/album.interface';
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { IDatabaseRepository } from 'src/interfaces/database.interface';
import { IEventRepository } from 'src/interfaces/event.interface';
@ -29,6 +30,7 @@ import { personStub } from 'test/fixtures/person.stub';
import { tagStub } from 'test/fixtures/tag.stub';
import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock';
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock';
import { newEventRepositoryMock } from 'test/repositories/event.repository.mock';
@ -46,10 +48,14 @@ import { newUserRepositoryMock } from 'test/repositories/user.repository.mock';
import { Mocked } from 'vitest';
describe(MetadataService.name, () => {
let sut: MetadataService;
let albumMock: Mocked<IAlbumRepository>;
let assetMock: Mocked<IAssetRepository>;
let systemMock: Mocked<ISystemMetadataRepository>;
let configMock: Mocked<IConfigRepository>;
let cryptoRepository: Mocked<ICryptoRepository>;
let databaseMock: Mocked<IDatabaseRepository>;
let eventMock: Mocked<IEventRepository>;
let jobMock: Mocked<IJobRepository>;
let mapMock: Mocked<IMapRepository>;
let metadataMock: Mocked<IMetadataRepository>;
@ -57,16 +63,15 @@ describe(MetadataService.name, () => {
let mediaMock: Mocked<IMediaRepository>;
let personMock: Mocked<IPersonRepository>;
let storageMock: Mocked<IStorageRepository>;
let eventMock: Mocked<IEventRepository>;
let databaseMock: Mocked<IDatabaseRepository>;
let systemMock: Mocked<ISystemMetadataRepository>;
let tagMock: Mocked<ITagRepository>;
let userMock: Mocked<IUserRepository>;
let loggerMock: Mocked<ILoggerRepository>;
let tagMock: Mocked<ITagRepository>;
let sut: MetadataService;
beforeEach(() => {
albumMock = newAlbumRepositoryMock();
assetMock = newAssetRepositoryMock();
configMock = newConfigRepositoryMock();
cryptoRepository = newCryptoRepositoryMock();
jobMock = newJobRepositoryMock();
mapMock = newMapRepositoryMock();
@ -85,9 +90,10 @@ describe(MetadataService.name, () => {
sut = new MetadataService(
albumMock,
assetMock,
eventMock,
configMock,
cryptoRepository,
databaseMock,
eventMock,
jobMock,
mapMock,
mediaMock,

View File

@ -15,6 +15,7 @@ import { PersonEntity } from 'src/entities/person.entity';
import { AssetType, SourceType } from 'src/enum';
import { IAlbumRepository } from 'src/interfaces/album.interface';
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface';
import { ArgOf, IEventRepository } from 'src/interfaces/event.interface';
@ -103,9 +104,10 @@ export class MetadataService extends BaseService {
constructor(
@Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
@Inject(IEventRepository) private eventRepository: IEventRepository,
@Inject(IConfigRepository) configRepository: IConfigRepository,
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
@Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository,
@Inject(IEventRepository) private eventRepository: IEventRepository,
@Inject(IJobRepository) private jobRepository: IJobRepository,
@Inject(IMapRepository) private mapRepository: IMapRepository,
@Inject(IMediaRepository) private mediaRepository: IMediaRepository,
@ -118,10 +120,11 @@ export class MetadataService extends BaseService {
@Inject(IUserRepository) private userRepository: IUserRepository,
@Inject(ILoggerRepository) logger: ILoggerRepository,
) {
super(systemMetadataRepository, logger);
super(configRepository, systemMetadataRepository, logger);
this.logger.setContext(MetadataService.name);
this.storageCore = StorageCore.create(
assetRepository,
configRepository,
cryptoRepository,
moveRepository,
personRepository,

View File

@ -6,6 +6,7 @@ import { AssetFileEntity } from 'src/entities/asset-files.entity';
import { AssetFileType, UserMetadataKey } from 'src/enum';
import { IAlbumRepository } from 'src/interfaces/album.interface';
import { IAssetRepository } from 'src/interfaces/asset.interface';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { IEventRepository } from 'src/interfaces/event.interface';
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
@ -18,6 +19,7 @@ import { assetStub } from 'test/fixtures/asset.stub';
import { userStub } from 'test/fixtures/user.stub';
import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock';
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
import { newEventRepositoryMock } from 'test/repositories/event.repository.mock';
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
@ -66,6 +68,7 @@ const configs = {
describe(NotificationService.name, () => {
let albumMock: Mocked<IAlbumRepository>;
let assetMock: Mocked<IAssetRepository>;
let configMock: Mocked<IConfigRepository>;
let eventMock: Mocked<IEventRepository>;
let jobMock: Mocked<IJobRepository>;
let loggerMock: Mocked<ILoggerRepository>;
@ -77,6 +80,7 @@ describe(NotificationService.name, () => {
beforeEach(() => {
albumMock = newAlbumRepositoryMock();
assetMock = newAssetRepositoryMock();
configMock = newConfigRepositoryMock();
eventMock = newEventRepositoryMock();
jobMock = newJobRepositoryMock();
loggerMock = newLoggerRepositoryMock();
@ -85,6 +89,7 @@ describe(NotificationService.name, () => {
userMock = newUserRepositoryMock();
sut = new NotificationService(
configMock,
eventMock,
systemMock,
notificationMock,

View File

@ -5,6 +5,7 @@ import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto';
import { AlbumEntity } from 'src/entities/album.entity';
import { IAlbumRepository } from 'src/interfaces/album.interface';
import { IAssetRepository } from 'src/interfaces/asset.interface';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { ArgOf, IEventRepository } from 'src/interfaces/event.interface';
import {
IEmailJob,
@ -28,6 +29,7 @@ import { getPreferences } from 'src/utils/preferences';
@Injectable()
export class NotificationService extends BaseService {
constructor(
@Inject(IConfigRepository) configRepository: IConfigRepository,
@Inject(IEventRepository) private eventRepository: IEventRepository,
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
@Inject(INotificationRepository) private notificationRepository: INotificationRepository,
@ -37,7 +39,7 @@ export class NotificationService extends BaseService {
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
@Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
) {
super(systemMetadataRepository, logger);
super(configRepository, systemMetadataRepository, logger);
this.logger.setContext(NotificationService.name);
}

View File

@ -4,6 +4,7 @@ import { PersonResponseDto, mapFaces, mapPerson } from 'src/dtos/person.dto';
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
import { CacheControl, Colorspace, ImageFormat, SourceType, SystemMetadataKey } from 'src/enum';
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
@ -23,6 +24,7 @@ import { personStub } from 'test/fixtures/person.stub';
import { systemConfigStub } from 'test/fixtures/system-config.stub';
import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock';
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
@ -67,6 +69,7 @@ const detectFaceMock: DetectedFaces = {
describe(PersonService.name, () => {
let accessMock: IAccessRepositoryMock;
let assetMock: Mocked<IAssetRepository>;
let configMock: Mocked<IConfigRepository>;
let systemMock: Mocked<ISystemMetadataRepository>;
let jobMock: Mocked<IJobRepository>;
let machineLearningMock: Mocked<IMachineLearningRepository>;
@ -82,6 +85,7 @@ describe(PersonService.name, () => {
beforeEach(() => {
accessMock = newAccessRepositoryMock();
assetMock = newAssetRepositoryMock();
configMock = newConfigRepositoryMock();
systemMock = newSystemMetadataRepositoryMock();
jobMock = newJobRepositoryMock();
machineLearningMock = newMachineLearningRepositoryMock();
@ -92,9 +96,11 @@ describe(PersonService.name, () => {
searchMock = newSearchRepositoryMock();
cryptoMock = newCryptoRepositoryMock();
loggerMock = newLoggerRepositoryMock();
sut = new PersonService(
accessMock,
assetMock,
configMock,
machineLearningMock,
moveMock,
mediaMock,

View File

@ -33,6 +33,7 @@ import {
} from 'src/enum';
import { IAccessRepository } from 'src/interfaces/access.interface';
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import {
IBaseJob,
@ -70,6 +71,7 @@ export class PersonService extends BaseService {
constructor(
@Inject(IAccessRepository) private access: IAccessRepository,
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
@Inject(IConfigRepository) configRepository: IConfigRepository,
@Inject(IMachineLearningRepository) private machineLearningRepository: IMachineLearningRepository,
@Inject(IMoveRepository) moveRepository: IMoveRepository,
@Inject(IMediaRepository) private mediaRepository: IMediaRepository,
@ -81,10 +83,11 @@ export class PersonService extends BaseService {
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
@Inject(ILoggerRepository) logger: ILoggerRepository,
) {
super(systemMetadataRepository, logger);
super(configRepository, systemMetadataRepository, logger);
this.logger.setContext(PersonService.name);
this.storageCore = StorageCore.create(
assetRepository,
configRepository,
cryptoRepository,
moveRepository,
repository,

View File

@ -1,6 +1,7 @@
import { mapAsset } from 'src/dtos/asset-response.dto';
import { SearchSuggestionType } from 'src/dtos/search.dto';
import { IAssetRepository } from 'src/interfaces/asset.interface';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface';
import { IPartnerRepository } from 'src/interfaces/partner.interface';
@ -12,6 +13,7 @@ import { assetStub } from 'test/fixtures/asset.stub';
import { authStub } from 'test/fixtures/auth.stub';
import { personStub } from 'test/fixtures/person.stub';
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
import { newMachineLearningRepositoryMock } from 'test/repositories/machine-learning.repository.mock';
import { newPartnerRepositoryMock } from 'test/repositories/partner.repository.mock';
@ -25,6 +27,7 @@ vitest.useFakeTimers();
describe(SearchService.name, () => {
let sut: SearchService;
let assetMock: Mocked<IAssetRepository>;
let configMock: Mocked<IConfigRepository>;
let systemMock: Mocked<ISystemMetadataRepository>;
let machineMock: Mocked<IMachineLearningRepository>;
let personMock: Mocked<IPersonRepository>;
@ -34,6 +37,7 @@ describe(SearchService.name, () => {
beforeEach(() => {
assetMock = newAssetRepositoryMock();
configMock = newConfigRepositoryMock();
systemMock = newSystemMetadataRepositoryMock();
machineMock = newMachineLearningRepositoryMock();
personMock = newPersonRepositoryMock();
@ -41,7 +45,16 @@ describe(SearchService.name, () => {
partnerMock = newPartnerRepositoryMock();
loggerMock = newLoggerRepositoryMock();
sut = new SearchService(systemMock, machineMock, personMock, searchMock, assetMock, partnerMock, loggerMock);
sut = new SearchService(
configMock,
systemMock,
machineMock,
personMock,
searchMock,
assetMock,
partnerMock,
loggerMock,
);
});
it('should work', () => {

View File

@ -17,6 +17,7 @@ import {
import { AssetEntity } from 'src/entities/asset.entity';
import { AssetOrder } from 'src/enum';
import { IAssetRepository } from 'src/interfaces/asset.interface';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface';
import { IPartnerRepository } from 'src/interfaces/partner.interface';
@ -30,6 +31,7 @@ import { isSmartSearchEnabled } from 'src/utils/misc';
@Injectable()
export class SearchService extends BaseService {
constructor(
@Inject(IConfigRepository) configRepository: IConfigRepository,
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
@Inject(IMachineLearningRepository) private machineLearning: IMachineLearningRepository,
@Inject(IPersonRepository) private personRepository: IPersonRepository,
@ -38,7 +40,7 @@ export class SearchService extends BaseService {
@Inject(IPartnerRepository) private partnerRepository: IPartnerRepository,
@Inject(ILoggerRepository) logger: ILoggerRepository,
) {
super(systemMetadataRepository, logger);
super(configRepository, systemMetadataRepository, logger);
this.logger.setContext(SearchService.name);
}

View File

@ -1,4 +1,5 @@
import { SystemMetadataKey } from 'src/enum';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
@ -6,6 +7,7 @@ import { IStorageRepository } from 'src/interfaces/storage.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { IUserRepository } from 'src/interfaces/user.interface';
import { ServerService } from 'src/services/server.service';
import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
import { newServerInfoRepositoryMock } from 'test/repositories/server-info.repository.mock';
@ -16,6 +18,7 @@ import { Mocked } from 'vitest';
describe(ServerService.name, () => {
let sut: ServerService;
let configMock: Mocked<IConfigRepository>;
let storageMock: Mocked<IStorageRepository>;
let userMock: Mocked<IUserRepository>;
let serverInfoMock: Mocked<IServerInfoRepository>;
@ -24,6 +27,7 @@ describe(ServerService.name, () => {
let cryptoMock: Mocked<ICryptoRepository>;
beforeEach(() => {
configMock = newConfigRepositoryMock();
storageMock = newStorageRepositoryMock();
userMock = newUserRepositoryMock();
serverInfoMock = newServerInfoRepositoryMock();
@ -31,7 +35,7 @@ describe(ServerService.name, () => {
loggerMock = newLoggerRepositoryMock();
cryptoMock = newCryptoRepositoryMock();
sut = new ServerService(userMock, storageMock, systemMock, serverInfoMock, loggerMock, cryptoMock);
sut = new ServerService(configMock, userMock, storageMock, systemMock, serverInfoMock, loggerMock, cryptoMock);
});
it('should work', () => {

View File

@ -15,6 +15,7 @@ import {
UsageByUserDto,
} from 'src/dtos/server.dto';
import { StorageFolder, SystemMetadataKey } from 'src/enum';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
@ -23,13 +24,13 @@ import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interf
import { IUserRepository, UserStatsQueryResponse } from 'src/interfaces/user.interface';
import { BaseService } from 'src/services/base.service';
import { asHumanReadable } from 'src/utils/bytes';
import { isUsingConfigFile } from 'src/utils/config';
import { mimeTypes } from 'src/utils/mime-types';
import { isDuplicateDetectionEnabled, isFacialRecognitionEnabled, isSmartSearchEnabled } from 'src/utils/misc';
@Injectable()
export class ServerService extends BaseService {
constructor(
@Inject(IConfigRepository) configRepository: IConfigRepository,
@Inject(IUserRepository) private userRepository: IUserRepository,
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
@ -37,7 +38,7 @@ export class ServerService extends BaseService {
@Inject(ILoggerRepository) logger: ILoggerRepository,
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
) {
super(systemMetadataRepository, logger);
super(configRepository, systemMetadataRepository, logger);
this.logger.setContext(ServerService.name);
}
@ -91,6 +92,7 @@ export class ServerService extends BaseService {
async getFeatures(): Promise<ServerFeaturesDto> {
const { reverseGeocoding, metadata, map, machineLearning, trash, oauth, passwordLogin, notifications } =
await this.getConfig({ withCache: false });
const { configFile } = this.configRepository.getEnv();
return {
smartSearch: isSmartSearchEnabled(machineLearning),
@ -105,7 +107,7 @@ export class ServerService extends BaseService {
oauth: oauth.enabled,
oauthAutoLaunch: oauth.autoLaunch,
passwordLogin: passwordLogin.enabled,
configFile: isUsingConfigFile(),
configFile: !!configFile,
email: notifications.smtp.enabled,
};
}

View File

@ -3,6 +3,7 @@ import _ from 'lodash';
import { DEFAULT_EXTERNAL_DOMAIN } from 'src/constants';
import { AssetIdErrorReason } from 'src/dtos/asset-ids.response.dto';
import { SharedLinkType } from 'src/enum';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
@ -13,6 +14,7 @@ import { assetStub } from 'test/fixtures/asset.stub';
import { authStub } from 'test/fixtures/auth.stub';
import { sharedLinkResponseStub, sharedLinkStub } from 'test/fixtures/shared-link.stub';
import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock';
import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
import { newSharedLinkRepositoryMock } from 'test/repositories/shared-link.repository.mock';
@ -22,6 +24,7 @@ import { Mocked } from 'vitest';
describe(SharedLinkService.name, () => {
let sut: SharedLinkService;
let accessMock: IAccessRepositoryMock;
let configMock: Mocked<IConfigRepository>;
let cryptoMock: Mocked<ICryptoRepository>;
let shareMock: Mocked<ISharedLinkRepository>;
let systemMock: Mocked<ISystemMetadataRepository>;
@ -29,12 +32,13 @@ describe(SharedLinkService.name, () => {
beforeEach(() => {
accessMock = newAccessRepositoryMock();
configMock = newConfigRepositoryMock();
cryptoMock = newCryptoRepositoryMock();
shareMock = newSharedLinkRepositoryMock();
systemMock = newSystemMetadataRepositoryMock();
logMock = newLoggerRepositoryMock();
sut = new SharedLinkService(accessMock, cryptoMock, logMock, shareMock, systemMock);
sut = new SharedLinkService(accessMock, configMock, cryptoMock, logMock, shareMock, systemMock);
});
it('should work', () => {

View File

@ -15,6 +15,7 @@ import { AssetEntity } from 'src/entities/asset.entity';
import { SharedLinkEntity } from 'src/entities/shared-link.entity';
import { Permission, SharedLinkType } from 'src/enum';
import { IAccessRepository } from 'src/interfaces/access.interface';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
@ -27,12 +28,13 @@ import { OpenGraphTags } from 'src/utils/misc';
export class SharedLinkService extends BaseService {
constructor(
@Inject(IAccessRepository) private access: IAccessRepository,
@Inject(IConfigRepository) configRepository: IConfigRepository,
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
@Inject(ILoggerRepository) logger: ILoggerRepository,
@Inject(ISharedLinkRepository) private repository: ISharedLinkRepository,
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
) {
super(systemMetadataRepository, logger);
super(configRepository, systemMetadataRepository, logger);
this.logger.setContext(SharedLinkService.name);
}

View File

@ -1,5 +1,6 @@
import { SystemConfig } from 'src/config';
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { IDatabaseRepository } from 'src/interfaces/database.interface';
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
@ -11,6 +12,7 @@ import { getCLIPModelInfo } from 'src/utils/misc';
import { assetStub } from 'test/fixtures/asset.stub';
import { systemConfigStub } from 'test/fixtures/system-config.stub';
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock';
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
@ -22,6 +24,7 @@ import { Mocked } from 'vitest';
describe(SmartInfoService.name, () => {
let sut: SmartInfoService;
let assetMock: Mocked<IAssetRepository>;
let configMock: Mocked<IConfigRepository>;
let systemMock: Mocked<ISystemMetadataRepository>;
let jobMock: Mocked<IJobRepository>;
let searchMock: Mocked<ISearchRepository>;
@ -31,13 +34,24 @@ describe(SmartInfoService.name, () => {
beforeEach(() => {
assetMock = newAssetRepositoryMock();
configMock = newConfigRepositoryMock();
systemMock = newSystemMetadataRepositoryMock();
searchMock = newSearchRepositoryMock();
jobMock = newJobRepositoryMock();
machineMock = newMachineLearningRepositoryMock();
databaseMock = newDatabaseRepositoryMock();
loggerMock = newLoggerRepositoryMock();
sut = new SmartInfoService(assetMock, databaseMock, jobMock, machineMock, searchMock, systemMock, loggerMock);
sut = new SmartInfoService(
assetMock,
configMock,
databaseMock,
jobMock,
machineMock,
searchMock,
systemMock,
loggerMock,
);
assetMock.getByIds.mockResolvedValue([assetStub.image]);
});

View File

@ -2,6 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
import { SystemConfig } from 'src/config';
import { OnEvent } from 'src/decorators';
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface';
import { ArgOf } from 'src/interfaces/event.interface';
import {
@ -26,6 +27,7 @@ import { usePagination } from 'src/utils/pagination';
export class SmartInfoService extends BaseService {
constructor(
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
@Inject(IConfigRepository) configRepository: IConfigRepository,
@Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository,
@Inject(IJobRepository) private jobRepository: IJobRepository,
@Inject(IMachineLearningRepository) private machineLearning: IMachineLearningRepository,
@ -33,7 +35,7 @@ export class SmartInfoService extends BaseService {
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
@Inject(ILoggerRepository) logger: ILoggerRepository,
) {
super(systemMetadataRepository, logger);
super(configRepository, systemMetadataRepository, logger);
this.logger.setContext(SmartInfoService.name);
}

View File

@ -4,6 +4,7 @@ import { AssetEntity } from 'src/entities/asset.entity';
import { AssetPathType } from 'src/enum';
import { IAlbumRepository } from 'src/interfaces/album.interface';
import { IAssetRepository } from 'src/interfaces/asset.interface';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { IDatabaseRepository } from 'src/interfaces/database.interface';
import { JobStatus } from 'src/interfaces/job.interface';
@ -19,6 +20,7 @@ import { assetStub } from 'test/fixtures/asset.stub';
import { userStub } from 'test/fixtures/user.stub';
import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock';
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock';
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
@ -33,6 +35,7 @@ describe(StorageTemplateService.name, () => {
let sut: StorageTemplateService;
let albumMock: Mocked<IAlbumRepository>;
let assetMock: Mocked<IAssetRepository>;
let configMock: Mocked<IConfigRepository>;
let cryptoMock: Mocked<ICryptoRepository>;
let databaseMock: Mocked<IDatabaseRepository>;
let moveMock: Mocked<IMoveRepository>;
@ -49,6 +52,7 @@ describe(StorageTemplateService.name, () => {
beforeEach(() => {
assetMock = newAssetRepositoryMock();
albumMock = newAlbumRepositoryMock();
configMock = newConfigRepositoryMock();
cryptoMock = newCryptoRepositoryMock();
databaseMock = newDatabaseRepositoryMock();
moveMock = newMoveRepositoryMock();
@ -63,6 +67,7 @@ describe(StorageTemplateService.name, () => {
sut = new StorageTemplateService(
albumMock,
assetMock,
configMock,
systemMock,
moveMock,
personMock,

View File

@ -18,6 +18,7 @@ import { AssetEntity } from 'src/entities/asset.entity';
import { AssetPathType, AssetType, StorageFolder } from 'src/enum';
import { IAlbumRepository } from 'src/interfaces/album.interface';
import { IAssetRepository } from 'src/interfaces/asset.interface';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface';
import { ArgOf } from 'src/interfaces/event.interface';
@ -63,6 +64,7 @@ export class StorageTemplateService extends BaseService {
constructor(
@Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
@Inject(IConfigRepository) configRepository: IConfigRepository,
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
@Inject(IMoveRepository) moveRepository: IMoveRepository,
@Inject(IPersonRepository) personRepository: IPersonRepository,
@ -72,10 +74,11 @@ export class StorageTemplateService extends BaseService {
@Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository,
@Inject(ILoggerRepository) logger: ILoggerRepository,
) {
super(systemMetadataRepository, logger);
super(configRepository, systemMetadataRepository, logger);
this.logger.setContext(StorageTemplateService.name);
this.storageCore = StorageCore.create(
assetRepository,
configRepository,
cryptoRepository,
moveRepository,
personRepository,

View File

@ -12,11 +12,13 @@ import {
VideoCodec,
VideoContainer,
} from 'src/enum';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { IEventRepository } from 'src/interfaces/event.interface';
import { QueueName } from 'src/interfaces/job.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { SystemConfigService } from 'src/services/system-config.service';
import { mockEnvData, newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
import { newEventRepositoryMock } from 'test/repositories/event.repository.mock';
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
@ -187,16 +189,18 @@ const updatedConfig = Object.freeze<SystemConfig>({
describe(SystemConfigService.name, () => {
let sut: SystemConfigService;
let configMock: Mocked<IConfigRepository>;
let systemMock: Mocked<ISystemMetadataRepository>;
let eventMock: Mocked<IEventRepository>;
let loggerMock: Mocked<ILoggerRepository>;
beforeEach(() => {
delete process.env.IMMICH_CONFIG_FILE;
systemMock = newSystemMetadataRepositoryMock();
configMock = newConfigRepositoryMock();
eventMock = newEventRepositoryMock();
systemMock = newSystemMetadataRepositoryMock();
loggerMock = newLoggerRepositoryMock();
sut = new SystemConfigService(systemMock, eventMock, loggerMock);
sut = new SystemConfigService(configMock, eventMock, systemMock, loggerMock);
});
it('should work', () => {
@ -231,8 +235,7 @@ describe(SystemConfigService.name, () => {
});
it('should load the config from a json file', async () => {
process.env.IMMICH_CONFIG_FILE = 'immich-config.json';
configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
systemMock.readFile.mockResolvedValue(JSON.stringify(partialConfig));
await expect(sut.getSystemConfig()).resolves.toEqual(updatedConfig);
@ -241,7 +244,7 @@ describe(SystemConfigService.name, () => {
});
it('should log errors with the config file', async () => {
process.env.IMMICH_CONFIG_FILE = 'immich-config.json';
configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
systemMock.readFile.mockResolvedValue(`{ "ffmpeg2": true, "ffmpeg2": true }`);
@ -256,7 +259,7 @@ describe(SystemConfigService.name, () => {
});
it('should load the config from a yaml file', async () => {
process.env.IMMICH_CONFIG_FILE = 'immich-config.yaml';
configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.yaml' }));
const partialConfig = `
ffmpeg:
crf: 30
@ -275,7 +278,7 @@ describe(SystemConfigService.name, () => {
});
it('should accept an empty configuration file', async () => {
process.env.IMMICH_CONFIG_FILE = 'immich-config.json';
configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
systemMock.readFile.mockResolvedValue(JSON.stringify({}));
await expect(sut.getSystemConfig()).resolves.toEqual(defaults);
@ -284,7 +287,7 @@ describe(SystemConfigService.name, () => {
});
it('should allow underscores in the machine learning url', async () => {
process.env.IMMICH_CONFIG_FILE = 'immich-config.json';
configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
const partialConfig = { machineLearning: { url: 'immich_machine_learning' } };
systemMock.readFile.mockResolvedValue(JSON.stringify(partialConfig));
@ -300,7 +303,7 @@ describe(SystemConfigService.name, () => {
for (const { should, externalDomain, result } of externalDomainTests) {
it(`should normalize an external domain ${should}`, async () => {
process.env.IMMICH_CONFIG_FILE = 'immich-config.json';
configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
const partialConfig = { server: { externalDomain } };
systemMock.readFile.mockResolvedValue(JSON.stringify(partialConfig));
@ -310,7 +313,7 @@ describe(SystemConfigService.name, () => {
}
it('should warn for unknown options in yaml', async () => {
process.env.IMMICH_CONFIG_FILE = 'immich-config.yaml';
configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.yaml' }));
const partialConfig = `
unknownOption: true
`;
@ -331,7 +334,7 @@ describe(SystemConfigService.name, () => {
for (const test of tests) {
it(`should ${test.should}`, async () => {
process.env.IMMICH_CONFIG_FILE = 'immich-config.json';
configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
systemMock.readFile.mockResolvedValue(JSON.stringify(test.config));
if (test.warn) {
@ -390,7 +393,7 @@ describe(SystemConfigService.name, () => {
});
it('should throw an error if a config file is in use', async () => {
process.env.IMMICH_CONFIG_FILE = 'immich-config.json';
configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
systemMock.readFile.mockResolvedValue(JSON.stringify({}));
await expect(sut.updateSystemConfig(defaults)).rejects.toBeInstanceOf(BadRequestException);
expect(systemMock.set).not.toHaveBeenCalled();

View File

@ -15,21 +15,23 @@ import {
import { OnEvent } from 'src/decorators';
import { SystemConfigDto, SystemConfigTemplateStorageOptionDto, mapConfig } from 'src/dtos/system-config.dto';
import { LogLevel } from 'src/enum';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { ArgOf, IEventRepository } from 'src/interfaces/event.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { BaseService } from 'src/services/base.service';
import { clearConfigCache, isUsingConfigFile } from 'src/utils/config';
import { clearConfigCache } from 'src/utils/config';
import { toPlainObject } from 'src/utils/object';
@Injectable()
export class SystemConfigService extends BaseService {
constructor(
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
@Inject(IConfigRepository) configRepository: IConfigRepository,
@Inject(IEventRepository) private eventRepository: IEventRepository,
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
@Inject(ILoggerRepository) logger: ILoggerRepository,
) {
super(systemMetadataRepository, logger);
super(configRepository, systemMetadataRepository, logger);
this.logger.setContext(SystemConfigService.name);
}
@ -67,7 +69,8 @@ export class SystemConfigService extends BaseService {
}
async updateSystemConfig(dto: SystemConfigDto): Promise<SystemConfigDto> {
if (isUsingConfigFile()) {
const { configFile } = this.configRepository.getEnv();
if (configFile) {
throw new BadRequestException('Cannot update configuration while IMMICH_CONFIG_FILE is in use');
}

View File

@ -2,6 +2,7 @@ import { BadRequestException, InternalServerErrorException, NotFoundException }
import { UserEntity } from 'src/entities/user.entity';
import { CacheControl, UserMetadataKey } from 'src/enum';
import { IAlbumRepository } from 'src/interfaces/album.interface';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { IJobRepository, JobName } from 'src/interfaces/job.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
@ -14,6 +15,7 @@ import { authStub } from 'test/fixtures/auth.stub';
import { systemConfigStub } from 'test/fixtures/system-config.stub';
import { userStub } from 'test/fixtures/user.stub';
import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock';
import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
@ -34,6 +36,7 @@ describe(UserService.name, () => {
let cryptoRepositoryMock: Mocked<ICryptoRepository>;
let albumMock: Mocked<IAlbumRepository>;
let configMock: Mocked<IConfigRepository>;
let jobMock: Mocked<IJobRepository>;
let storageMock: Mocked<IStorageRepository>;
let systemMock: Mocked<ISystemMetadataRepository>;
@ -41,14 +44,24 @@ describe(UserService.name, () => {
beforeEach(() => {
albumMock = newAlbumRepositoryMock();
systemMock = newSystemMetadataRepositoryMock();
configMock = newConfigRepositoryMock();
cryptoRepositoryMock = newCryptoRepositoryMock();
jobMock = newJobRepositoryMock();
storageMock = newStorageRepositoryMock();
systemMock = newSystemMetadataRepositoryMock();
userMock = newUserRepositoryMock();
loggerMock = newLoggerRepositoryMock();
sut = new UserService(albumMock, cryptoRepositoryMock, jobMock, storageMock, systemMock, userMock, loggerMock);
sut = new UserService(
albumMock,
configMock,
cryptoRepositoryMock,
jobMock,
storageMock,
systemMock,
userMock,
loggerMock,
);
userMock.get.mockImplementation((userId) =>
Promise.resolve([userStub.admin, userStub.user1].find((user) => user.id === userId) ?? null),

View File

@ -12,6 +12,7 @@ import { UserMetadataEntity } from 'src/entities/user-metadata.entity';
import { UserEntity } from 'src/entities/user.entity';
import { CacheControl, StorageFolder, UserMetadataKey } from 'src/enum';
import { IAlbumRepository } from 'src/interfaces/album.interface';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { IEntityJob, IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
@ -26,6 +27,7 @@ import { getPreferences, getPreferencesPartial, mergePreferences } from 'src/uti
export class UserService extends BaseService {
constructor(
@Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
@Inject(IConfigRepository) configRepository: IConfigRepository,
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
@Inject(IJobRepository) private jobRepository: IJobRepository,
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
@ -33,7 +35,7 @@ export class UserService extends BaseService {
@Inject(IUserRepository) private userRepository: IUserRepository,
@Inject(ILoggerRepository) logger: ILoggerRepository,
) {
super(systemMetadataRepository, logger);
super(configRepository, systemMetadataRepository, logger);
this.logger.setContext(UserService.name);
}

View File

@ -1,6 +1,7 @@
import { DateTime } from 'luxon';
import { serverVersion } from 'src/constants';
import { SystemMetadataKey } from 'src/enum';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { IDatabaseRepository } from 'src/interfaces/database.interface';
import { IEventRepository } from 'src/interfaces/event.interface';
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
@ -9,6 +10,7 @@ import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { IVersionHistoryRepository } from 'src/interfaces/version-history.interface';
import { VersionService } from 'src/services/version.service';
import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock';
import { newEventRepositoryMock } from 'test/repositories/event.repository.mock';
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
@ -30,6 +32,7 @@ const mockRelease = (version: string) => ({
describe(VersionService.name, () => {
let sut: VersionService;
let configMock: Mocked<IConfigRepository>;
let databaseMock: Mocked<IDatabaseRepository>;
let eventMock: Mocked<IEventRepository>;
let jobMock: Mocked<IJobRepository>;
@ -39,6 +42,7 @@ describe(VersionService.name, () => {
let loggerMock: Mocked<ILoggerRepository>;
beforeEach(() => {
configMock = newConfigRepositoryMock();
databaseMock = newDatabaseRepositoryMock();
eventMock = newEventRepositoryMock();
jobMock = newJobRepositoryMock();
@ -47,7 +51,16 @@ describe(VersionService.name, () => {
versionMock = newVersionHistoryRepositoryMock();
loggerMock = newLoggerRepositoryMock();
sut = new VersionService(databaseMock, eventMock, jobMock, serverMock, systemMock, versionMock, loggerMock);
sut = new VersionService(
configMock,
databaseMock,
eventMock,
jobMock,
serverMock,
systemMock,
versionMock,
loggerMock,
);
});
it('should work', () => {

View File

@ -6,6 +6,7 @@ import { OnEvent } from 'src/decorators';
import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.dto';
import { VersionCheckMetadata } from 'src/entities/system-metadata.entity';
import { SystemMetadataKey } from 'src/enum';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface';
import { ArgOf, IEventRepository } from 'src/interfaces/event.interface';
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
@ -27,6 +28,7 @@ const asNotification = ({ checkedAt, releaseVersion }: VersionCheckMetadata): Re
@Injectable()
export class VersionService extends BaseService {
constructor(
@Inject(IConfigRepository) configRepository: IConfigRepository,
@Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository,
@Inject(IEventRepository) private eventRepository: IEventRepository,
@Inject(IJobRepository) private jobRepository: IJobRepository,
@ -35,7 +37,7 @@ export class VersionService extends BaseService {
@Inject(IVersionHistoryRepository) private versionRepository: IVersionHistoryRepository,
@Inject(ILoggerRepository) logger: ILoggerRepository,
) {
super(systemMetadataRepository, logger);
super(configRepository, systemMetadataRepository, logger);
this.logger.setContext(VersionService.name);
}

View File

@ -6,6 +6,7 @@ import * as _ from 'lodash';
import { SystemConfig, defaults } from 'src/config';
import { SystemConfigDto } from 'src/dtos/system-config.dto';
import { SystemMetadataKey } from 'src/enum';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { DatabaseLock } from 'src/interfaces/database.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
@ -15,6 +16,7 @@ import { DeepPartial } from 'typeorm';
export type SystemConfigValidator = (config: SystemConfig, newConfig: SystemConfig) => void | Promise<void>;
type RepoDeps = {
configRepo: IConfigRepository;
metadataRepo: ISystemMetadataRepository;
logger: ILoggerRepository;
};
@ -28,10 +30,6 @@ export const clearConfigCache = () => {
lastUpdated = null;
};
export const isUsingConfigFile = () => {
return !!process.env.IMMICH_CONFIG_FILE;
};
export const getConfig = async (repos: RepoDeps, { withCache }: { withCache: boolean }): Promise<SystemConfig> => {
if (!withCache || !config) {
const timestamp = lastUpdated;
@ -80,11 +78,12 @@ const loadFromFile = async ({ metadataRepo, logger }: RepoDeps, filepath: string
};
const buildConfig = async (repos: RepoDeps) => {
const { metadataRepo, logger } = repos;
const { configRepo, metadataRepo, logger } = repos;
const { configFile } = configRepo.getEnv();
// load partial
const partial = isUsingConfigFile()
? await loadFromFile(repos, process.env.IMMICH_CONFIG_FILE as string)
const partial = configFile
? await loadFromFile(repos, configFile)
: await metadataRepo.get(SystemMetadataKey.SYSTEM_CONFIG);
// merge with defaults
@ -106,7 +105,7 @@ const buildConfig = async (repos: RepoDeps) => {
// validate full config
const errors = await validate(plainToInstance(SystemConfigDto, config));
if (errors.length > 0) {
if (isUsingConfigFile()) {
if (configFile) {
throw new Error(`Invalid value(s) in file: ${errors}`);
} else {
logger.error('Validation error', errors);

6
web/package-lock.json generated
View File

@ -769,9 +769,9 @@
}
},
"node_modules/@faker-js/faker": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.0.2.tgz",
"integrity": "sha512-nI/FP30ZGXb+UaR7yXawVTH40NVKXPIx0tA3GKjkKLjorqBoMAeq4iSEacl8mJmpVhOCDa0vYHwYDmOOcFMrYw==",
"version": "9.0.3",
"resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.0.3.tgz",
"integrity": "sha512-lWrrK4QNlFSU+13PL9jMbMKLJYXDFu3tQfayBsMXX7KL/GiQeqfB1CzHkqD5UHBUtPAuPo6XwGbMFNdVMZObRA==",
"dev": true,
"funding": [
{

View File

@ -43,6 +43,7 @@
} from '@mdi/js';
import { canCopyImageToClipboard } from '$lib/utils/asset-utils';
import { t } from 'svelte-i18n';
import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';
export let asset: AssetResponseDto;
export let album: AlbumResponseDto | null = null;
@ -82,6 +83,11 @@
class="flex w-[calc(100%-3rem)] justify-end gap-2 overflow-hidden text-white"
data-testid="asset-viewer-navbar-actions"
>
{#if asset.owner && asset.owner.id != $user.id}
<div class="p-3 margin:auto">
<UserAvatar user={asset.owner} size="xs"></UserAvatar>
</div>
{/if}
{#if !asset.isTrashed && $user}
<ShareAction {asset} />
{/if}
@ -124,7 +130,6 @@
title={$t('editor')}
/>
{/if} -->
{#if isOwner}
<DeleteAction {asset} {onAction} />

View File

@ -462,9 +462,13 @@
</div>
{/if}
{#if currentAlbum && currentAlbum.albumUsers.length > 0 && asset.owner}
{#if (currentAlbum && currentAlbum.albumUsers.length > 0 && asset.owner && asset.ownerId != $user.id) || (asset.ownerId != $user.id && asset.owner)}
<section class="px-6 dark:text-immich-dark-fg mt-4">
<p class="text-sm">{$t('shared_by').toUpperCase()}</p>
{#if currentAlbum}
<p class="text-sm">{$t('shared_by').toUpperCase()}</p>
{:else}
<p class="text-sm">{$t('partner_sharing').toUpperCase()}</p>
{/if}
<div class="flex gap-4 pt-4">
<div>
<UserAvatar user={asset.owner} size="md" />

View File

@ -2,11 +2,14 @@
import { intersectionObserver } from '$lib/actions/intersection-observer';
import Icon from '$lib/components/elements/icon.svelte';
import { ProjectionType } from '$lib/constants';
import { getAssetThumbnailUrl, isSharedLink } from '$lib/utils';
import { getAssetThumbnailUrl, isSharedLink, handlePromiseError } from '$lib/utils';
import { handleError } from '$lib/utils/handle-error';
import { getAltText } from '$lib/utils/thumbnail-util';
import { timeToSeconds } from '$lib/utils/date-time';
import { AssetMediaSize, AssetTypeEnum, type AssetResponseDto } from '@immich/sdk';
import { locale, playVideoThumbnailOnHover } from '$lib/stores/preferences.store';
import { user } from '$lib/stores/user.store';
import { AssetMediaSize, AssetTypeEnum, type AssetResponseDto, type UserResponseDto } from '@immich/sdk';
import { locale, playVideoThumbnailOnHover, showUserThumbnails } from '$lib/stores/preferences.store';
import { getUserAndCacheResult } from '$lib/utils/users';
import { getAssetPlaybackUrl } from '$lib/utils';
import {
mdiArchiveArrowDownOutline,
@ -19,6 +22,7 @@
} from '@mdi/js';
import { fade } from 'svelte/transition';
import { t } from 'svelte-i18n';
import ImageThumbnail from './image-thumbnail.svelte';
import VideoThumbnail from './video-thumbnail.svelte';
import { currentUrlReplaceAssetId } from '$lib/utils/navigation';
@ -30,6 +34,7 @@
import { onDestroy } from 'svelte';
import { TUNABLES } from '$lib/utils/tunables';
import { thumbhash } from '$lib/actions/thumbhash';
import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';
export let asset: AssetResponseDto;
export let dateGroup: DateGroup | undefined = undefined;
@ -44,6 +49,7 @@
export let readonly = false;
export let showArchiveIcon = false;
export let showStackedIcon = true;
export let showUserThumbnailsinViewer = true;
export let intersectionConfig: {
root?: HTMLElement;
bottom?: string;
@ -63,7 +69,6 @@
let className = '';
export { className as class };
let {
IMAGE_THUMBNAIL: { THUMBHASH_FADE_DURATION },
} = TUNABLES;
@ -74,6 +79,7 @@
let intersecting = false;
let lastRetrievedElement: HTMLElement | undefined;
let loaded = false;
let shareUser: UserResponseDto | undefined;
$: if (!retrieveElement) {
lastRetrievedElement = undefined;
@ -82,6 +88,9 @@
lastRetrievedElement = element;
onRetrieveElement?.(element);
}
$: if ($showUserThumbnails && showUserThumbnailsinViewer && (isSharedLink() || asset.ownerId != $user.id)) {
handlePromiseError(getShareUser());
}
$: width = thumbnailSize || thumbnailWidth || 235;
$: height = thumbnailSize || thumbnailHeight || 235;
@ -159,6 +168,14 @@
}
};
const getShareUser = async () => {
try {
shareUser = await getUserAndCacheResult(asset.ownerId);
} catch (error) {
handleError(error, $t('errors.unable_to_load_liked_status'));
}
};
onDestroy(() => {
assetStore?.taskManager.removeAllTasksForComponent(componentId);
});
@ -268,6 +285,12 @@
</div>
{/if}
{#if shareUser && showUserThumbnailsinViewer}
<div class="absolute bottom-2 left-2 z-10">
<UserAvatar user={shareUser} size="sm" />
</div>
{/if}
{#if !isSharedLink() && showArchiveIcon && asset.isArchived}
<div class="absolute {asset.isFavorite ? 'bottom-10' : 'bottom-2'} left-2 z-10">
<Icon path={mdiArchiveArrowDownOutline} size="24" class="text-white" />

View File

@ -20,6 +20,7 @@
export let singleSelect = false;
export let withStacked = false;
export let showArchiveIcon = false;
export let showUserThumbnailsinViewer = true;
export let assetGridElement: HTMLElement | undefined = undefined;
export let renderThumbsAtBottomMargin: string | undefined = undefined;
export let renderThumbsAtTopMargin: string | undefined = undefined;
@ -207,6 +208,7 @@
onRetrieveElement={(element) => onRetrieveElement(dateGroup, asset, element)}
showStackedIcon={withStacked}
{showArchiveIcon}
{showUserThumbnailsinViewer}
{asset}
{groupIndex}
onClick={(asset) => onClick(dateGroup.assets, dateGroup.groupTitle, asset)}

View File

@ -62,6 +62,7 @@
export let withStacked = false;
export let showArchiveIcon = false;
export let isShared = false;
export let showUserThumbnailsinViewer = true;
export let album: AlbumResponseDto | null = null;
export let isShowDeleteConfirmation = false;
export let onSelect: (asset: AssetResponseDto) => void = () => {};
@ -839,6 +840,7 @@
renderThumbsAtBottomMargin={THUMBNAIL_INTERSECTION_ROOT_BOTTOM}
{withStacked}
{showArchiveIcon}
{showUserThumbnailsinViewer}
{assetStore}
{assetInteractionStore}
{isSelectionMode}
@ -864,6 +866,7 @@
<AssetViewer
{withStacked}
{assetStore}
{showUserThumbnailsinViewer}
asset={$viewingAsset}
preloadAssets={$preloadAssets}
{isShared}

View File

@ -1,5 +1,5 @@
<script lang="ts" context="module">
export type Size = 'full' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl' | 'xxxl';
export type Size = 'full' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl' | 'xxxl';
</script>
<script lang="ts">
@ -56,6 +56,7 @@
const sizeClasses: Record<Size, string> = {
full: 'w-full h-full',
xs: 'w-6 h-6',
sm: 'w-7 h-7',
md: 'w-10 h-10',
lg: 'w-12 h-12',
@ -90,7 +91,8 @@
{#if showFallback}
<span
class="flex h-full w-full select-none items-center justify-center font-medium"
class:text-xs={size === 'sm'}
class:text-xs={size === 'xs'}
class:text-sm={size === 'sm'}
class:text-lg={size === 'lg'}
class:text-xl={size === 'xl'}
class:text-2xl={size === 'xxl'}

View File

@ -11,6 +11,7 @@
loopVideo,
playVideoThumbnailOnHover,
showDeleteModal,
showUserThumbnails,
} from '$lib/stores/preferences.store';
import { findLocale } from '$lib/utils';
import { getClosestAvailableLocale, langCodes } from '$lib/utils/i18n';
@ -169,6 +170,14 @@
bind:checked={$showDeleteModal}
/>
</div>
<div class="ml-4">
<SettingSwitch
title={$t('show_user_thumbnails')}
subtitle={$t('show_user_thumbnails_description')}
bind:checked={$showUserThumbnails}
/>
</div>
</div>
</div>
</section>

View File

@ -1153,6 +1153,8 @@
"show_slideshow_transition": "Show slideshow transition",
"show_supporter_badge": "Supporter badge",
"show_supporter_badge_description": "Show a supporter badge",
"show_user_thumbnails": "Show user thumbnails",
"show_user_thumbnails_description": "Show user avatars on timelinle for shared albums and partners",
"shuffle": "Shuffle",
"sidebar": "Sidebar",
"sidebar_display_description": "Display a link to the view in the sidebar",

View File

@ -139,6 +139,8 @@ export const albumViewSettings = persisted<AlbumViewSettings>('album-view-settin
export const showDeleteModal = persisted<boolean>('delete-confirm-dialog', true, {});
export const showUserThumbnails = persisted<boolean>('show-user-thumbnails', true, {});
export const alwaysLoadOriginalFile = persisted<boolean>('always-load-original-file', false, {});
export const playVideoThumbnailOnHover = persisted<boolean>('play-video-thumbnail-on-hover', true, {});

View File

@ -0,0 +1,35 @@
import { type UserResponseDto } from '@immich/sdk';
import { writable } from 'svelte/store';
export const users = writable<{ [key: string]: UserResponseDto | undefined }>({});
export const userExistsInStore = (userId: string): boolean => {
let exists = false;
users.subscribe((userStore) => {
try {
exists = userId in userStore;
} catch {
exists = false;
}
})();
return exists;
};
export const updateUserInStore = ({ user, userId }: { user?: UserResponseDto; userId?: string }) => {
users.update((userStore) => {
if (user) {
userStore[user.id] = user;
} else if (userId) {
userStore[userId] = undefined;
}
return userStore;
});
};
export const getUserFromStore = (userId: string): UserResponseDto | undefined => {
let userInfo: UserResponseDto | undefined;
users.subscribe((userStore) => {
userInfo = userStore[userId];
})();
return userInfo;
};

View File

@ -0,0 +1,16 @@
import { getUserFromStore, updateUserInStore, userExistsInStore } from '$lib/stores/users.store';
import { getUser, type UserResponseDto } from '@immich/sdk';
export const getUserAndCacheResult = async (userId: string, skipCache: boolean = false): Promise<UserResponseDto> => {
let user: UserResponseDto;
if (!skipCache && userExistsInStore(userId)) {
user = getUserFromStore(userId)!;
} else {
//Add to store indicating a request to server is in-flight
updateUserInStore({ userId });
user = await getUser({ id: userId });
//Update store with results of server request
updateUserInStore({ user });
}
return user;
};

View File

@ -46,5 +46,5 @@
</svelte:fragment>
</ControlAppBar>
{/if}
<AssetGrid enableRouting={true} {assetStore} {assetInteractionStore} />
<AssetGrid enableRouting={true} {assetStore} {assetInteractionStore} showUserThumbnailsinViewer={false} />
</main>