mirror of
https://github.com/immich-app/immich.git
synced 2024-11-24 08:52:28 +02:00
feat(server) user-defined storage structure (#1098)
[Breaking] newly uploaded file will conform to the default structure of `{uploadLocation}/{userId}/year/year-month-day/filename.ext`
This commit is contained in:
parent
391d00bcb9
commit
c754c860fd
6
mobile/openapi/.openapi-generator/FILES
generated
6
mobile/openapi/.openapi-generator/FILES
generated
@ -64,6 +64,8 @@ doc/SystemConfigApi.md
|
|||||||
doc/SystemConfigDto.md
|
doc/SystemConfigDto.md
|
||||||
doc/SystemConfigFFmpegDto.md
|
doc/SystemConfigFFmpegDto.md
|
||||||
doc/SystemConfigOAuthDto.md
|
doc/SystemConfigOAuthDto.md
|
||||||
|
doc/SystemConfigStorageTemplateDto.md
|
||||||
|
doc/SystemConfigTemplateStorageOptionDto.md
|
||||||
doc/TagApi.md
|
doc/TagApi.md
|
||||||
doc/TagResponseDto.md
|
doc/TagResponseDto.md
|
||||||
doc/TagTypeEnum.md
|
doc/TagTypeEnum.md
|
||||||
@ -152,6 +154,8 @@ lib/model/smart_info_response_dto.dart
|
|||||||
lib/model/system_config_dto.dart
|
lib/model/system_config_dto.dart
|
||||||
lib/model/system_config_f_fmpeg_dto.dart
|
lib/model/system_config_f_fmpeg_dto.dart
|
||||||
lib/model/system_config_o_auth_dto.dart
|
lib/model/system_config_o_auth_dto.dart
|
||||||
|
lib/model/system_config_storage_template_dto.dart
|
||||||
|
lib/model/system_config_template_storage_option_dto.dart
|
||||||
lib/model/tag_response_dto.dart
|
lib/model/tag_response_dto.dart
|
||||||
lib/model/tag_type_enum.dart
|
lib/model/tag_type_enum.dart
|
||||||
lib/model/thumbnail_format.dart
|
lib/model/thumbnail_format.dart
|
||||||
@ -227,6 +231,8 @@ test/system_config_api_test.dart
|
|||||||
test/system_config_dto_test.dart
|
test/system_config_dto_test.dart
|
||||||
test/system_config_f_fmpeg_dto_test.dart
|
test/system_config_f_fmpeg_dto_test.dart
|
||||||
test/system_config_o_auth_dto_test.dart
|
test/system_config_o_auth_dto_test.dart
|
||||||
|
test/system_config_storage_template_dto_test.dart
|
||||||
|
test/system_config_template_storage_option_dto_test.dart
|
||||||
test/tag_api_test.dart
|
test/tag_api_test.dart
|
||||||
test/tag_response_dto_test.dart
|
test/tag_response_dto_test.dart
|
||||||
test/tag_type_enum_test.dart
|
test/tag_type_enum_test.dart
|
||||||
|
BIN
mobile/openapi/README.md
generated
BIN
mobile/openapi/README.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/SystemConfigApi.md
generated
BIN
mobile/openapi/doc/SystemConfigApi.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/SystemConfigDto.md
generated
BIN
mobile/openapi/doc/SystemConfigDto.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/SystemConfigStorageTemplateDto.md
generated
Normal file
BIN
mobile/openapi/doc/SystemConfigStorageTemplateDto.md
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/doc/SystemConfigTemplateStorageOptionDto.md
generated
Normal file
BIN
mobile/openapi/doc/SystemConfigTemplateStorageOptionDto.md
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/lib/api.dart
generated
BIN
mobile/openapi/lib/api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/system_config_api.dart
generated
BIN
mobile/openapi/lib/api/system_config_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api_client.dart
generated
BIN
mobile/openapi/lib/api_client.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/system_config_dto.dart
generated
BIN
mobile/openapi/lib/model/system_config_dto.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/system_config_storage_template_dto.dart
generated
Normal file
BIN
mobile/openapi/lib/model/system_config_storage_template_dto.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/lib/model/system_config_template_storage_option_dto.dart
generated
Normal file
BIN
mobile/openapi/lib/model/system_config_template_storage_option_dto.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/test/system_config_api_test.dart
generated
BIN
mobile/openapi/test/system_config_api_test.dart
generated
Binary file not shown.
BIN
mobile/openapi/test/system_config_dto_test.dart
generated
BIN
mobile/openapi/test/system_config_dto_test.dart
generated
Binary file not shown.
BIN
mobile/openapi/test/system_config_storage_template_dto_test.dart
generated
Normal file
BIN
mobile/openapi/test/system_config_storage_template_dto_test.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/test/system_config_template_storage_option_dto_test.dart
generated
Normal file
BIN
mobile/openapi/test/system_config_template_storage_option_dto_test.dart
generated
Normal file
Binary file not shown.
10
notes.md
Normal file
10
notes.md
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# User defined storage structure
|
||||||
|
|
||||||
|
# Folder structure
|
||||||
|
* Year is the top level
|
||||||
|
* Different parsing sequence will be the second level
|
||||||
|
|
||||||
|
# Filename
|
||||||
|
* Filename will always be appended by a unique ID. Maybe use https://github.com/ai/nanoid
|
||||||
|
* Example: `notes.md` -> `notes-1234567890.md`
|
||||||
|
* Filename will be unique in the same folder
|
@ -13,6 +13,7 @@ import { DownloadModule } from '../../modules/download/download.module';
|
|||||||
import { TagModule } from '../tag/tag.module';
|
import { TagModule } from '../tag/tag.module';
|
||||||
import { AlbumModule } from '../album/album.module';
|
import { AlbumModule } from '../album/album.module';
|
||||||
import { UserModule } from '../user/user.module';
|
import { UserModule } from '../user/user.module';
|
||||||
|
import { StorageModule } from '@app/storage';
|
||||||
|
|
||||||
const ASSET_REPOSITORY_PROVIDER = {
|
const ASSET_REPOSITORY_PROVIDER = {
|
||||||
provide: ASSET_REPOSITORY,
|
provide: ASSET_REPOSITORY,
|
||||||
@ -28,6 +29,7 @@ const ASSET_REPOSITORY_PROVIDER = {
|
|||||||
UserModule,
|
UserModule,
|
||||||
AlbumModule,
|
AlbumModule,
|
||||||
TagModule,
|
TagModule,
|
||||||
|
StorageModule,
|
||||||
forwardRef(() => AlbumModule),
|
forwardRef(() => AlbumModule),
|
||||||
BullModule.registerQueue({
|
BullModule.registerQueue({
|
||||||
name: QueueNameEnum.ASSET_UPLOADED,
|
name: QueueNameEnum.ASSET_UPLOADED,
|
||||||
|
@ -11,7 +11,8 @@ import { DownloadService } from '../../modules/download/download.service';
|
|||||||
import { BackgroundTaskService } from '../../modules/background-task/background-task.service';
|
import { BackgroundTaskService } from '../../modules/background-task/background-task.service';
|
||||||
import { IAssetUploadedJob, IVideoTranscodeJob } from '@app/job';
|
import { IAssetUploadedJob, IVideoTranscodeJob } from '@app/job';
|
||||||
import { Queue } from 'bull';
|
import { Queue } from 'bull';
|
||||||
import { IAlbumRepository } from "../album/album-repository";
|
import { IAlbumRepository } from '../album/album-repository';
|
||||||
|
import { StorageService } from '@app/storage';
|
||||||
|
|
||||||
describe('AssetService', () => {
|
describe('AssetService', () => {
|
||||||
let sui: AssetService;
|
let sui: AssetService;
|
||||||
@ -22,6 +23,7 @@ describe('AssetService', () => {
|
|||||||
let backgroundTaskServiceMock: jest.Mocked<BackgroundTaskService>;
|
let backgroundTaskServiceMock: jest.Mocked<BackgroundTaskService>;
|
||||||
let assetUploadedQueueMock: jest.Mocked<Queue<IAssetUploadedJob>>;
|
let assetUploadedQueueMock: jest.Mocked<Queue<IAssetUploadedJob>>;
|
||||||
let videoConversionQueueMock: jest.Mocked<Queue<IVideoTranscodeJob>>;
|
let videoConversionQueueMock: jest.Mocked<Queue<IVideoTranscodeJob>>;
|
||||||
|
let storageSeriveMock: jest.Mocked<StorageService>;
|
||||||
const authUser: AuthUserDto = Object.freeze({
|
const authUser: AuthUserDto = Object.freeze({
|
||||||
id: 'user_id_1',
|
id: 'user_id_1',
|
||||||
email: 'auth@test.com',
|
email: 'auth@test.com',
|
||||||
@ -139,6 +141,7 @@ describe('AssetService', () => {
|
|||||||
assetUploadedQueueMock,
|
assetUploadedQueueMock,
|
||||||
videoConversionQueueMock,
|
videoConversionQueueMock,
|
||||||
downloadServiceMock as DownloadService,
|
downloadServiceMock as DownloadService,
|
||||||
|
storageSeriveMock,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -55,6 +55,7 @@ import { Queue } from 'bull';
|
|||||||
import { DownloadService } from '../../modules/download/download.service';
|
import { DownloadService } from '../../modules/download/download.service';
|
||||||
import { DownloadDto } from './dto/download-library.dto';
|
import { DownloadDto } from './dto/download-library.dto';
|
||||||
import { ALBUM_REPOSITORY, IAlbumRepository } from '../album/album-repository';
|
import { ALBUM_REPOSITORY, IAlbumRepository } from '../album/album-repository';
|
||||||
|
import { StorageService } from '@app/storage';
|
||||||
|
|
||||||
const fileInfo = promisify(stat);
|
const fileInfo = promisify(stat);
|
||||||
|
|
||||||
@ -79,6 +80,8 @@ export class AssetService {
|
|||||||
private videoConversionQueue: Queue<IVideoTranscodeJob>,
|
private videoConversionQueue: Queue<IVideoTranscodeJob>,
|
||||||
|
|
||||||
private downloadService: DownloadService,
|
private downloadService: DownloadService,
|
||||||
|
|
||||||
|
private storageService: StorageService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async handleUploadedAsset(
|
public async handleUploadedAsset(
|
||||||
@ -113,6 +116,8 @@ export class AssetService {
|
|||||||
throw new BadRequestException('Asset not created');
|
throw new BadRequestException('Asset not created');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.storageService.moveAsset(livePhotoAssetEntity, originalAssetData.originalname);
|
||||||
|
|
||||||
await this.videoConversionQueue.add(
|
await this.videoConversionQueue.add(
|
||||||
mp4ConversionProcessorName,
|
mp4ConversionProcessorName,
|
||||||
{ asset: livePhotoAssetEntity },
|
{ asset: livePhotoAssetEntity },
|
||||||
@ -139,13 +144,15 @@ export class AssetService {
|
|||||||
throw new BadRequestException('Asset not created');
|
throw new BadRequestException('Asset not created');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const movedAsset = await this.storageService.moveAsset(assetEntity, originalAssetData.originalname);
|
||||||
|
|
||||||
await this.assetUploadedQueue.add(
|
await this.assetUploadedQueue.add(
|
||||||
assetUploadedProcessorName,
|
assetUploadedProcessorName,
|
||||||
{ asset: assetEntity, fileName: originalAssetData.originalname },
|
{ asset: movedAsset, fileName: originalAssetData.originalname },
|
||||||
{ jobId: assetEntity.id },
|
{ jobId: movedAsset.id },
|
||||||
);
|
);
|
||||||
|
|
||||||
return new AssetFileUploadResponseDto(assetEntity.id);
|
return new AssetFileUploadResponseDto(movedAsset.id);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
await this.backgroundTaskService.deleteFileOnDisk([
|
await this.backgroundTaskService.deleteFileOnDisk([
|
||||||
{
|
{
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
import { IsNotEmpty, IsString } from 'class-validator';
|
||||||
|
|
||||||
|
export class SystemConfigStorageTemplateDto {
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
template!: string;
|
||||||
|
}
|
@ -2,6 +2,7 @@ import { SystemConfig } from '@app/database/entities/system-config.entity';
|
|||||||
import { ValidateNested } from 'class-validator';
|
import { ValidateNested } from 'class-validator';
|
||||||
import { SystemConfigFFmpegDto } from './system-config-ffmpeg.dto';
|
import { SystemConfigFFmpegDto } from './system-config-ffmpeg.dto';
|
||||||
import { SystemConfigOAuthDto } from './system-config-oauth.dto';
|
import { SystemConfigOAuthDto } from './system-config-oauth.dto';
|
||||||
|
import { SystemConfigStorageTemplateDto } from './system-config-storage-template.dto';
|
||||||
|
|
||||||
export class SystemConfigDto {
|
export class SystemConfigDto {
|
||||||
@ValidateNested()
|
@ValidateNested()
|
||||||
@ -9,6 +10,9 @@ export class SystemConfigDto {
|
|||||||
|
|
||||||
@ValidateNested()
|
@ValidateNested()
|
||||||
oauth!: SystemConfigOAuthDto;
|
oauth!: SystemConfigOAuthDto;
|
||||||
|
|
||||||
|
@ValidateNested()
|
||||||
|
storageTemplate!: SystemConfigStorageTemplateDto;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mapConfig(config: SystemConfig): SystemConfigDto {
|
export function mapConfig(config: SystemConfig): SystemConfigDto {
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
export class SystemConfigTemplateStorageOptionDto {
|
||||||
|
yearOptions!: string[];
|
||||||
|
monthOptions!: string[];
|
||||||
|
dayOptions!: string[];
|
||||||
|
hourOptions!: string[];
|
||||||
|
minuteOptions!: string[];
|
||||||
|
secondOptions!: string[];
|
||||||
|
presetOptions!: string[];
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
import { Body, Controller, Get, Put, ValidationPipe } from '@nestjs/common';
|
import { Body, Controller, Get, Put, ValidationPipe } from '@nestjs/common';
|
||||||
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||||
import { Authenticated } from '../../decorators/authenticated.decorator';
|
import { Authenticated } from '../../decorators/authenticated.decorator';
|
||||||
|
import { SystemConfigTemplateStorageOptionDto } from './response-dto/system-config-template-storage-option.dto';
|
||||||
import { SystemConfigDto } from './dto/system-config.dto';
|
import { SystemConfigDto } from './dto/system-config.dto';
|
||||||
import { SystemConfigService } from './system-config.service';
|
import { SystemConfigService } from './system-config.service';
|
||||||
|
|
||||||
@ -25,4 +26,9 @@ export class SystemConfigController {
|
|||||||
public updateConfig(@Body(ValidationPipe) dto: SystemConfigDto): Promise<SystemConfigDto> {
|
public updateConfig(@Body(ValidationPipe) dto: SystemConfigDto): Promise<SystemConfigDto> {
|
||||||
return this.systemConfigService.updateConfig(dto);
|
return this.systemConfigService.updateConfig(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('storage-template-options')
|
||||||
|
public getStorageTemplateOptions(): SystemConfigTemplateStorageOptionDto {
|
||||||
|
return this.systemConfigService.getStorageTemplateOptions();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,16 @@
|
|||||||
|
import {
|
||||||
|
supportedDayTokens,
|
||||||
|
supportedHourTokens,
|
||||||
|
supportedMinuteTokens,
|
||||||
|
supportedMonthTokens,
|
||||||
|
supportedPresetTokens,
|
||||||
|
supportedSecondTokens,
|
||||||
|
supportedYearTokens,
|
||||||
|
} from '@app/storage/constants/supported-datetime-template';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { ImmichConfigService } from 'libs/immich-config/src';
|
import { ImmichConfigService } from 'libs/immich-config/src';
|
||||||
import { mapConfig, SystemConfigDto } from './dto/system-config.dto';
|
import { mapConfig, SystemConfigDto } from './dto/system-config.dto';
|
||||||
|
import { SystemConfigTemplateStorageOptionDto } from './response-dto/system-config-template-storage-option.dto';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SystemConfigService {
|
export class SystemConfigService {
|
||||||
@ -17,7 +27,21 @@ export class SystemConfigService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async updateConfig(dto: SystemConfigDto): Promise<SystemConfigDto> {
|
public async updateConfig(dto: SystemConfigDto): Promise<SystemConfigDto> {
|
||||||
await this.immichConfigService.updateConfig(dto);
|
const config = await this.immichConfigService.updateConfig(dto);
|
||||||
return this.getConfig();
|
return mapConfig(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getStorageTemplateOptions(): SystemConfigTemplateStorageOptionDto {
|
||||||
|
const options = new SystemConfigTemplateStorageOptionDto();
|
||||||
|
|
||||||
|
options.dayOptions = supportedDayTokens;
|
||||||
|
options.monthOptions = supportedMonthTokens;
|
||||||
|
options.yearOptions = supportedYearTokens;
|
||||||
|
options.hourOptions = supportedHourTokens;
|
||||||
|
options.minuteOptions = supportedMinuteTokens;
|
||||||
|
options.secondOptions = supportedSecondTokens;
|
||||||
|
options.presetOptions = supportedPresetTokens;
|
||||||
|
|
||||||
|
return options;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ describe('UserService', () => {
|
|||||||
email: 'immich@test.com',
|
email: 'immich@test.com',
|
||||||
});
|
});
|
||||||
|
|
||||||
const adminUser: UserEntity = Object.freeze({
|
const adminUser: UserEntity = {
|
||||||
id: 'admin_id',
|
id: 'admin_id',
|
||||||
email: 'admin@test.com',
|
email: 'admin@test.com',
|
||||||
password: 'admin_password',
|
password: 'admin_password',
|
||||||
@ -32,9 +32,9 @@ describe('UserService', () => {
|
|||||||
profileImagePath: '',
|
profileImagePath: '',
|
||||||
createdAt: '2021-01-01',
|
createdAt: '2021-01-01',
|
||||||
tags: [],
|
tags: [],
|
||||||
});
|
};
|
||||||
|
|
||||||
const immichUser: UserEntity = Object.freeze({
|
const immichUser: UserEntity = {
|
||||||
id: 'immich_id',
|
id: 'immich_id',
|
||||||
email: 'immich@test.com',
|
email: 'immich@test.com',
|
||||||
password: 'immich_password',
|
password: 'immich_password',
|
||||||
@ -47,9 +47,9 @@ describe('UserService', () => {
|
|||||||
profileImagePath: '',
|
profileImagePath: '',
|
||||||
createdAt: '2021-01-01',
|
createdAt: '2021-01-01',
|
||||||
tags: [],
|
tags: [],
|
||||||
});
|
};
|
||||||
|
|
||||||
const updatedImmichUser: UserEntity = Object.freeze({
|
const updatedImmichUser: UserEntity = {
|
||||||
id: 'immich_id',
|
id: 'immich_id',
|
||||||
email: 'immich@test.com',
|
email: 'immich@test.com',
|
||||||
password: 'immich_password',
|
password: 'immich_password',
|
||||||
@ -62,7 +62,7 @@ describe('UserService', () => {
|
|||||||
profileImagePath: '',
|
profileImagePath: '',
|
||||||
createdAt: '2021-01-01',
|
createdAt: '2021-01-01',
|
||||||
tags: [],
|
tags: [],
|
||||||
});
|
};
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
userRepositoryMock = newUserRepositoryMock();
|
userRepositoryMock = newUserRepositoryMock();
|
||||||
@ -75,7 +75,7 @@ describe('UserService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('Update user', () => {
|
describe('Update user', () => {
|
||||||
it('should update user', () => {
|
it('should update user', async () => {
|
||||||
const requestor = immichAuthUser;
|
const requestor = immichAuthUser;
|
||||||
const userToUpdate = immichUser;
|
const userToUpdate = immichUser;
|
||||||
|
|
||||||
@ -83,11 +83,11 @@ describe('UserService', () => {
|
|||||||
userRepositoryMock.get.mockImplementationOnce(() => Promise.resolve(userToUpdate));
|
userRepositoryMock.get.mockImplementationOnce(() => Promise.resolve(userToUpdate));
|
||||||
userRepositoryMock.update.mockImplementationOnce(() => Promise.resolve(updatedImmichUser));
|
userRepositoryMock.update.mockImplementationOnce(() => Promise.resolve(updatedImmichUser));
|
||||||
|
|
||||||
const result = sui.updateUser(requestor, {
|
const result = await sui.updateUser(requestor, {
|
||||||
id: userToUpdate.id,
|
id: userToUpdate.id,
|
||||||
shouldChangePassword: true,
|
shouldChangePassword: true,
|
||||||
});
|
});
|
||||||
expect(result).resolves.toBeDefined();
|
expect(result.shouldChangePassword).toEqual(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('user can only update its information', () => {
|
it('user can only update its information', () => {
|
||||||
|
@ -44,6 +44,7 @@ export class ThumbnailGeneratorProcessor {
|
|||||||
private configService: ConfigService,
|
private configService: ConfigService,
|
||||||
) {
|
) {
|
||||||
this.logLevel = this.configService.get('LOG_LEVEL') || ImmichLogLevel.SIMPLE;
|
this.logLevel = this.configService.get('LOG_LEVEL') || ImmichLogLevel.SIMPLE;
|
||||||
|
// TODO - Add observable paterrn to listen to the config change
|
||||||
}
|
}
|
||||||
|
|
||||||
@Process({ name: generateJPEGThumbnailProcessorName, concurrency: 3 })
|
@Process({ name: generateJPEGThumbnailProcessorName, concurrency: 3 })
|
||||||
@ -59,9 +60,7 @@ export class ThumbnailGeneratorProcessor {
|
|||||||
mkdirSync(resizePath, { recursive: true });
|
mkdirSync(resizePath, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
const temp = asset.originalPath.split('/');
|
const jpegThumbnailPath = join(resizePath, `${asset.id}.jpeg`);
|
||||||
const originalFilename = temp[temp.length - 1].split('.')[0];
|
|
||||||
const jpegThumbnailPath = join(resizePath, `${originalFilename}.jpeg`);
|
|
||||||
|
|
||||||
if (asset.type == AssetType.IMAGE) {
|
if (asset.type == AssetType.IMAGE) {
|
||||||
try {
|
try {
|
||||||
|
@ -2169,12 +2169,38 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"/system-config/storage-template-options": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "getStorageTemplateOptions",
|
||||||
|
"parameters": [],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/SystemConfigTemplateStorageOptionDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"System Config"
|
||||||
|
],
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"info": {
|
"info": {
|
||||||
"title": "Immich",
|
"title": "Immich",
|
||||||
"description": "Immich API",
|
"description": "Immich API",
|
||||||
"version": "1.38.0",
|
"version": "1.38.2",
|
||||||
"contact": {}
|
"contact": {}
|
||||||
},
|
},
|
||||||
"tags": [],
|
"tags": [],
|
||||||
@ -3664,6 +3690,17 @@
|
|||||||
"autoRegister"
|
"autoRegister"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"SystemConfigStorageTemplateDto": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"template": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"template"
|
||||||
|
]
|
||||||
|
},
|
||||||
"SystemConfigDto": {
|
"SystemConfigDto": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@ -3672,11 +3709,71 @@
|
|||||||
},
|
},
|
||||||
"oauth": {
|
"oauth": {
|
||||||
"$ref": "#/components/schemas/SystemConfigOAuthDto"
|
"$ref": "#/components/schemas/SystemConfigOAuthDto"
|
||||||
|
},
|
||||||
|
"storageTemplate": {
|
||||||
|
"$ref": "#/components/schemas/SystemConfigStorageTemplateDto"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"ffmpeg",
|
"ffmpeg",
|
||||||
"oauth"
|
"oauth",
|
||||||
|
"storageTemplate"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"SystemConfigTemplateStorageOptionDto": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"yearOptions": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"monthOptions": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dayOptions": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"hourOptions": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"minuteOptions": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"secondOptions": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"presetOptions": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"yearOptions",
|
||||||
|
"monthOptions",
|
||||||
|
"dayOptions",
|
||||||
|
"hourOptions",
|
||||||
|
"minuteOptions",
|
||||||
|
"secondOptions",
|
||||||
|
"presetOptions"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,7 @@ export enum SystemConfigKey {
|
|||||||
OAUTH_SCOPE = 'oauth.scope',
|
OAUTH_SCOPE = 'oauth.scope',
|
||||||
OAUTH_BUTTON_TEXT = 'oauth.buttonText',
|
OAUTH_BUTTON_TEXT = 'oauth.buttonText',
|
||||||
OAUTH_AUTO_REGISTER = 'oauth.autoRegister',
|
OAUTH_AUTO_REGISTER = 'oauth.autoRegister',
|
||||||
|
STORAGE_TEMPLATE = 'storageTemplate.template',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SystemConfig {
|
export interface SystemConfig {
|
||||||
@ -44,4 +45,7 @@ export interface SystemConfig {
|
|||||||
buttonText: string;
|
buttonText: string;
|
||||||
autoRegister: boolean;
|
autoRegister: boolean;
|
||||||
};
|
};
|
||||||
|
storageTemplate: {
|
||||||
|
template: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,24 @@
|
|||||||
import { SystemConfigEntity } from '@app/database/entities/system-config.entity';
|
import { SystemConfigEntity } from '@app/database/entities/system-config.entity';
|
||||||
import { Module } from '@nestjs/common';
|
import { Module, Provider } from '@nestjs/common';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { ImmichConfigService } from './immich-config.service';
|
import { ImmichConfigService } from './immich-config.service';
|
||||||
|
|
||||||
|
export const INITIAL_SYSTEM_CONFIG = 'INITIAL_SYSTEM_CONFIG';
|
||||||
|
|
||||||
|
const providers: Provider[] = [
|
||||||
|
ImmichConfigService,
|
||||||
|
{
|
||||||
|
provide: INITIAL_SYSTEM_CONFIG,
|
||||||
|
inject: [ImmichConfigService],
|
||||||
|
useFactory: async (configService: ImmichConfigService) => {
|
||||||
|
return configService.getConfig();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature([SystemConfigEntity])],
|
imports: [TypeOrmModule.forFeature([SystemConfigEntity])],
|
||||||
providers: [ImmichConfigService],
|
providers: [...providers],
|
||||||
exports: [ImmichConfigService],
|
exports: [...providers],
|
||||||
})
|
})
|
||||||
export class ImmichConfigModule {}
|
export class ImmichConfigModule {}
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
import { SystemConfig, SystemConfigEntity, SystemConfigKey } from '@app/database/entities/system-config.entity';
|
import { SystemConfig, SystemConfigEntity, SystemConfigKey } from '@app/database/entities/system-config.entity';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
|
import { Subject } from 'rxjs';
|
||||||
import { DeepPartial, In, Repository } from 'typeorm';
|
import { DeepPartial, In, Repository } from 'typeorm';
|
||||||
|
|
||||||
|
export type SystemConfigValidator = (config: SystemConfig) => void | Promise<void>;
|
||||||
|
|
||||||
const defaults: SystemConfig = Object.freeze({
|
const defaults: SystemConfig = Object.freeze({
|
||||||
ffmpeg: {
|
ffmpeg: {
|
||||||
crf: '23',
|
crf: '23',
|
||||||
@ -21,10 +24,19 @@ const defaults: SystemConfig = Object.freeze({
|
|||||||
buttonText: 'Login with OAuth',
|
buttonText: 'Login with OAuth',
|
||||||
autoRegister: true,
|
autoRegister: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
storageTemplate: {
|
||||||
|
template: '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ImmichConfigService {
|
export class ImmichConfigService {
|
||||||
|
private logger = new Logger(ImmichConfigService.name);
|
||||||
|
private validators: SystemConfigValidator[] = [];
|
||||||
|
|
||||||
|
public config$ = new Subject<SystemConfig>();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(SystemConfigEntity)
|
@InjectRepository(SystemConfigEntity)
|
||||||
private systemConfigRepository: Repository<SystemConfigEntity>,
|
private systemConfigRepository: Repository<SystemConfigEntity>,
|
||||||
@ -34,6 +46,10 @@ export class ImmichConfigService {
|
|||||||
return defaults;
|
return defaults;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public addValidator(validator: SystemConfigValidator) {
|
||||||
|
this.validators.push(validator);
|
||||||
|
}
|
||||||
|
|
||||||
public async getConfig() {
|
public async getConfig() {
|
||||||
const overrides = await this.systemConfigRepository.find();
|
const overrides = await this.systemConfigRepository.find();
|
||||||
const config: DeepPartial<SystemConfig> = {};
|
const config: DeepPartial<SystemConfig> = {};
|
||||||
@ -45,7 +61,16 @@ export class ImmichConfigService {
|
|||||||
return _.defaultsDeep(config, defaults) as SystemConfig;
|
return _.defaultsDeep(config, defaults) as SystemConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateConfig(config: DeepPartial<SystemConfig> | null): Promise<void> {
|
public async updateConfig(config: SystemConfig): Promise<SystemConfig> {
|
||||||
|
try {
|
||||||
|
for (const validator of this.validators) {
|
||||||
|
await validator(config);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.warn(`Unable to save system config due to a validation error: ${e}`);
|
||||||
|
throw new BadRequestException(e instanceof Error ? e.message : e);
|
||||||
|
}
|
||||||
|
|
||||||
const updates: SystemConfigEntity[] = [];
|
const updates: SystemConfigEntity[] = [];
|
||||||
const deletes: SystemConfigEntity[] = [];
|
const deletes: SystemConfigEntity[] = [];
|
||||||
|
|
||||||
@ -70,5 +95,11 @@ export class ImmichConfigService {
|
|||||||
if (deletes.length > 0) {
|
if (deletes.length > 0) {
|
||||||
await this.systemConfigRepository.delete({ key: In(deletes.map((item) => item.key)) });
|
await this.systemConfigRepository.delete({ key: In(deletes.map((item) => item.key)) });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const newConfig = await this.getConfig();
|
||||||
|
|
||||||
|
this.config$.next(newConfig);
|
||||||
|
|
||||||
|
return newConfig;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,20 @@
|
|||||||
|
export const supportedYearTokens = ['y', 'yy'];
|
||||||
|
export const supportedMonthTokens = ['M', 'MM', 'MMM', 'MMMM'];
|
||||||
|
export const supportedDayTokens = ['d', 'dd'];
|
||||||
|
export const supportedHourTokens = ['h', 'hh', 'H', 'HH'];
|
||||||
|
export const supportedMinuteTokens = ['m', 'mm'];
|
||||||
|
export const supportedSecondTokens = ['s', 'ss'];
|
||||||
|
export const supportedPresetTokens = [
|
||||||
|
'{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}',
|
||||||
|
'{{y}}/{{MM}}-{{dd}}/{{filename}}',
|
||||||
|
'{{y}}/{{MMMM}}-{{dd}}/{{filename}}',
|
||||||
|
'{{y}}/{{MM}}/{{filename}}',
|
||||||
|
'{{y}}/{{MMM}}/{{filename}}',
|
||||||
|
'{{y}}/{{MMMM}}/{{filename}}',
|
||||||
|
'{{y}}/{{MM}}/{{dd}}/{{filename}}',
|
||||||
|
'{{y}}/{{MMMM}}/{{dd}}/{{filename}}',
|
||||||
|
'{{y}}/{{y}}-{{MM}}/{{y}}-{{MM}}-{{dd}}/{{filename}}',
|
||||||
|
'{{y}}-{{MM}}-{{dd}}/{{filename}}',
|
||||||
|
'{{y}}-{{MMM}}-{{dd}}/{{filename}}',
|
||||||
|
'{{y}}-{{MMMM}}-{{dd}}/{{filename}}',
|
||||||
|
];
|
2
server/libs/storage/src/index.ts
Normal file
2
server/libs/storage/src/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './storage.module';
|
||||||
|
export * from './storage.service';
|
@ -0,0 +1,6 @@
|
|||||||
|
export interface IImmichStorage {
|
||||||
|
write(): Promise<void>;
|
||||||
|
read(): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum IStorageType {}
|
13
server/libs/storage/src/storage.module.ts
Normal file
13
server/libs/storage/src/storage.module.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { AssetEntity } from '@app/database/entities/asset.entity';
|
||||||
|
import { SystemConfigEntity } from '@app/database/entities/system-config.entity';
|
||||||
|
import { ImmichConfigModule } from '@app/immich-config';
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { StorageService } from './storage.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [TypeOrmModule.forFeature([AssetEntity, SystemConfigEntity]), ImmichConfigModule],
|
||||||
|
providers: [StorageService],
|
||||||
|
exports: [StorageService],
|
||||||
|
})
|
||||||
|
export class StorageModule {}
|
153
server/libs/storage/src/storage.service.ts
Normal file
153
server/libs/storage/src/storage.service.ts
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
import { APP_UPLOAD_LOCATION } from '@app/common';
|
||||||
|
import { AssetEntity } from '@app/database/entities/asset.entity';
|
||||||
|
import { SystemConfig } from '@app/database/entities/system-config.entity';
|
||||||
|
import { ImmichConfigService, INITIAL_SYSTEM_CONFIG } from '@app/immich-config';
|
||||||
|
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import fsPromise from 'fs/promises';
|
||||||
|
import handlebar from 'handlebars';
|
||||||
|
import * as luxon from 'luxon';
|
||||||
|
import mv from 'mv';
|
||||||
|
import { constants } from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { promisify } from 'node:util';
|
||||||
|
import sanitize from 'sanitize-filename';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import {
|
||||||
|
supportedDayTokens,
|
||||||
|
supportedHourTokens,
|
||||||
|
supportedMinuteTokens,
|
||||||
|
supportedMonthTokens,
|
||||||
|
supportedSecondTokens,
|
||||||
|
supportedYearTokens,
|
||||||
|
} from './constants/supported-datetime-template';
|
||||||
|
|
||||||
|
const moveFile = promisify<string, string, mv.Options>(mv);
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class StorageService {
|
||||||
|
readonly log = new Logger(StorageService.name);
|
||||||
|
|
||||||
|
private storageTemplate: HandlebarsTemplateDelegate<any>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(AssetEntity)
|
||||||
|
private assetRepository: Repository<AssetEntity>,
|
||||||
|
private immichConfigService: ImmichConfigService,
|
||||||
|
@Inject(INITIAL_SYSTEM_CONFIG) config: SystemConfig,
|
||||||
|
) {
|
||||||
|
this.storageTemplate = this.compile(config.storageTemplate.template);
|
||||||
|
|
||||||
|
this.immichConfigService.addValidator((config) => this.validateConfig(config));
|
||||||
|
|
||||||
|
this.immichConfigService.config$.subscribe((config) => {
|
||||||
|
this.log.debug(`Received new config, recompiling storage template: ${config.storageTemplate.template}`);
|
||||||
|
this.storageTemplate = this.compile(config.storageTemplate.template);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async moveAsset(asset: AssetEntity, filename: string): Promise<AssetEntity> {
|
||||||
|
try {
|
||||||
|
const source = asset.originalPath;
|
||||||
|
const ext = path.extname(source).split('.').pop() as string;
|
||||||
|
const sanitized = sanitize(path.basename(filename, `.${ext}`));
|
||||||
|
const rootPath = path.join(APP_UPLOAD_LOCATION, asset.userId);
|
||||||
|
const storagePath = this.render(this.storageTemplate, asset, sanitized, ext);
|
||||||
|
const fullPath = path.normalize(path.join(rootPath, storagePath));
|
||||||
|
|
||||||
|
if (!fullPath.startsWith(rootPath)) {
|
||||||
|
this.log.warn(`Skipped attempt to access an invalid path: ${fullPath}. Path should start with ${rootPath}`);
|
||||||
|
return asset;
|
||||||
|
}
|
||||||
|
|
||||||
|
let duplicateCount = 0;
|
||||||
|
let destination = `${fullPath}.${ext}`;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const exists = await this.checkFileExist(destination);
|
||||||
|
if (!exists) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
duplicateCount++;
|
||||||
|
destination = `${fullPath}_${duplicateCount}.${ext}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.safeMove(source, destination);
|
||||||
|
|
||||||
|
asset.originalPath = destination;
|
||||||
|
return await this.assetRepository.save(asset);
|
||||||
|
} catch (error: any) {
|
||||||
|
this.log.error(error, error.stack);
|
||||||
|
return asset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private safeMove(source: string, destination: string): Promise<void> {
|
||||||
|
return moveFile(source, destination, { mkdirp: true, clobber: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
private async checkFileExist(path: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await fsPromise.access(path, constants.F_OK);
|
||||||
|
return true;
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private validateConfig(config: SystemConfig) {
|
||||||
|
this.validateStorageTemplate(config.storageTemplate.template);
|
||||||
|
}
|
||||||
|
|
||||||
|
private validateStorageTemplate(templateString: string) {
|
||||||
|
try {
|
||||||
|
const template = this.compile(templateString);
|
||||||
|
|
||||||
|
// test render an asset
|
||||||
|
this.render(
|
||||||
|
template,
|
||||||
|
{
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
originalPath: '/upload/test/IMG_123.jpg',
|
||||||
|
} as AssetEntity,
|
||||||
|
'IMG_123',
|
||||||
|
'jpg',
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
this.log.warn(`Storage template validation failed: ${e}`);
|
||||||
|
throw new Error(`Invalid storage template: ${e}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private compile(template: string) {
|
||||||
|
return handlebar.compile(template, {
|
||||||
|
knownHelpers: undefined,
|
||||||
|
strict: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private render(template: HandlebarsTemplateDelegate<any>, asset: AssetEntity, filename: string, ext: string) {
|
||||||
|
const substitutions: Record<string, string> = {
|
||||||
|
filename,
|
||||||
|
ext,
|
||||||
|
};
|
||||||
|
|
||||||
|
const dt = luxon.DateTime.fromISO(new Date(asset.createdAt).toISOString());
|
||||||
|
|
||||||
|
const dateTokens = [
|
||||||
|
...supportedYearTokens,
|
||||||
|
...supportedMonthTokens,
|
||||||
|
...supportedDayTokens,
|
||||||
|
...supportedHourTokens,
|
||||||
|
...supportedMinuteTokens,
|
||||||
|
...supportedSecondTokens,
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const token of dateTokens) {
|
||||||
|
substitutions[token] = dt.toFormat(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
return template(substitutions);
|
||||||
|
}
|
||||||
|
}
|
9
server/libs/storage/tsconfig.lib.json
Normal file
9
server/libs/storage/tsconfig.lib.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"declaration": true,
|
||||||
|
"outDir": "../../dist/libs/storage"
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist", "test", "**/*spec.ts"]
|
||||||
|
}
|
@ -79,6 +79,15 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"tsConfigPath": "libs/immich-config/tsconfig.lib.json"
|
"tsConfigPath": "libs/immich-config/tsconfig.lib.json"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"storage": {
|
||||||
|
"type": "library",
|
||||||
|
"root": "libs/storage",
|
||||||
|
"entryFile": "index",
|
||||||
|
"sourceRoot": "libs/storage/src",
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsConfigPath": "libs/storage/tsconfig.lib.json"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
180
server/package-lock.json
generated
180
server/package-lock.json
generated
@ -36,11 +36,13 @@
|
|||||||
"fdir": "^5.3.0",
|
"fdir": "^5.3.0",
|
||||||
"fluent-ffmpeg": "^2.1.2",
|
"fluent-ffmpeg": "^2.1.2",
|
||||||
"geo-tz": "^7.0.2",
|
"geo-tz": "^7.0.2",
|
||||||
|
"handlebars": "^4.7.7",
|
||||||
"i18n-iso-countries": "^7.5.0",
|
"i18n-iso-countries": "^7.5.0",
|
||||||
"joi": "^17.5.0",
|
"joi": "^17.5.0",
|
||||||
"local-reverse-geocoder": "^0.12.5",
|
"local-reverse-geocoder": "^0.12.5",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"luxon": "^3.0.3",
|
"luxon": "^3.0.3",
|
||||||
|
"mv": "^2.1.1",
|
||||||
"nest-commander": "^3.3.0",
|
"nest-commander": "^3.3.0",
|
||||||
"openid-client": "^5.2.1",
|
"openid-client": "^5.2.1",
|
||||||
"passport": "^0.6.0",
|
"passport": "^0.6.0",
|
||||||
@ -76,6 +78,7 @@
|
|||||||
"@types/jest": "27.0.2",
|
"@types/jest": "27.0.2",
|
||||||
"@types/lodash": "^4.14.178",
|
"@types/lodash": "^4.14.178",
|
||||||
"@types/multer": "^1.4.7",
|
"@types/multer": "^1.4.7",
|
||||||
|
"@types/mv": "^2.1.2",
|
||||||
"@types/node": "^16.0.0",
|
"@types/node": "^16.0.0",
|
||||||
"@types/passport-jwt": "^3.0.6",
|
"@types/passport-jwt": "^3.0.6",
|
||||||
"@types/sharp": "^0.30.2",
|
"@types/sharp": "^0.30.2",
|
||||||
@ -2544,6 +2547,12 @@
|
|||||||
"@types/express": "*"
|
"@types/express": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/mv": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/mv/-/mv-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-IvAjPuiQ2exDicnTrMidt1m+tj3gZ60BM0PaoRsU0m9Cn+lrOyemuO9Tf8CvHFmXlxMjr1TVCfadi9sfwbSuKg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "16.11.21",
|
"version": "16.11.21",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.21.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.21.tgz",
|
||||||
@ -6168,6 +6177,34 @@
|
|||||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz",
|
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz",
|
||||||
"integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ=="
|
"integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/handlebars": {
|
||||||
|
"version": "4.7.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz",
|
||||||
|
"integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==",
|
||||||
|
"dependencies": {
|
||||||
|
"minimist": "^1.2.5",
|
||||||
|
"neo-async": "^2.6.0",
|
||||||
|
"source-map": "^0.6.1",
|
||||||
|
"wordwrap": "^1.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"handlebars": "bin/handlebars"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.7"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"uglify-js": "^3.1.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/handlebars/node_modules/source-map": {
|
||||||
|
"version": "0.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||||
|
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/har-schema": {
|
"node_modules/har-schema": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
|
||||||
@ -8178,6 +8215,45 @@
|
|||||||
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
|
||||||
"integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA=="
|
"integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA=="
|
||||||
},
|
},
|
||||||
|
"node_modules/mv": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-at/ZndSy3xEGJ8i0ygALh8ru9qy7gWW1cmkaqBN29JmMlIvM//MEO9y1sk/avxuwnPcfhkejkLsuPxH81BrkSg==",
|
||||||
|
"dependencies": {
|
||||||
|
"mkdirp": "~0.5.1",
|
||||||
|
"ncp": "~2.0.0",
|
||||||
|
"rimraf": "~2.4.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mv/node_modules/glob": {
|
||||||
|
"version": "6.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz",
|
||||||
|
"integrity": "sha512-MKZeRNyYZAVVVG1oZeLaWie1uweH40m9AZwIwxyPbTSX4hHrVYSzLg0Ro5Z5R7XKkIX+Cc6oD1rqeDJnwsB8/A==",
|
||||||
|
"dependencies": {
|
||||||
|
"inflight": "^1.0.4",
|
||||||
|
"inherits": "2",
|
||||||
|
"minimatch": "2 || 3",
|
||||||
|
"once": "^1.3.0",
|
||||||
|
"path-is-absolute": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mv/node_modules/rimraf": {
|
||||||
|
"version": "2.4.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz",
|
||||||
|
"integrity": "sha512-J5xnxTyqaiw06JjMftq7L9ouA448dw/E7dKghkP9WpKNuwmARNNg+Gk8/u5ryb9N/Yo2+z3MCwuqFK/+qPOPfQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"glob": "^6.0.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"rimraf": "bin.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/mz": {
|
"node_modules/mz": {
|
||||||
"version": "2.7.0",
|
"version": "2.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
|
||||||
@ -8204,6 +8280,14 @@
|
|||||||
"integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=",
|
"integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/ncp": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==",
|
||||||
|
"bin": {
|
||||||
|
"ncp": "bin/ncp"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/negotiator": {
|
"node_modules/negotiator": {
|
||||||
"version": "0.6.3",
|
"version": "0.6.3",
|
||||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
||||||
@ -8215,8 +8299,7 @@
|
|||||||
"node_modules/neo-async": {
|
"node_modules/neo-async": {
|
||||||
"version": "2.6.2",
|
"version": "2.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
|
||||||
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
|
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/nest-commander": {
|
"node_modules/nest-commander": {
|
||||||
"version": "3.3.0",
|
"version": "3.3.0",
|
||||||
@ -11006,6 +11089,18 @@
|
|||||||
"node": ">=4.2.0"
|
"node": ">=4.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/uglify-js": {
|
||||||
|
"version": "3.17.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz",
|
||||||
|
"integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==",
|
||||||
|
"optional": true,
|
||||||
|
"bin": {
|
||||||
|
"uglifyjs": "bin/uglifyjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/uid2": {
|
"node_modules/uid2": {
|
||||||
"version": "0.0.3",
|
"version": "0.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.3.tgz",
|
||||||
@ -11329,6 +11424,11 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/wordwrap": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q=="
|
||||||
|
},
|
||||||
"node_modules/wrap-ansi": {
|
"node_modules/wrap-ansi": {
|
||||||
"version": "7.0.0",
|
"version": "7.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||||
@ -13393,6 +13493,12 @@
|
|||||||
"@types/express": "*"
|
"@types/express": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/mv": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/mv/-/mv-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-IvAjPuiQ2exDicnTrMidt1m+tj3gZ60BM0PaoRsU0m9Cn+lrOyemuO9Tf8CvHFmXlxMjr1TVCfadi9sfwbSuKg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"@types/node": {
|
"@types/node": {
|
||||||
"version": "16.11.21",
|
"version": "16.11.21",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.21.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.21.tgz",
|
||||||
@ -16213,6 +16319,25 @@
|
|||||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz",
|
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz",
|
||||||
"integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ=="
|
"integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ=="
|
||||||
},
|
},
|
||||||
|
"handlebars": {
|
||||||
|
"version": "4.7.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz",
|
||||||
|
"integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==",
|
||||||
|
"requires": {
|
||||||
|
"minimist": "^1.2.5",
|
||||||
|
"neo-async": "^2.6.0",
|
||||||
|
"source-map": "^0.6.1",
|
||||||
|
"uglify-js": "^3.1.4",
|
||||||
|
"wordwrap": "^1.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"source-map": {
|
||||||
|
"version": "0.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||||
|
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"har-schema": {
|
"har-schema": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
|
||||||
@ -17773,6 +17898,38 @@
|
|||||||
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
|
||||||
"integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA=="
|
"integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA=="
|
||||||
},
|
},
|
||||||
|
"mv": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-at/ZndSy3xEGJ8i0ygALh8ru9qy7gWW1cmkaqBN29JmMlIvM//MEO9y1sk/avxuwnPcfhkejkLsuPxH81BrkSg==",
|
||||||
|
"requires": {
|
||||||
|
"mkdirp": "~0.5.1",
|
||||||
|
"ncp": "~2.0.0",
|
||||||
|
"rimraf": "~2.4.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"glob": {
|
||||||
|
"version": "6.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz",
|
||||||
|
"integrity": "sha512-MKZeRNyYZAVVVG1oZeLaWie1uweH40m9AZwIwxyPbTSX4hHrVYSzLg0Ro5Z5R7XKkIX+Cc6oD1rqeDJnwsB8/A==",
|
||||||
|
"requires": {
|
||||||
|
"inflight": "^1.0.4",
|
||||||
|
"inherits": "2",
|
||||||
|
"minimatch": "2 || 3",
|
||||||
|
"once": "^1.3.0",
|
||||||
|
"path-is-absolute": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rimraf": {
|
||||||
|
"version": "2.4.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz",
|
||||||
|
"integrity": "sha512-J5xnxTyqaiw06JjMftq7L9ouA448dw/E7dKghkP9WpKNuwmARNNg+Gk8/u5ryb9N/Yo2+z3MCwuqFK/+qPOPfQ==",
|
||||||
|
"requires": {
|
||||||
|
"glob": "^6.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"mz": {
|
"mz": {
|
||||||
"version": "2.7.0",
|
"version": "2.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
|
||||||
@ -17799,6 +17956,11 @@
|
|||||||
"integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=",
|
"integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"ncp": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA=="
|
||||||
|
},
|
||||||
"negotiator": {
|
"negotiator": {
|
||||||
"version": "0.6.3",
|
"version": "0.6.3",
|
||||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
||||||
@ -17807,8 +17969,7 @@
|
|||||||
"neo-async": {
|
"neo-async": {
|
||||||
"version": "2.6.2",
|
"version": "2.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
|
||||||
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
|
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"nest-commander": {
|
"nest-commander": {
|
||||||
"version": "3.3.0",
|
"version": "3.3.0",
|
||||||
@ -19794,6 +19955,12 @@
|
|||||||
"integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==",
|
"integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==",
|
||||||
"devOptional": true
|
"devOptional": true
|
||||||
},
|
},
|
||||||
|
"uglify-js": {
|
||||||
|
"version": "3.17.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz",
|
||||||
|
"integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"uid2": {
|
"uid2": {
|
||||||
"version": "0.0.3",
|
"version": "0.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.3.tgz",
|
||||||
@ -20049,6 +20216,11 @@
|
|||||||
"integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
|
"integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"wordwrap": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q=="
|
||||||
|
},
|
||||||
"wrap-ansi": {
|
"wrap-ansi": {
|
||||||
"version": "7.0.0",
|
"version": "7.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||||
|
@ -59,11 +59,13 @@
|
|||||||
"fdir": "^5.3.0",
|
"fdir": "^5.3.0",
|
||||||
"fluent-ffmpeg": "^2.1.2",
|
"fluent-ffmpeg": "^2.1.2",
|
||||||
"geo-tz": "^7.0.2",
|
"geo-tz": "^7.0.2",
|
||||||
|
"handlebars": "^4.7.7",
|
||||||
"i18n-iso-countries": "^7.5.0",
|
"i18n-iso-countries": "^7.5.0",
|
||||||
"joi": "^17.5.0",
|
"joi": "^17.5.0",
|
||||||
"local-reverse-geocoder": "^0.12.5",
|
"local-reverse-geocoder": "^0.12.5",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"luxon": "^3.0.3",
|
"luxon": "^3.0.3",
|
||||||
|
"mv": "^2.1.1",
|
||||||
"nest-commander": "^3.3.0",
|
"nest-commander": "^3.3.0",
|
||||||
"openid-client": "^5.2.1",
|
"openid-client": "^5.2.1",
|
||||||
"passport": "^0.6.0",
|
"passport": "^0.6.0",
|
||||||
@ -96,6 +98,7 @@
|
|||||||
"@types/jest": "27.0.2",
|
"@types/jest": "27.0.2",
|
||||||
"@types/lodash": "^4.14.178",
|
"@types/lodash": "^4.14.178",
|
||||||
"@types/multer": "^1.4.7",
|
"@types/multer": "^1.4.7",
|
||||||
|
"@types/mv": "^2.1.2",
|
||||||
"@types/node": "^16.0.0",
|
"@types/node": "^16.0.0",
|
||||||
"@types/passport-jwt": "^3.0.6",
|
"@types/passport-jwt": "^3.0.6",
|
||||||
"@types/sharp": "^0.30.2",
|
"@types/sharp": "^0.30.2",
|
||||||
@ -142,7 +145,8 @@
|
|||||||
"@app/database/config": "<rootDir>/libs/database/src/config",
|
"@app/database/config": "<rootDir>/libs/database/src/config",
|
||||||
"@app/common": "<rootDir>/libs/common/src",
|
"@app/common": "<rootDir>/libs/common/src",
|
||||||
"^@app/job(|/.*)$": "<rootDir>/libs/job/src/$1",
|
"^@app/job(|/.*)$": "<rootDir>/libs/job/src/$1",
|
||||||
"^@app/immich-config(|/.*)$": "<rootDir>/libs/immich-config/src/$1"
|
"^@app/immich-config(|/.*)$": "<rootDir>/libs/immich-config/src/$1",
|
||||||
|
"^@app/storage(|/.*)$": "<rootDir>/libs/storage/src/$1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,15 +16,41 @@
|
|||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"baseUrl": "./",
|
"baseUrl": "./",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@app/common": ["libs/common/src"],
|
"@app/common": [
|
||||||
"@app/common/*": ["libs/common/src/*"],
|
"libs/common/src"
|
||||||
"@app/database": ["libs/database/src"],
|
],
|
||||||
"@app/database/*": ["libs/database/src/*"],
|
"@app/common/*": [
|
||||||
"@app/job": ["libs/job/src"],
|
"libs/common/src/*"
|
||||||
"@app/job/*": ["libs/job/src/*"],
|
],
|
||||||
"@app/immich-config": ["libs/immich-config/src"],
|
"@app/database": [
|
||||||
"@app/immich-config/*": ["libs/immich-config/src/*"]
|
"libs/database/src"
|
||||||
|
],
|
||||||
|
"@app/database/*": [
|
||||||
|
"libs/database/src/*"
|
||||||
|
],
|
||||||
|
"@app/job": [
|
||||||
|
"libs/job/src"
|
||||||
|
],
|
||||||
|
"@app/job/*": [
|
||||||
|
"libs/job/src/*"
|
||||||
|
],
|
||||||
|
"@app/immich-config": [
|
||||||
|
"libs/immich-config/src"
|
||||||
|
],
|
||||||
|
"@app/immich-config/*": [
|
||||||
|
"libs/immich-config/src/*"
|
||||||
|
],
|
||||||
|
"@app/storage": [
|
||||||
|
"libs/storage/src"
|
||||||
|
],
|
||||||
|
"@app/storage/*": [
|
||||||
|
"libs/storage/src/*"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"exclude": ["dist", "node_modules", "upload"]
|
"exclude": [
|
||||||
}
|
"dist",
|
||||||
|
"node_modules",
|
||||||
|
"upload"
|
||||||
|
]
|
||||||
|
}
|
106
web/package-lock.json
generated
106
web/package-lock.json
generated
@ -12,9 +12,11 @@
|
|||||||
"cookie": "^0.4.2",
|
"cookie": "^0.4.2",
|
||||||
"copy-image-clipboard": "^2.1.2",
|
"copy-image-clipboard": "^2.1.2",
|
||||||
"exifr": "^7.1.3",
|
"exifr": "^7.1.3",
|
||||||
|
"handlebars": "^4.7.7",
|
||||||
"leaflet": "^1.8.0",
|
"leaflet": "^1.8.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
|
"luxon": "^3.1.1",
|
||||||
"socket.io-client": "^4.5.1",
|
"socket.io-client": "^4.5.1",
|
||||||
"svelte-keydown": "^0.5.0",
|
"svelte-keydown": "^0.5.0",
|
||||||
"svelte-material-icons": "^2.0.2"
|
"svelte-material-icons": "^2.0.2"
|
||||||
@ -34,6 +36,7 @@
|
|||||||
"@types/leaflet": "^1.7.10",
|
"@types/leaflet": "^1.7.10",
|
||||||
"@types/lodash": "^4.14.182",
|
"@types/lodash": "^4.14.182",
|
||||||
"@types/lodash-es": "^4.17.6",
|
"@types/lodash-es": "^4.17.6",
|
||||||
|
"@types/luxon": "^3.1.0",
|
||||||
"@types/socket.io-client": "^3.0.0",
|
"@types/socket.io-client": "^3.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.27.0",
|
"@typescript-eslint/eslint-plugin": "^5.27.0",
|
||||||
"@typescript-eslint/parser": "^5.27.0",
|
"@typescript-eslint/parser": "^5.27.0",
|
||||||
@ -3319,6 +3322,12 @@
|
|||||||
"@types/lodash": "*"
|
"@types/lodash": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/luxon": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-gCd/HcCgjqSxfMrgtqxCgYk/22NBQfypwFUG7ZAyG/4pqs51WLTcUzVp1hqTbieDYeHS3WoVEh2Yv/2l+7B0Vg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "18.11.11",
|
"version": "18.11.11",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.11.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.11.tgz",
|
||||||
@ -6149,6 +6158,26 @@
|
|||||||
"integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==",
|
"integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/handlebars": {
|
||||||
|
"version": "4.7.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz",
|
||||||
|
"integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==",
|
||||||
|
"dependencies": {
|
||||||
|
"minimist": "^1.2.5",
|
||||||
|
"neo-async": "^2.6.0",
|
||||||
|
"source-map": "^0.6.1",
|
||||||
|
"wordwrap": "^1.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"handlebars": "bin/handlebars"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.7"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"uglify-js": "^3.1.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/has": {
|
"node_modules/has": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
|
||||||
@ -8976,6 +9005,14 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/luxon": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-Ah6DloGmvseB/pX1cAmjbFvyU/pKuwQMQqz7d0yvuDlVYLTs2WeDHQMpC8tGjm1da+BriHROW/OEIT/KfYg6xw==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/lz-string": {
|
"node_modules/lz-string": {
|
||||||
"version": "1.4.4",
|
"version": "1.4.4",
|
||||||
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz",
|
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz",
|
||||||
@ -9114,7 +9151,6 @@
|
|||||||
"version": "1.2.7",
|
"version": "1.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz",
|
||||||
"integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==",
|
"integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==",
|
||||||
"dev": true,
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
@ -9178,6 +9214,11 @@
|
|||||||
"integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==",
|
"integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/neo-async": {
|
||||||
|
"version": "2.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
|
||||||
|
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="
|
||||||
|
},
|
||||||
"node_modules/node-int64": {
|
"node_modules/node-int64": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
|
||||||
@ -10280,7 +10321,6 @@
|
|||||||
"version": "0.6.1",
|
"version": "0.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@ -10835,6 +10875,18 @@
|
|||||||
"node": ">=4.2.0"
|
"node": ">=4.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/uglify-js": {
|
||||||
|
"version": "3.17.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz",
|
||||||
|
"integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==",
|
||||||
|
"optional": true,
|
||||||
|
"bin": {
|
||||||
|
"uglifyjs": "bin/uglifyjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/undici": {
|
"node_modules/undici": {
|
||||||
"version": "5.13.0",
|
"version": "5.13.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici/-/undici-5.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici/-/undici-5.13.0.tgz",
|
||||||
@ -11163,6 +11215,11 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/wordwrap": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q=="
|
||||||
|
},
|
||||||
"node_modules/wrap-ansi": {
|
"node_modules/wrap-ansi": {
|
||||||
"version": "7.0.0",
|
"version": "7.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||||
@ -13726,6 +13783,12 @@
|
|||||||
"@types/lodash": "*"
|
"@types/lodash": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/luxon": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-gCd/HcCgjqSxfMrgtqxCgYk/22NBQfypwFUG7ZAyG/4pqs51WLTcUzVp1hqTbieDYeHS3WoVEh2Yv/2l+7B0Vg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"@types/node": {
|
"@types/node": {
|
||||||
"version": "18.11.11",
|
"version": "18.11.11",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.11.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.11.tgz",
|
||||||
@ -15703,6 +15766,18 @@
|
|||||||
"integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==",
|
"integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"handlebars": {
|
||||||
|
"version": "4.7.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz",
|
||||||
|
"integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==",
|
||||||
|
"requires": {
|
||||||
|
"minimist": "^1.2.5",
|
||||||
|
"neo-async": "^2.6.0",
|
||||||
|
"source-map": "^0.6.1",
|
||||||
|
"uglify-js": "^3.1.4",
|
||||||
|
"wordwrap": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"has": {
|
"has": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
|
||||||
@ -17789,6 +17864,11 @@
|
|||||||
"yallist": "^4.0.0"
|
"yallist": "^4.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"luxon": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-Ah6DloGmvseB/pX1cAmjbFvyU/pKuwQMQqz7d0yvuDlVYLTs2WeDHQMpC8tGjm1da+BriHROW/OEIT/KfYg6xw=="
|
||||||
|
},
|
||||||
"lz-string": {
|
"lz-string": {
|
||||||
"version": "1.4.4",
|
"version": "1.4.4",
|
||||||
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz",
|
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz",
|
||||||
@ -17887,8 +17967,7 @@
|
|||||||
"minimist": {
|
"minimist": {
|
||||||
"version": "1.2.7",
|
"version": "1.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz",
|
||||||
"integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==",
|
"integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"mkdirp": {
|
"mkdirp": {
|
||||||
"version": "0.5.6",
|
"version": "0.5.6",
|
||||||
@ -17934,6 +18013,11 @@
|
|||||||
"integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==",
|
"integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"neo-async": {
|
||||||
|
"version": "2.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
|
||||||
|
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="
|
||||||
|
},
|
||||||
"node-int64": {
|
"node-int64": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
|
||||||
@ -18708,8 +18792,7 @@
|
|||||||
"source-map": {
|
"source-map": {
|
||||||
"version": "0.6.1",
|
"version": "0.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"source-map-js": {
|
"source-map-js": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
@ -19092,6 +19175,12 @@
|
|||||||
"integrity": "sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA==",
|
"integrity": "sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"uglify-js": {
|
||||||
|
"version": "3.17.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz",
|
||||||
|
"integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"undici": {
|
"undici": {
|
||||||
"version": "5.13.0",
|
"version": "5.13.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici/-/undici-5.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici/-/undici-5.13.0.tgz",
|
||||||
@ -19304,6 +19393,11 @@
|
|||||||
"integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
|
"integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"wordwrap": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q=="
|
||||||
|
},
|
||||||
"wrap-ansi": {
|
"wrap-ansi": {
|
||||||
"version": "7.0.0",
|
"version": "7.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||||
|
@ -32,6 +32,7 @@
|
|||||||
"@types/leaflet": "^1.7.10",
|
"@types/leaflet": "^1.7.10",
|
||||||
"@types/lodash": "^4.14.182",
|
"@types/lodash": "^4.14.182",
|
||||||
"@types/lodash-es": "^4.17.6",
|
"@types/lodash-es": "^4.17.6",
|
||||||
|
"@types/luxon": "^3.1.0",
|
||||||
"@types/socket.io-client": "^3.0.0",
|
"@types/socket.io-client": "^3.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.27.0",
|
"@typescript-eslint/eslint-plugin": "^5.27.0",
|
||||||
"@typescript-eslint/parser": "^5.27.0",
|
"@typescript-eslint/parser": "^5.27.0",
|
||||||
@ -62,9 +63,11 @@
|
|||||||
"cookie": "^0.4.2",
|
"cookie": "^0.4.2",
|
||||||
"copy-image-clipboard": "^2.1.2",
|
"copy-image-clipboard": "^2.1.2",
|
||||||
"exifr": "^7.1.3",
|
"exifr": "^7.1.3",
|
||||||
|
"handlebars": "^4.7.7",
|
||||||
"leaflet": "^1.8.0",
|
"leaflet": "^1.8.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
|
"luxon": "^3.1.1",
|
||||||
"socket.io-client": "^4.5.1",
|
"socket.io-client": "^4.5.1",
|
||||||
"svelte-keydown": "^0.5.0",
|
"svelte-keydown": "^0.5.0",
|
||||||
"svelte-material-icons": "^2.0.2"
|
"svelte-material-icons": "^2.0.2"
|
||||||
|
130
web/src/api/open-api/api.ts
generated
130
web/src/api/open-api/api.ts
generated
@ -4,7 +4,7 @@
|
|||||||
* Immich
|
* Immich
|
||||||
* Immich API
|
* Immich API
|
||||||
*
|
*
|
||||||
* The version of the OpenAPI document: 1.38.0
|
* The version of the OpenAPI document: 1.38.2
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||||
@ -1443,6 +1443,12 @@ export interface SystemConfigDto {
|
|||||||
* @memberof SystemConfigDto
|
* @memberof SystemConfigDto
|
||||||
*/
|
*/
|
||||||
'oauth': SystemConfigOAuthDto;
|
'oauth': SystemConfigOAuthDto;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {SystemConfigStorageTemplateDto}
|
||||||
|
* @memberof SystemConfigDto
|
||||||
|
*/
|
||||||
|
'storageTemplate': SystemConfigStorageTemplateDto;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -1530,6 +1536,68 @@ export interface SystemConfigOAuthDto {
|
|||||||
*/
|
*/
|
||||||
'autoRegister': boolean;
|
'autoRegister': boolean;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
* @interface SystemConfigStorageTemplateDto
|
||||||
|
*/
|
||||||
|
export interface SystemConfigStorageTemplateDto {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof SystemConfigStorageTemplateDto
|
||||||
|
*/
|
||||||
|
'template': string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
* @interface SystemConfigTemplateStorageOptionDto
|
||||||
|
*/
|
||||||
|
export interface SystemConfigTemplateStorageOptionDto {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {Array<string>}
|
||||||
|
* @memberof SystemConfigTemplateStorageOptionDto
|
||||||
|
*/
|
||||||
|
'yearOptions': Array<string>;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {Array<string>}
|
||||||
|
* @memberof SystemConfigTemplateStorageOptionDto
|
||||||
|
*/
|
||||||
|
'monthOptions': Array<string>;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {Array<string>}
|
||||||
|
* @memberof SystemConfigTemplateStorageOptionDto
|
||||||
|
*/
|
||||||
|
'dayOptions': Array<string>;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {Array<string>}
|
||||||
|
* @memberof SystemConfigTemplateStorageOptionDto
|
||||||
|
*/
|
||||||
|
'hourOptions': Array<string>;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {Array<string>}
|
||||||
|
* @memberof SystemConfigTemplateStorageOptionDto
|
||||||
|
*/
|
||||||
|
'minuteOptions': Array<string>;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {Array<string>}
|
||||||
|
* @memberof SystemConfigTemplateStorageOptionDto
|
||||||
|
*/
|
||||||
|
'secondOptions': Array<string>;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {Array<string>}
|
||||||
|
* @memberof SystemConfigTemplateStorageOptionDto
|
||||||
|
*/
|
||||||
|
'presetOptions': Array<string>;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @export
|
* @export
|
||||||
@ -5312,6 +5380,39 @@ export const SystemConfigApiAxiosParamCreator = function (configuration?: Config
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||||
|
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||||
|
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||||
|
|
||||||
|
return {
|
||||||
|
url: toPathString(localVarUrlObj),
|
||||||
|
options: localVarRequestOptions,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
getStorageTemplateOptions: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||||
|
const localVarPath = `/system-config/storage-template-options`;
|
||||||
|
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||||
|
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||||
|
let baseOptions;
|
||||||
|
if (configuration) {
|
||||||
|
baseOptions = configuration.baseOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
|
||||||
|
const localVarHeaderParameter = {} as any;
|
||||||
|
const localVarQueryParameter = {} as any;
|
||||||
|
|
||||||
|
// authentication bearer required
|
||||||
|
// http bearer authentication required
|
||||||
|
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||||
@ -5388,6 +5489,15 @@ export const SystemConfigApiFp = function(configuration?: Configuration) {
|
|||||||
const localVarAxiosArgs = await localVarAxiosParamCreator.getDefaults(options);
|
const localVarAxiosArgs = await localVarAxiosParamCreator.getDefaults(options);
|
||||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
async getStorageTemplateOptions(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SystemConfigTemplateStorageOptionDto>> {
|
||||||
|
const localVarAxiosArgs = await localVarAxiosParamCreator.getStorageTemplateOptions(options);
|
||||||
|
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {SystemConfigDto} systemConfigDto
|
* @param {SystemConfigDto} systemConfigDto
|
||||||
@ -5424,6 +5534,14 @@ export const SystemConfigApiFactory = function (configuration?: Configuration, b
|
|||||||
getDefaults(options?: any): AxiosPromise<SystemConfigDto> {
|
getDefaults(options?: any): AxiosPromise<SystemConfigDto> {
|
||||||
return localVarFp.getDefaults(options).then((request) => request(axios, basePath));
|
return localVarFp.getDefaults(options).then((request) => request(axios, basePath));
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
getStorageTemplateOptions(options?: any): AxiosPromise<SystemConfigTemplateStorageOptionDto> {
|
||||||
|
return localVarFp.getStorageTemplateOptions(options).then((request) => request(axios, basePath));
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {SystemConfigDto} systemConfigDto
|
* @param {SystemConfigDto} systemConfigDto
|
||||||
@ -5463,6 +5581,16 @@ export class SystemConfigApi extends BaseAPI {
|
|||||||
return SystemConfigApiFp(this.configuration).getDefaults(options).then((request) => request(this.axios, this.basePath));
|
return SystemConfigApiFp(this.configuration).getDefaults(options).then((request) => request(this.axios, this.basePath));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
* @memberof SystemConfigApi
|
||||||
|
*/
|
||||||
|
public getStorageTemplateOptions(options?: AxiosRequestConfig) {
|
||||||
|
return SystemConfigApiFp(this.configuration).getStorageTemplateOptions(options).then((request) => request(this.axios, this.basePath));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {SystemConfigDto} systemConfigDto
|
* @param {SystemConfigDto} systemConfigDto
|
||||||
|
2
web/src/api/open-api/base.ts
generated
2
web/src/api/open-api/base.ts
generated
@ -4,7 +4,7 @@
|
|||||||
* Immich
|
* Immich
|
||||||
* Immich API
|
* Immich API
|
||||||
*
|
*
|
||||||
* The version of the OpenAPI document: 1.38.0
|
* The version of the OpenAPI document: 1.38.2
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||||
|
2
web/src/api/open-api/common.ts
generated
2
web/src/api/open-api/common.ts
generated
@ -4,7 +4,7 @@
|
|||||||
* Immich
|
* Immich
|
||||||
* Immich API
|
* Immich API
|
||||||
*
|
*
|
||||||
* The version of the OpenAPI document: 1.38.0
|
* The version of the OpenAPI document: 1.38.2
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||||
|
2
web/src/api/open-api/configuration.ts
generated
2
web/src/api/open-api/configuration.ts
generated
@ -4,7 +4,7 @@
|
|||||||
* Immich
|
* Immich
|
||||||
* Immich API
|
* Immich API
|
||||||
*
|
*
|
||||||
* The version of the OpenAPI document: 1.38.0
|
* The version of the OpenAPI document: 1.38.2
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||||
|
2
web/src/api/open-api/index.ts
generated
2
web/src/api/open-api/index.ts
generated
@ -4,7 +4,7 @@
|
|||||||
* Immich
|
* Immich
|
||||||
* Immich API
|
* Immich API
|
||||||
*
|
*
|
||||||
* The version of the OpenAPI document: 1.38.0
|
* The version of the OpenAPI document: 1.38.2
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||||
|
@ -59,11 +59,11 @@ input:focus-visible {
|
|||||||
|
|
||||||
@layer utilities {
|
@layer utilities {
|
||||||
.immich-form-input {
|
.immich-form-input {
|
||||||
@apply bg-slate-100 p-2 rounded-md focus:border-immich-primary text-sm dark:bg-gray-600 dark:text-immich-dark-fg disabled:bg-gray-500 dark:disabled:bg-gray-900 disabled:cursor-not-allowed;
|
@apply bg-slate-200 p-2 rounded-lg focus:border-immich-primary text-sm dark:bg-gray-600 dark:text-immich-dark-fg disabled:bg-gray-400 dark:disabled:bg-gray-800 disabled:cursor-not-allowed disabled:text-gray-200;
|
||||||
}
|
}
|
||||||
|
|
||||||
.immich-form-label {
|
.immich-form-label {
|
||||||
@apply font-medium text-sm text-gray-500 dark:text-gray-300;
|
@apply font-medium text-gray-500 dark:text-gray-300;
|
||||||
}
|
}
|
||||||
|
|
||||||
.immich-btn-primary {
|
.immich-btn-primary {
|
||||||
|
@ -25,12 +25,12 @@
|
|||||||
const { data: configs } = await api.systemConfigApi.getConfig();
|
const { data: configs } = await api.systemConfigApi.getConfig();
|
||||||
|
|
||||||
const result = await api.systemConfigApi.updateConfig({
|
const result = await api.systemConfigApi.updateConfig({
|
||||||
ffmpeg: ffmpegConfig,
|
...configs,
|
||||||
oauth: configs.oauth
|
ffmpeg: ffmpegConfig
|
||||||
});
|
});
|
||||||
|
|
||||||
ffmpegConfig = result.data.ffmpeg;
|
ffmpegConfig = { ...result.data.ffmpeg };
|
||||||
savedConfig = result.data.ffmpeg;
|
savedConfig = { ...result.data.ffmpeg };
|
||||||
|
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
message: 'FFmpeg settings saved',
|
message: 'FFmpeg settings saved',
|
||||||
@ -48,8 +48,8 @@
|
|||||||
async function reset() {
|
async function reset() {
|
||||||
const { data: resetConfig } = await api.systemConfigApi.getConfig();
|
const { data: resetConfig } = await api.systemConfigApi.getConfig();
|
||||||
|
|
||||||
ffmpegConfig = resetConfig.ffmpeg;
|
ffmpegConfig = { ...resetConfig.ffmpeg };
|
||||||
savedConfig = resetConfig.ffmpeg;
|
savedConfig = { ...resetConfig.ffmpeg };
|
||||||
|
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
message: 'Reset FFmpeg settings to the recent saved settings',
|
message: 'Reset FFmpeg settings to the recent saved settings',
|
||||||
@ -60,8 +60,8 @@
|
|||||||
async function resetToDefault() {
|
async function resetToDefault() {
|
||||||
const { data: configs } = await api.systemConfigApi.getDefaults();
|
const { data: configs } = await api.systemConfigApi.getDefaults();
|
||||||
|
|
||||||
ffmpegConfig = configs.ffmpeg;
|
ffmpegConfig = { ...configs.ffmpeg };
|
||||||
defaultConfig = configs.ffmpeg;
|
defaultConfig = { ...configs.ffmpeg };
|
||||||
|
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
message: 'Reset FFmpeg settings to default',
|
message: 'Reset FFmpeg settings to default',
|
||||||
@ -74,52 +74,56 @@
|
|||||||
{#await getConfigs() then}
|
{#await getConfigs() then}
|
||||||
<div in:fade={{ duration: 500 }}>
|
<div in:fade={{ duration: 500 }}>
|
||||||
<form autocomplete="off" on:submit|preventDefault>
|
<form autocomplete="off" on:submit|preventDefault>
|
||||||
<SettingInputField
|
<div class="flex flex-col gap-4 ml-4 mt-4">
|
||||||
inputType={SettingInputFieldType.NUMBER}
|
<SettingInputField
|
||||||
label="CRF"
|
inputType={SettingInputFieldType.NUMBER}
|
||||||
bind:value={ffmpegConfig.crf}
|
label="CRF"
|
||||||
required={true}
|
bind:value={ffmpegConfig.crf}
|
||||||
isEdited={!(ffmpegConfig.crf == savedConfig.crf)}
|
required={true}
|
||||||
/>
|
isEdited={!(ffmpegConfig.crf == savedConfig.crf)}
|
||||||
|
/>
|
||||||
|
|
||||||
<SettingInputField
|
<SettingInputField
|
||||||
inputType={SettingInputFieldType.TEXT}
|
inputType={SettingInputFieldType.TEXT}
|
||||||
label="PRESET"
|
label="PRESET"
|
||||||
bind:value={ffmpegConfig.preset}
|
bind:value={ffmpegConfig.preset}
|
||||||
required={true}
|
required={true}
|
||||||
isEdited={!(ffmpegConfig.preset == savedConfig.preset)}
|
isEdited={!(ffmpegConfig.preset == savedConfig.preset)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SettingInputField
|
<SettingInputField
|
||||||
inputType={SettingInputFieldType.TEXT}
|
inputType={SettingInputFieldType.TEXT}
|
||||||
label="AUDIO CODEC"
|
label="AUDIO CODEC"
|
||||||
bind:value={ffmpegConfig.targetAudioCodec}
|
bind:value={ffmpegConfig.targetAudioCodec}
|
||||||
required={true}
|
required={true}
|
||||||
isEdited={!(ffmpegConfig.targetAudioCodec == savedConfig.targetAudioCodec)}
|
isEdited={!(ffmpegConfig.targetAudioCodec == savedConfig.targetAudioCodec)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SettingInputField
|
<SettingInputField
|
||||||
inputType={SettingInputFieldType.TEXT}
|
inputType={SettingInputFieldType.TEXT}
|
||||||
label="VIDEO CODEC"
|
label="VIDEO CODEC"
|
||||||
bind:value={ffmpegConfig.targetVideoCodec}
|
bind:value={ffmpegConfig.targetVideoCodec}
|
||||||
required={true}
|
required={true}
|
||||||
isEdited={!(ffmpegConfig.targetVideoCodec == savedConfig.targetVideoCodec)}
|
isEdited={!(ffmpegConfig.targetVideoCodec == savedConfig.targetVideoCodec)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SettingInputField
|
<SettingInputField
|
||||||
inputType={SettingInputFieldType.TEXT}
|
inputType={SettingInputFieldType.TEXT}
|
||||||
label="SCALING"
|
label="SCALING"
|
||||||
bind:value={ffmpegConfig.targetScaling}
|
bind:value={ffmpegConfig.targetScaling}
|
||||||
required={true}
|
required={true}
|
||||||
isEdited={!(ffmpegConfig.targetScaling == savedConfig.targetScaling)}
|
isEdited={!(ffmpegConfig.targetScaling == savedConfig.targetScaling)}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<SettingButtonsRow
|
<div class="ml-4">
|
||||||
on:reset={reset}
|
<SettingButtonsRow
|
||||||
on:save={saveSetting}
|
on:reset={reset}
|
||||||
on:reset-to-default={resetToDefault}
|
on:save={saveSetting}
|
||||||
showResetToDefault={!_.isEqual(savedConfig, defaultConfig)}
|
on:reset-to-default={resetToDefault}
|
||||||
/>
|
showResetToDefault={!_.isEqual(savedConfig, defaultConfig)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{/await}
|
{/await}
|
||||||
|
@ -25,8 +25,8 @@
|
|||||||
async function reset() {
|
async function reset() {
|
||||||
const { data: resetConfig } = await api.systemConfigApi.getConfig();
|
const { data: resetConfig } = await api.systemConfigApi.getConfig();
|
||||||
|
|
||||||
oauthConfig = resetConfig.oauth;
|
oauthConfig = { ...resetConfig.oauth };
|
||||||
savedConfig = resetConfig.oauth;
|
savedConfig = { ...resetConfig.oauth };
|
||||||
|
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
message: 'Reset OAuth settings to the last saved settings',
|
message: 'Reset OAuth settings to the last saved settings',
|
||||||
@ -39,12 +39,12 @@
|
|||||||
const { data: currentConfig } = await api.systemConfigApi.getConfig();
|
const { data: currentConfig } = await api.systemConfigApi.getConfig();
|
||||||
|
|
||||||
const result = await api.systemConfigApi.updateConfig({
|
const result = await api.systemConfigApi.updateConfig({
|
||||||
ffmpeg: currentConfig.ffmpeg,
|
...currentConfig,
|
||||||
oauth: oauthConfig
|
oauth: oauthConfig
|
||||||
});
|
});
|
||||||
|
|
||||||
oauthConfig = result.data.oauth;
|
oauthConfig = { ...result.data.oauth };
|
||||||
savedConfig = result.data.oauth;
|
savedConfig = { ...result.data.oauth };
|
||||||
|
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
message: 'OAuth settings saved',
|
message: 'OAuth settings saved',
|
||||||
@ -62,7 +62,7 @@
|
|||||||
async function resetToDefault() {
|
async function resetToDefault() {
|
||||||
const { data: defaultConfig } = await api.systemConfigApi.getDefaults();
|
const { data: defaultConfig } = await api.systemConfigApi.getDefaults();
|
||||||
|
|
||||||
oauthConfig = defaultConfig.oauth;
|
oauthConfig = { ...defaultConfig.oauth };
|
||||||
|
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
message: 'Reset OAuth settings to default',
|
message: 'Reset OAuth settings to default',
|
||||||
@ -80,51 +80,52 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class="m-4" />
|
<hr class="m-4" />
|
||||||
|
<div class="flex flex-col gap-4 ml-4">
|
||||||
|
<SettingInputField
|
||||||
|
inputType={SettingInputFieldType.TEXT}
|
||||||
|
label="ISSUER URL"
|
||||||
|
bind:value={oauthConfig.issuerUrl}
|
||||||
|
required={true}
|
||||||
|
disabled={!oauthConfig.enabled}
|
||||||
|
isEdited={!(oauthConfig.issuerUrl == savedConfig.issuerUrl)}
|
||||||
|
/>
|
||||||
|
|
||||||
<SettingInputField
|
<SettingInputField
|
||||||
inputType={SettingInputFieldType.TEXT}
|
inputType={SettingInputFieldType.TEXT}
|
||||||
label="ISSUER URL"
|
label="CLIENT ID"
|
||||||
bind:value={oauthConfig.issuerUrl}
|
bind:value={oauthConfig.clientId}
|
||||||
required={true}
|
required={true}
|
||||||
disabled={!oauthConfig.enabled}
|
disabled={!oauthConfig.enabled}
|
||||||
isEdited={!(oauthConfig.issuerUrl == savedConfig.issuerUrl)}
|
isEdited={!(oauthConfig.clientId == savedConfig.clientId)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SettingInputField
|
<SettingInputField
|
||||||
inputType={SettingInputFieldType.TEXT}
|
inputType={SettingInputFieldType.TEXT}
|
||||||
label="CLIENT ID"
|
label="CLIENT SECRET"
|
||||||
bind:value={oauthConfig.clientId}
|
bind:value={oauthConfig.clientSecret}
|
||||||
required={true}
|
required={true}
|
||||||
disabled={!oauthConfig.enabled}
|
disabled={!oauthConfig.enabled}
|
||||||
isEdited={!(oauthConfig.clientId == savedConfig.clientId)}
|
isEdited={!(oauthConfig.clientSecret == savedConfig.clientSecret)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SettingInputField
|
<SettingInputField
|
||||||
inputType={SettingInputFieldType.TEXT}
|
inputType={SettingInputFieldType.TEXT}
|
||||||
label="CLIENT SECRET"
|
label="SCOPE"
|
||||||
bind:value={oauthConfig.clientSecret}
|
bind:value={oauthConfig.scope}
|
||||||
required={true}
|
required={true}
|
||||||
disabled={!oauthConfig.enabled}
|
disabled={!oauthConfig.enabled}
|
||||||
isEdited={!(oauthConfig.clientSecret == savedConfig.clientSecret)}
|
isEdited={!(oauthConfig.scope == savedConfig.scope)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SettingInputField
|
<SettingInputField
|
||||||
inputType={SettingInputFieldType.TEXT}
|
inputType={SettingInputFieldType.TEXT}
|
||||||
label="SCOPE"
|
label="BUTTON TEXT"
|
||||||
bind:value={oauthConfig.scope}
|
bind:value={oauthConfig.buttonText}
|
||||||
required={true}
|
required={false}
|
||||||
disabled={!oauthConfig.enabled}
|
disabled={!oauthConfig.enabled}
|
||||||
isEdited={!(oauthConfig.scope == savedConfig.scope)}
|
isEdited={!(oauthConfig.buttonText == savedConfig.buttonText)}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
<SettingInputField
|
|
||||||
inputType={SettingInputFieldType.TEXT}
|
|
||||||
label="BUTTON TEXT"
|
|
||||||
bind:value={oauthConfig.buttonText}
|
|
||||||
required={false}
|
|
||||||
disabled={!oauthConfig.enabled}
|
|
||||||
isEdited={!(oauthConfig.buttonText == savedConfig.buttonText)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<SettingSwitch
|
<SettingSwitch
|
||||||
@ -135,12 +136,14 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SettingButtonsRow
|
<div class="ml-4">
|
||||||
on:reset={reset}
|
<SettingButtonsRow
|
||||||
on:save={saveSetting}
|
on:reset={reset}
|
||||||
on:reset-to-default={resetToDefault}
|
on:save={saveSetting}
|
||||||
showResetToDefault={!_.isEqual(savedConfig, defaultConfig)}
|
on:reset-to-default={resetToDefault}
|
||||||
/>
|
showResetToDefault={!_.isEqual(savedConfig, defaultConfig)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{/await}
|
{/await}
|
||||||
|
@ -6,11 +6,11 @@
|
|||||||
export let showResetToDefault = true;
|
export let showResetToDefault = true;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex justify-between gap-2 mx-4 mt-8">
|
<div class="flex justify-between gap-2 mt-8">
|
||||||
<div class="left">
|
<div class="left">
|
||||||
{#if showResetToDefault}
|
{#if showResetToDefault}
|
||||||
<button
|
<button
|
||||||
on:click|preventDefault={() => dispatch('reset-to-default')}
|
on:click={() => dispatch('reset-to-default')}
|
||||||
class="text-sm dark:text-immich-dark-primary hover:dark:text-immich-dark-primary/75 text-immich-primary hover:text-immich-primary/75 font-medium bg-none"
|
class="text-sm dark:text-immich-dark-primary hover:dark:text-immich-dark-primary/75 text-immich-primary hover:text-immich-primary/75 font-medium bg-none"
|
||||||
>
|
>
|
||||||
Reset to default
|
Reset to default
|
||||||
@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
<div class="right">
|
<div class="right">
|
||||||
<button
|
<button
|
||||||
on:click|preventDefault={() => dispatch('reset')}
|
on:click={() => dispatch('reset')}
|
||||||
class="text-sm bg-gray-500 dark:bg-gray-200 hover:bg-gray-500/75 dark:hover:bg-gray-200/80 px-4 py-2 text-white dark:text-immich-dark-gray rounded-full shadow-md font-medium disabled:opacity-50 disabled:cursor-not-allowed"
|
class="text-sm bg-gray-500 dark:bg-gray-200 hover:bg-gray-500/75 dark:hover:bg-gray-200/80 px-4 py-2 text-white dark:text-immich-dark-gray rounded-full shadow-md font-medium disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
>Reset
|
>Reset
|
||||||
</button>
|
</button>
|
||||||
|
@ -12,19 +12,19 @@
|
|||||||
|
|
||||||
export let inputType: SettingInputFieldType;
|
export let inputType: SettingInputFieldType;
|
||||||
export let value: string;
|
export let value: string;
|
||||||
export let label: string;
|
export let label = '';
|
||||||
export let required = false;
|
export let required = false;
|
||||||
export let disabled = false;
|
export let disabled = false;
|
||||||
export let isEdited: boolean;
|
export let isEdited = false;
|
||||||
|
|
||||||
const handleInput = (e: Event) => {
|
const handleInput = (e: Event) => {
|
||||||
value = (e.target as HTMLInputElement).value;
|
value = (e.target as HTMLInputElement).value;
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="m-4 flex flex-col gap-2">
|
<div class="w-full">
|
||||||
<div class="flex place-items-center gap-1">
|
<div class={`flex place-items-center gap-1 h-[26px]`}>
|
||||||
<label class="immich-form-label" for={label}>{label.toUpperCase()} </label>
|
<label class={`immich-form-label text-xs`} for={label}>{label.toUpperCase()} </label>
|
||||||
{#if required}
|
{#if required}
|
||||||
<div class="text-red-400">*</div>
|
<div class="text-red-400">*</div>
|
||||||
{/if}
|
{/if}
|
||||||
@ -32,14 +32,14 @@
|
|||||||
{#if isEdited}
|
{#if isEdited}
|
||||||
<div
|
<div
|
||||||
transition:fly={{ x: 10, duration: 200, easing: quintOut }}
|
transition:fly={{ x: 10, duration: 200, easing: quintOut }}
|
||||||
class="text-gray-500 text-xs italic"
|
class="bg-orange-100 px-2 rounded-full text-orange-900 text-[10px]"
|
||||||
>
|
>
|
||||||
Unsaved change
|
Unsaved change
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
class="immich-form-input"
|
class="immich-form-input w-full"
|
||||||
id={label}
|
id={label}
|
||||||
name={label}
|
name={label}
|
||||||
type={inputType}
|
type={inputType}
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
<div class="flex justify-between mx-4 place-items-center">
|
<div class="flex justify-between mx-4 place-items-center">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="immich-form-label">
|
<h2 class="immich-form-label text-sm">
|
||||||
{title.toUpperCase()}
|
{title.toUpperCase()}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
|
@ -0,0 +1,227 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {
|
||||||
|
api,
|
||||||
|
SystemConfigStorageTemplateDto,
|
||||||
|
SystemConfigTemplateStorageOptionDto,
|
||||||
|
UserResponseDto
|
||||||
|
} from '@api';
|
||||||
|
import * as luxon from 'luxon';
|
||||||
|
import handlebar from 'handlebars';
|
||||||
|
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
|
||||||
|
import { fade } from 'svelte/transition';
|
||||||
|
import SupportedDatetimePanel from './supported-datetime-panel.svelte';
|
||||||
|
import SupportedVariablesPanel from './supported-variables-panel.svelte';
|
||||||
|
import SettingButtonsRow from '../setting-buttons-row.svelte';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import {
|
||||||
|
notificationController,
|
||||||
|
NotificationType
|
||||||
|
} from '$lib/components/shared-components/notification/notification';
|
||||||
|
import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte';
|
||||||
|
|
||||||
|
export let storageConfig: SystemConfigStorageTemplateDto;
|
||||||
|
export let user: UserResponseDto;
|
||||||
|
|
||||||
|
let savedConfig: SystemConfigStorageTemplateDto;
|
||||||
|
let defaultConfig: SystemConfigStorageTemplateDto;
|
||||||
|
let templateOptions: SystemConfigTemplateStorageOptionDto;
|
||||||
|
let selectedPreset = '';
|
||||||
|
|
||||||
|
async function getConfigs() {
|
||||||
|
[savedConfig, defaultConfig, templateOptions] = await Promise.all([
|
||||||
|
api.systemConfigApi.getConfig().then((res) => res.data.storageTemplate),
|
||||||
|
api.systemConfigApi.getDefaults().then((res) => res.data.storageTemplate),
|
||||||
|
api.systemConfigApi.getStorageTemplateOptions().then((res) => res.data)
|
||||||
|
]);
|
||||||
|
|
||||||
|
selectedPreset = templateOptions.presetOptions[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSupportDateTimeFormat = async () => {
|
||||||
|
const { data } = await api.systemConfigApi.getStorageTemplateOptions();
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
$: parsedTemplate = () => {
|
||||||
|
try {
|
||||||
|
return renderTemplate(storageConfig.template);
|
||||||
|
} catch (error) {
|
||||||
|
return 'error';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderTemplate = (templateString: string) => {
|
||||||
|
const template = handlebar.compile(templateString, {
|
||||||
|
knownHelpers: undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
const substitutions: Record<string, string> = {
|
||||||
|
filename: 'IMG_10041123',
|
||||||
|
ext: 'jpeg'
|
||||||
|
};
|
||||||
|
|
||||||
|
const dt = luxon.DateTime.fromISO(new Date('2022-09-04T20:03:05.250').toISOString());
|
||||||
|
|
||||||
|
const dateTokens = [
|
||||||
|
...templateOptions.yearOptions,
|
||||||
|
...templateOptions.monthOptions,
|
||||||
|
...templateOptions.dayOptions,
|
||||||
|
...templateOptions.hourOptions,
|
||||||
|
...templateOptions.minuteOptions,
|
||||||
|
...templateOptions.secondOptions
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const token of dateTokens) {
|
||||||
|
substitutions[token] = dt.toFormat(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
return template(substitutions);
|
||||||
|
};
|
||||||
|
|
||||||
|
async function reset() {
|
||||||
|
const { data: resetConfig } = await api.systemConfigApi.getConfig();
|
||||||
|
|
||||||
|
storageConfig.template = resetConfig.storageTemplate.template;
|
||||||
|
savedConfig.template = resetConfig.storageTemplate.template;
|
||||||
|
|
||||||
|
notificationController.show({
|
||||||
|
message: 'Reset storage template settings to the recent saved settings',
|
||||||
|
type: NotificationType.Info
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveSetting() {
|
||||||
|
try {
|
||||||
|
const { data: currentConfig } = await api.systemConfigApi.getConfig();
|
||||||
|
|
||||||
|
const result = await api.systemConfigApi.updateConfig({
|
||||||
|
...currentConfig,
|
||||||
|
storageTemplate: storageConfig
|
||||||
|
});
|
||||||
|
|
||||||
|
storageConfig.template = result.data.storageTemplate.template;
|
||||||
|
savedConfig.template = result.data.storageTemplate.template;
|
||||||
|
|
||||||
|
notificationController.show({
|
||||||
|
message: 'Storage template saved',
|
||||||
|
type: NotificationType.Info
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error [storage-template-settings] [saveSetting]', e);
|
||||||
|
notificationController.show({
|
||||||
|
message: 'Unable to save settings',
|
||||||
|
type: NotificationType.Error
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resetToDefault() {
|
||||||
|
const { data: defaultConfig } = await api.systemConfigApi.getDefaults();
|
||||||
|
|
||||||
|
storageConfig.template = defaultConfig.storageTemplate.template;
|
||||||
|
|
||||||
|
notificationController.show({
|
||||||
|
message: 'Reset storage template to default',
|
||||||
|
type: NotificationType.Info
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePresetSelection = () => {
|
||||||
|
storageConfig.template = selectedPreset;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section class="dark:text-immich-dark-fg">
|
||||||
|
{#await getConfigs() then}
|
||||||
|
<div id="directory-path-builder" class="m-4">
|
||||||
|
<h3 class="font-medium text-immich-primary dark:text-immich-dark-primary text-base">
|
||||||
|
Variables
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<section class="support-date">
|
||||||
|
{#await getSupportDateTimeFormat()}
|
||||||
|
<LoadingSpinner />
|
||||||
|
{:then options}
|
||||||
|
<div transition:fade={{ duration: 200 }}>
|
||||||
|
<SupportedDatetimePanel {options} />
|
||||||
|
</div>
|
||||||
|
{/await}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="support-date">
|
||||||
|
<SupportedVariablesPanel />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="mt-4 flex flex-col">
|
||||||
|
<h3 class="font-medium text-immich-primary dark:text-immich-dark-primary text-base">
|
||||||
|
Template
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div class="text-xs my-2">
|
||||||
|
<h4>PREVIEW</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-xs">
|
||||||
|
Approximately path length limit : <span
|
||||||
|
class="font-semibold text-immich-primary dark:text-immich-dark-primary"
|
||||||
|
>{parsedTemplate().length + user.id.length + 'UPLOAD_LOCATION'.length}</span
|
||||||
|
>/260
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="text-xs">
|
||||||
|
{user.id} is the user's ID
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p
|
||||||
|
class="text-xs p-4 bg-gray-200 dark:bg-gray-700 dark:text-immich-dark-fg py-2 rounded-lg mt-2"
|
||||||
|
>
|
||||||
|
<span class="text-immich-fg/25 dark:text-immich-dark-fg/50"
|
||||||
|
>UPLOAD_LOCATION/{user.id}</span
|
||||||
|
>/{parsedTemplate()}.jpeg
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<form autocomplete="off" class="flex flex-col" on:submit|preventDefault>
|
||||||
|
<div class="flex flex-col my-2">
|
||||||
|
<label class="text-xs" for="presets">PRESET</label>
|
||||||
|
<select
|
||||||
|
class="text-sm bg-slate-200 p-2 rounded-lg mt-2 dark:bg-gray-600 hover:cursor-pointer"
|
||||||
|
name="presets"
|
||||||
|
id="preset-select"
|
||||||
|
bind:value={selectedPreset}
|
||||||
|
on:change={handlePresetSelection}
|
||||||
|
>
|
||||||
|
{#each templateOptions.presetOptions as preset}
|
||||||
|
<option value={preset}>{renderTemplate(preset)}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2 align-bottom">
|
||||||
|
<SettingInputField
|
||||||
|
label="template"
|
||||||
|
required
|
||||||
|
inputType={SettingInputFieldType.TEXT}
|
||||||
|
bind:value={storageConfig.template}
|
||||||
|
isEdited={!(storageConfig.template === savedConfig.template)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="flex-0">
|
||||||
|
<SettingInputField
|
||||||
|
label="Extension"
|
||||||
|
inputType={SettingInputFieldType.TEXT}
|
||||||
|
value={'.jpeg'}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SettingButtonsRow
|
||||||
|
on:reset={reset}
|
||||||
|
on:save={saveSetting}
|
||||||
|
on:reset-to-default={resetToDefault}
|
||||||
|
showResetToDefault={!_.isEqual(savedConfig, defaultConfig)}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/await}
|
||||||
|
</section>
|
@ -0,0 +1,78 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { SystemConfigTemplateStorageOptionDto } from '@api';
|
||||||
|
import * as luxon from 'luxon';
|
||||||
|
|
||||||
|
export let options: SystemConfigTemplateStorageOptionDto;
|
||||||
|
|
||||||
|
const getLuxonExample = (format: string) => {
|
||||||
|
return luxon.DateTime.fromISO(new Date('2022-09-04T20:03:05.250').toISOString()).toFormat(
|
||||||
|
format
|
||||||
|
);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="text-xs mt-2">
|
||||||
|
<h4>DATE & TIME</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-xs bg-gray-200 dark:bg-gray-700 dark:text-immich-dark-fg p-4 mt-2 rounded-lg">
|
||||||
|
<div class="mb-2 text-gray-600 dark:text-immich-dark-fg">
|
||||||
|
<p>Asset's creation timestamp is used for the datetime information</p>
|
||||||
|
<p>Sample time 2022-09-04T20:03:05.250</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-[50px]">
|
||||||
|
<div>
|
||||||
|
<p class="text-immich-primary font-medium dark:text-immich-dark-primary">YEAR</p>
|
||||||
|
<ul>
|
||||||
|
{#each options.yearOptions as yearFormat}
|
||||||
|
<li>{'{{'}{yearFormat}{'}}'} - {getLuxonExample(yearFormat)}</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p class="text-immich-primary font-medium dark:text-immich-dark-primary">MONTH</p>
|
||||||
|
<ul>
|
||||||
|
{#each options.monthOptions as monthFormat}
|
||||||
|
<li>{'{{'}{monthFormat}{'}}'} - {getLuxonExample(monthFormat)}</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p class="text-immich-primary font-medium dark:text-immich-dark-primary">DAY</p>
|
||||||
|
<ul>
|
||||||
|
{#each options.dayOptions as dayFormat}
|
||||||
|
<li>{'{{'}{dayFormat}{'}}'} - {getLuxonExample(dayFormat)}</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p class="text-immich-primary font-medium dark:text-immich-dark-primary">HOUR</p>
|
||||||
|
<ul>
|
||||||
|
{#each options.hourOptions as dayFormat}
|
||||||
|
<li>{'{{'}{dayFormat}{'}}'} - {getLuxonExample(dayFormat)}</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p class="text-immich-primary font-medium dark:text-immich-dark-primary">MINUTE</p>
|
||||||
|
<ul>
|
||||||
|
{#each options.minuteOptions as dayFormat}
|
||||||
|
<li>{'{{'}{dayFormat}{'}}'} - {getLuxonExample(dayFormat)}</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p class="text-immich-primary font-medium dark:text-immich-dark-primary">SECOND</p>
|
||||||
|
<ul>
|
||||||
|
{#each options.secondOptions as dayFormat}
|
||||||
|
<li>{'{{'}{dayFormat}{'}}'} - {getLuxonExample(dayFormat)}</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,21 @@
|
|||||||
|
<div class="text-xs mt-4">
|
||||||
|
<h4>OTHER VARIABLES</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-xs bg-gray-200 dark:bg-gray-700 dark:text-immich-dark-fg p-4 mt-2 rounded-lg">
|
||||||
|
<div class="flex gap-[50px]">
|
||||||
|
<div>
|
||||||
|
<p class="text-immich-primary font-medium dark:text-immich-dark-primary">FILE NAME</p>
|
||||||
|
<ul>
|
||||||
|
<li>{`{{filename}}`}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p class="text-immich-primary font-medium dark:text-immich-dark-primary">FILE EXTENSION</p>
|
||||||
|
<ul>
|
||||||
|
<li>{`{{ext}}`}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -2,11 +2,13 @@
|
|||||||
import FFmpegSettings from '$lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte';
|
import FFmpegSettings from '$lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte';
|
||||||
import OAuthSettings from '$lib/components/admin-page/settings/oauth/oauth-settings.svelte';
|
import OAuthSettings from '$lib/components/admin-page/settings/oauth/oauth-settings.svelte';
|
||||||
import SettingAccordion from '$lib/components/admin-page/settings/setting-accordion.svelte';
|
import SettingAccordion from '$lib/components/admin-page/settings/setting-accordion.svelte';
|
||||||
|
import StorageTemplateSettings from '$lib/components/admin-page/settings/storate-template/storage-template-settings.svelte';
|
||||||
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
|
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
|
||||||
import { api, SystemConfigDto } from '@api';
|
import { api, SystemConfigDto } from '@api';
|
||||||
|
import type { PageData } from './$types';
|
||||||
|
|
||||||
let systemConfig: SystemConfigDto;
|
let systemConfig: SystemConfigDto;
|
||||||
|
export let data: PageData;
|
||||||
const getConfig = async () => {
|
const getConfig = async () => {
|
||||||
const { data } = await api.systemConfigApi.getConfig();
|
const { data } = await api.systemConfigApi.getConfig();
|
||||||
systemConfig = data;
|
systemConfig = data;
|
||||||
@ -33,5 +35,12 @@
|
|||||||
<SettingAccordion title="OAuth Settings" subtitle="Manage the OAuth integration to Immich app">
|
<SettingAccordion title="OAuth Settings" subtitle="Manage the OAuth integration to Immich app">
|
||||||
<OAuthSettings oauthConfig={configs.oauth} />
|
<OAuthSettings oauthConfig={configs.oauth} />
|
||||||
</SettingAccordion>
|
</SettingAccordion>
|
||||||
|
|
||||||
|
<SettingAccordion
|
||||||
|
title="Storage Template"
|
||||||
|
subtitle="Manage the folder structure and file name of the upload asset"
|
||||||
|
>
|
||||||
|
<StorageTemplateSettings storageConfig={configs.storageTemplate} user={data.user} />
|
||||||
|
</SettingAccordion>
|
||||||
{/await}
|
{/await}
|
||||||
</section>
|
</section>
|
||||||
|
@ -22,7 +22,6 @@
|
|||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
allUsers = $page.data.allUsers;
|
allUsers = $page.data.allUsers;
|
||||||
console.log('getting all users', allUsers);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const isDeleted = (user: UserResponseDto): boolean => {
|
const isDeleted = (user: UserResponseDto): boolean => {
|
||||||
|
Loading…
Reference in New Issue
Block a user