mirror of
https://github.com/immich-app/immich.git
synced 2024-11-15 09:59:00 -07:00
parent
15c1cd6449
commit
8a445cac07
8
.github/workflows/docker.yml
vendored
8
.github/workflows/docker.yml
vendored
@ -124,7 +124,11 @@ jobs:
|
||||
push: ${{ !github.event.pull_request.head.repo.fork }}
|
||||
cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/immich-build-cache:${{matrix.image}}
|
||||
cache-to: ${{ steps.cache-target.outputs.cache-to }}
|
||||
build-args: |
|
||||
DEVICE=${{ matrix.device }}
|
||||
tags: ${{ steps.metadata.outputs.tags }}
|
||||
labels: ${{ steps.metadata.outputs.labels }}
|
||||
build-args: |
|
||||
DEVICE=${{ matrix.device }}
|
||||
BUILD_ID=${{ github.run_id }}
|
||||
BUILD_IMAGE=${{ github.event_name == 'release' && github.ref_name || steps.metadata.outputs.tags }}
|
||||
BUILD_SOURCE_REF=${{ github.ref_name }}
|
||||
BUILD_SOURCE_COMMIT=${{ github.sha }}
|
||||
|
@ -26,6 +26,16 @@ services:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
IMMICH_REPOSITORY: immich-app/immich
|
||||
IMMICH_REPOSITORY_URL: https://github.com/immich-app/immich
|
||||
IMMICH_SOURCE_REF: local
|
||||
IMMICH_SOURCE_COMMIT: af2efbdbbddc27cd06142f22253ccbbbbeec1f55
|
||||
IMMICH_SOURCE_URL: https://github.com/immich-app/immich/commit/af2efbdbbddc27cd06142f22253ccbbbbeec1f55
|
||||
IMMICH_BUILD: '9654404849'
|
||||
IMMICH_BUILD_URL: https://github.com/immich-app/immich/actions/runs/9654404849
|
||||
IMMICH_BUILD_IMAGE: development
|
||||
IMMICH_BUILD_IMAGE_URL: https://github.com/immich-app/immich/pkgs/container/immich-server
|
||||
ulimits:
|
||||
nofile:
|
||||
soft: 1048576
|
||||
@ -107,7 +117,22 @@ services:
|
||||
interval: 5m
|
||||
start_interval: 30s
|
||||
start_period: 5m
|
||||
command: ["postgres", "-c" ,"shared_preload_libraries=vectors.so", "-c", 'search_path="$$user", public, vectors', "-c", "logging_collector=on", "-c", "max_wal_size=2GB", "-c", "shared_buffers=512MB", "-c", "wal_compression=on"]
|
||||
command:
|
||||
[
|
||||
'postgres',
|
||||
'-c',
|
||||
'shared_preload_libraries=vectors.so',
|
||||
'-c',
|
||||
'search_path="$$user", public, vectors',
|
||||
'-c',
|
||||
'logging_collector=on',
|
||||
'-c',
|
||||
'max_wal_size=2GB',
|
||||
'-c',
|
||||
'shared_buffers=512MB',
|
||||
'-c',
|
||||
'wal_compression=on',
|
||||
]
|
||||
|
||||
# set IMMICH_METRICS=true in .env to enable metrics
|
||||
# immich-prometheus:
|
||||
|
@ -10,6 +10,11 @@ services:
|
||||
build:
|
||||
context: ../
|
||||
dockerfile: server/Dockerfile
|
||||
args:
|
||||
- BUILD_ID=1234567890
|
||||
- BUILD_IMAGE=e2e
|
||||
- BUILD_SOURCE_REF=e2e
|
||||
- BUILD_SOURCE_COMMIT=e2eeeeeeeeeeeeeeeeee
|
||||
environment:
|
||||
- DB_HOSTNAME=database
|
||||
- DB_USERNAME=postgres
|
||||
|
@ -15,6 +15,39 @@ describe('/server-info', () => {
|
||||
nonAdmin = await utils.userSetup(admin.accessToken, createUserDto.user1);
|
||||
});
|
||||
|
||||
describe('GET /server-info/about', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).get('/server-info/about');
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should return about information', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.get('/server-info/about')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual({
|
||||
version: expect.any(String),
|
||||
versionUrl: expect.any(String),
|
||||
repository: 'immich-app/immich',
|
||||
repositoryUrl: 'https://github.com/immich-app/immich',
|
||||
build: '1234567890',
|
||||
buildUrl: 'https://github.com/immich-app/immich/actions/runs/1234567890',
|
||||
buildImage: 'e2e',
|
||||
buildImageUrl: 'https://github.com/immich-app/immich/pkgs/container/immich-server',
|
||||
sourceRef: 'e2e',
|
||||
sourceCommit: 'e2eeeeeeeeeeeeeeeeee',
|
||||
sourceUrl: 'https://github.com/immich-app/immich/commit/e2eeeeeeeeeeeeeeeeee',
|
||||
nodejs: expect.any(String),
|
||||
ffmpeg: expect.any(String),
|
||||
imagemagick: expect.any(String),
|
||||
libvips: expect.any(String),
|
||||
exiftool: expect.any(String),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /server-info/storage', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).get('/server-info/storage');
|
||||
|
2
mobile/openapi/README.md
generated
2
mobile/openapi/README.md
generated
@ -171,6 +171,7 @@ Class | Method | HTTP request | Description
|
||||
*SearchApi* | [**searchPerson**](doc//SearchApi.md#searchperson) | **GET** /search/person |
|
||||
*SearchApi* | [**searchPlaces**](doc//SearchApi.md#searchplaces) | **GET** /search/places |
|
||||
*SearchApi* | [**searchSmart**](doc//SearchApi.md#searchsmart) | **POST** /search/smart |
|
||||
*ServerInfoApi* | [**getAboutInfo**](doc//ServerInfoApi.md#getaboutinfo) | **GET** /server-info/about |
|
||||
*ServerInfoApi* | [**getServerConfig**](doc//ServerInfoApi.md#getserverconfig) | **GET** /server-info/config |
|
||||
*ServerInfoApi* | [**getServerFeatures**](doc//ServerInfoApi.md#getserverfeatures) | **GET** /server-info/features |
|
||||
*ServerInfoApi* | [**getServerStatistics**](doc//ServerInfoApi.md#getserverstatistics) | **GET** /server-info/statistics |
|
||||
@ -360,6 +361,7 @@ Class | Method | HTTP request | Description
|
||||
- [SearchFacetResponseDto](doc//SearchFacetResponseDto.md)
|
||||
- [SearchResponseDto](doc//SearchResponseDto.md)
|
||||
- [SearchSuggestionType](doc//SearchSuggestionType.md)
|
||||
- [ServerAboutResponseDto](doc//ServerAboutResponseDto.md)
|
||||
- [ServerConfigDto](doc//ServerConfigDto.md)
|
||||
- [ServerFeaturesDto](doc//ServerFeaturesDto.md)
|
||||
- [ServerMediaTypesResponseDto](doc//ServerMediaTypesResponseDto.md)
|
||||
|
1
mobile/openapi/lib/api.dart
generated
1
mobile/openapi/lib/api.dart
generated
@ -187,6 +187,7 @@ part 'model/search_facet_count_response_dto.dart';
|
||||
part 'model/search_facet_response_dto.dart';
|
||||
part 'model/search_response_dto.dart';
|
||||
part 'model/search_suggestion_type.dart';
|
||||
part 'model/server_about_response_dto.dart';
|
||||
part 'model/server_config_dto.dart';
|
||||
part 'model/server_features_dto.dart';
|
||||
part 'model/server_media_types_response_dto.dart';
|
||||
|
41
mobile/openapi/lib/api/server_info_api.dart
generated
41
mobile/openapi/lib/api/server_info_api.dart
generated
@ -16,6 +16,47 @@ class ServerInfoApi {
|
||||
|
||||
final ApiClient apiClient;
|
||||
|
||||
/// Performs an HTTP 'GET /server-info/about' operation and returns the [Response].
|
||||
Future<Response> getAboutInfoWithHttpInfo() async {
|
||||
// ignore: prefer_const_declarations
|
||||
final path = r'/server-info/about';
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
const contentTypes = <String>[];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
path,
|
||||
'GET',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
Future<ServerAboutResponseDto?> getAboutInfo() async {
|
||||
final response = await getAboutInfoWithHttpInfo();
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||
// FormatException when trying to decode an empty string.
|
||||
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'ServerAboutResponseDto',) as ServerAboutResponseDto;
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'GET /server-info/config' operation and returns the [Response].
|
||||
Future<Response> getServerConfigWithHttpInfo() async {
|
||||
// ignore: prefer_const_declarations
|
||||
|
2
mobile/openapi/lib/api_client.dart
generated
2
mobile/openapi/lib/api_client.dart
generated
@ -436,6 +436,8 @@ class ApiClient {
|
||||
return SearchResponseDto.fromJson(value);
|
||||
case 'SearchSuggestionType':
|
||||
return SearchSuggestionTypeTypeTransformer().decode(value);
|
||||
case 'ServerAboutResponseDto':
|
||||
return ServerAboutResponseDto.fromJson(value);
|
||||
case 'ServerConfigDto':
|
||||
return ServerConfigDto.fromJson(value);
|
||||
case 'ServerFeaturesDto':
|
||||
|
344
mobile/openapi/lib/model/server_about_response_dto.dart
generated
Normal file
344
mobile/openapi/lib/model/server_about_response_dto.dart
generated
Normal file
@ -0,0 +1,344 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class ServerAboutResponseDto {
|
||||
/// Returns a new [ServerAboutResponseDto] instance.
|
||||
ServerAboutResponseDto({
|
||||
this.build,
|
||||
this.buildImage,
|
||||
this.buildImageUrl,
|
||||
this.buildUrl,
|
||||
this.exiftool,
|
||||
this.ffmpeg,
|
||||
this.imagemagick,
|
||||
this.libvips,
|
||||
this.nodejs,
|
||||
this.repository,
|
||||
this.repositoryUrl,
|
||||
this.sourceCommit,
|
||||
this.sourceRef,
|
||||
this.sourceUrl,
|
||||
required this.version,
|
||||
required this.versionUrl,
|
||||
});
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
String? build;
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
String? buildImage;
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
String? buildImageUrl;
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
String? buildUrl;
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
String? exiftool;
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
String? ffmpeg;
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
String? imagemagick;
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
String? libvips;
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
String? nodejs;
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
String? repository;
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
String? repositoryUrl;
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
String? sourceCommit;
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
String? sourceRef;
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
String? sourceUrl;
|
||||
|
||||
String version;
|
||||
|
||||
String versionUrl;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is ServerAboutResponseDto &&
|
||||
other.build == build &&
|
||||
other.buildImage == buildImage &&
|
||||
other.buildImageUrl == buildImageUrl &&
|
||||
other.buildUrl == buildUrl &&
|
||||
other.exiftool == exiftool &&
|
||||
other.ffmpeg == ffmpeg &&
|
||||
other.imagemagick == imagemagick &&
|
||||
other.libvips == libvips &&
|
||||
other.nodejs == nodejs &&
|
||||
other.repository == repository &&
|
||||
other.repositoryUrl == repositoryUrl &&
|
||||
other.sourceCommit == sourceCommit &&
|
||||
other.sourceRef == sourceRef &&
|
||||
other.sourceUrl == sourceUrl &&
|
||||
other.version == version &&
|
||||
other.versionUrl == versionUrl;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(build == null ? 0 : build!.hashCode) +
|
||||
(buildImage == null ? 0 : buildImage!.hashCode) +
|
||||
(buildImageUrl == null ? 0 : buildImageUrl!.hashCode) +
|
||||
(buildUrl == null ? 0 : buildUrl!.hashCode) +
|
||||
(exiftool == null ? 0 : exiftool!.hashCode) +
|
||||
(ffmpeg == null ? 0 : ffmpeg!.hashCode) +
|
||||
(imagemagick == null ? 0 : imagemagick!.hashCode) +
|
||||
(libvips == null ? 0 : libvips!.hashCode) +
|
||||
(nodejs == null ? 0 : nodejs!.hashCode) +
|
||||
(repository == null ? 0 : repository!.hashCode) +
|
||||
(repositoryUrl == null ? 0 : repositoryUrl!.hashCode) +
|
||||
(sourceCommit == null ? 0 : sourceCommit!.hashCode) +
|
||||
(sourceRef == null ? 0 : sourceRef!.hashCode) +
|
||||
(sourceUrl == null ? 0 : sourceUrl!.hashCode) +
|
||||
(version.hashCode) +
|
||||
(versionUrl.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'ServerAboutResponseDto[build=$build, buildImage=$buildImage, buildImageUrl=$buildImageUrl, buildUrl=$buildUrl, exiftool=$exiftool, ffmpeg=$ffmpeg, imagemagick=$imagemagick, libvips=$libvips, nodejs=$nodejs, repository=$repository, repositoryUrl=$repositoryUrl, sourceCommit=$sourceCommit, sourceRef=$sourceRef, sourceUrl=$sourceUrl, version=$version, versionUrl=$versionUrl]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
if (this.build != null) {
|
||||
json[r'build'] = this.build;
|
||||
} else {
|
||||
// json[r'build'] = null;
|
||||
}
|
||||
if (this.buildImage != null) {
|
||||
json[r'buildImage'] = this.buildImage;
|
||||
} else {
|
||||
// json[r'buildImage'] = null;
|
||||
}
|
||||
if (this.buildImageUrl != null) {
|
||||
json[r'buildImageUrl'] = this.buildImageUrl;
|
||||
} else {
|
||||
// json[r'buildImageUrl'] = null;
|
||||
}
|
||||
if (this.buildUrl != null) {
|
||||
json[r'buildUrl'] = this.buildUrl;
|
||||
} else {
|
||||
// json[r'buildUrl'] = null;
|
||||
}
|
||||
if (this.exiftool != null) {
|
||||
json[r'exiftool'] = this.exiftool;
|
||||
} else {
|
||||
// json[r'exiftool'] = null;
|
||||
}
|
||||
if (this.ffmpeg != null) {
|
||||
json[r'ffmpeg'] = this.ffmpeg;
|
||||
} else {
|
||||
// json[r'ffmpeg'] = null;
|
||||
}
|
||||
if (this.imagemagick != null) {
|
||||
json[r'imagemagick'] = this.imagemagick;
|
||||
} else {
|
||||
// json[r'imagemagick'] = null;
|
||||
}
|
||||
if (this.libvips != null) {
|
||||
json[r'libvips'] = this.libvips;
|
||||
} else {
|
||||
// json[r'libvips'] = null;
|
||||
}
|
||||
if (this.nodejs != null) {
|
||||
json[r'nodejs'] = this.nodejs;
|
||||
} else {
|
||||
// json[r'nodejs'] = null;
|
||||
}
|
||||
if (this.repository != null) {
|
||||
json[r'repository'] = this.repository;
|
||||
} else {
|
||||
// json[r'repository'] = null;
|
||||
}
|
||||
if (this.repositoryUrl != null) {
|
||||
json[r'repositoryUrl'] = this.repositoryUrl;
|
||||
} else {
|
||||
// json[r'repositoryUrl'] = null;
|
||||
}
|
||||
if (this.sourceCommit != null) {
|
||||
json[r'sourceCommit'] = this.sourceCommit;
|
||||
} else {
|
||||
// json[r'sourceCommit'] = null;
|
||||
}
|
||||
if (this.sourceRef != null) {
|
||||
json[r'sourceRef'] = this.sourceRef;
|
||||
} else {
|
||||
// json[r'sourceRef'] = null;
|
||||
}
|
||||
if (this.sourceUrl != null) {
|
||||
json[r'sourceUrl'] = this.sourceUrl;
|
||||
} else {
|
||||
// json[r'sourceUrl'] = null;
|
||||
}
|
||||
json[r'version'] = this.version;
|
||||
json[r'versionUrl'] = this.versionUrl;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [ServerAboutResponseDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static ServerAboutResponseDto? fromJson(dynamic value) {
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return ServerAboutResponseDto(
|
||||
build: mapValueOfType<String>(json, r'build'),
|
||||
buildImage: mapValueOfType<String>(json, r'buildImage'),
|
||||
buildImageUrl: mapValueOfType<String>(json, r'buildImageUrl'),
|
||||
buildUrl: mapValueOfType<String>(json, r'buildUrl'),
|
||||
exiftool: mapValueOfType<String>(json, r'exiftool'),
|
||||
ffmpeg: mapValueOfType<String>(json, r'ffmpeg'),
|
||||
imagemagick: mapValueOfType<String>(json, r'imagemagick'),
|
||||
libvips: mapValueOfType<String>(json, r'libvips'),
|
||||
nodejs: mapValueOfType<String>(json, r'nodejs'),
|
||||
repository: mapValueOfType<String>(json, r'repository'),
|
||||
repositoryUrl: mapValueOfType<String>(json, r'repositoryUrl'),
|
||||
sourceCommit: mapValueOfType<String>(json, r'sourceCommit'),
|
||||
sourceRef: mapValueOfType<String>(json, r'sourceRef'),
|
||||
sourceUrl: mapValueOfType<String>(json, r'sourceUrl'),
|
||||
version: mapValueOfType<String>(json, r'version')!,
|
||||
versionUrl: mapValueOfType<String>(json, r'versionUrl')!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<ServerAboutResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <ServerAboutResponseDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = ServerAboutResponseDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, ServerAboutResponseDto> mapFromJson(dynamic json) {
|
||||
final map = <String, ServerAboutResponseDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = ServerAboutResponseDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of ServerAboutResponseDto-objects as value to a dart map
|
||||
static Map<String, List<ServerAboutResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<ServerAboutResponseDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = ServerAboutResponseDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'version',
|
||||
'versionUrl',
|
||||
};
|
||||
}
|
||||
|
@ -4718,6 +4718,38 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/server-info/about": {
|
||||
"get": {
|
||||
"operationId": "getAboutInfo",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ServerAboutResponseDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Server Info"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/server-info/config": {
|
||||
"get": {
|
||||
"operationId": "getServerConfig",
|
||||
@ -9630,6 +9662,63 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ServerAboutResponseDto": {
|
||||
"properties": {
|
||||
"build": {
|
||||
"type": "string"
|
||||
},
|
||||
"buildImage": {
|
||||
"type": "string"
|
||||
},
|
||||
"buildImageUrl": {
|
||||
"type": "string"
|
||||
},
|
||||
"buildUrl": {
|
||||
"type": "string"
|
||||
},
|
||||
"exiftool": {
|
||||
"type": "string"
|
||||
},
|
||||
"ffmpeg": {
|
||||
"type": "string"
|
||||
},
|
||||
"imagemagick": {
|
||||
"type": "string"
|
||||
},
|
||||
"libvips": {
|
||||
"type": "string"
|
||||
},
|
||||
"nodejs": {
|
||||
"type": "string"
|
||||
},
|
||||
"repository": {
|
||||
"type": "string"
|
||||
},
|
||||
"repositoryUrl": {
|
||||
"type": "string"
|
||||
},
|
||||
"sourceCommit": {
|
||||
"type": "string"
|
||||
},
|
||||
"sourceRef": {
|
||||
"type": "string"
|
||||
},
|
||||
"sourceUrl": {
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
},
|
||||
"versionUrl": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"version",
|
||||
"versionUrl"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ServerConfigDto": {
|
||||
"properties": {
|
||||
"externalDomain": {
|
||||
|
@ -787,6 +787,24 @@ export type SmartSearchDto = {
|
||||
withDeleted?: boolean;
|
||||
withExif?: boolean;
|
||||
};
|
||||
export type ServerAboutResponseDto = {
|
||||
build?: string;
|
||||
buildImage?: string;
|
||||
buildImageUrl?: string;
|
||||
buildUrl?: string;
|
||||
exiftool?: string;
|
||||
ffmpeg?: string;
|
||||
imagemagick?: string;
|
||||
libvips?: string;
|
||||
nodejs?: string;
|
||||
repository?: string;
|
||||
repositoryUrl?: string;
|
||||
sourceCommit?: string;
|
||||
sourceRef?: string;
|
||||
sourceUrl?: string;
|
||||
version: string;
|
||||
versionUrl: string;
|
||||
};
|
||||
export type ServerConfigDto = {
|
||||
externalDomain: string;
|
||||
isInitialized: boolean;
|
||||
@ -2363,6 +2381,14 @@ export function getSearchSuggestions({ country, make, model, state, $type }: {
|
||||
...opts
|
||||
}));
|
||||
}
|
||||
export function getAboutInfo(opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: ServerAboutResponseDto;
|
||||
}>("/server-info/about", {
|
||||
...opts
|
||||
}));
|
||||
}
|
||||
export function getServerConfig(opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
|
@ -59,6 +59,22 @@ RUN npm link && npm install -g @immich/cli && npm cache clean --force
|
||||
COPY LICENSE /licenses/LICENSE.txt
|
||||
COPY LICENSE /LICENSE
|
||||
ENV PATH="${PATH}:/usr/src/app/bin"
|
||||
|
||||
ARG BUILD_ID
|
||||
ARG BUILD_IMAGE
|
||||
ARG BUILD_SOURCE_REF
|
||||
ARG BUILD_SOURCE_COMMIT
|
||||
|
||||
ENV IMMICH_BUILD=${BUILD_ID}
|
||||
ENV IMMICH_BUILD_URL=https://github.com/immich-app/immich/actions/runs/${BUILD_ID}
|
||||
ENV IMMICH_BUILD_IMAGE=${BUILD_IMAGE}
|
||||
ENV IMMICH_BUILD_IMAGE_URL=https://github.com/immich-app/immich/pkgs/container/immich-server
|
||||
ENV IMMICH_REPOSITORY=immich-app/immich
|
||||
ENV IMMICH_REPOSITORY_URL=https://github.com/immich-app/immich
|
||||
ENV IMMICH_SOURCE_REF=${BUILD_SOURCE_REF}
|
||||
ENV IMMICH_SOURCE_COMMIT=${BUILD_SOURCE_COMMIT}
|
||||
ENV IMMICH_SOURCE_URL=https://github.com/immich-app/immich/commit/${BUILD_SOURCE_COMMIT}
|
||||
|
||||
VOLUME /usr/src/app/upload
|
||||
EXPOSE 3001
|
||||
ENTRYPOINT ["tini", "--", "/bin/bash"]
|
||||
|
@ -429,3 +429,15 @@ export const clsConfig: ClsModuleOptions = {
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const getBuildMetadata = () => ({
|
||||
build: process.env.IMMICH_BUILD,
|
||||
buildUrl: process.env.IMMICH_BUILD_URL,
|
||||
buildImage: process.env.IMMICH_BUILD_IMAGE,
|
||||
buildImageUrl: process.env.IMMICH_BUILD_IMAGE_URL,
|
||||
repository: process.env.IMMICH_REPOSITORY,
|
||||
repositoryUrl: process.env.IMMICH_REPOSITORY_URL,
|
||||
sourceRef: process.env.IMMICH_SOURCE_REF,
|
||||
sourceCommit: process.env.IMMICH_SOURCE_COMMIT,
|
||||
sourceUrl: process.env.IMMICH_SOURCE_URL,
|
||||
});
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import {
|
||||
ServerAboutResponseDto,
|
||||
ServerConfigDto,
|
||||
ServerFeaturesDto,
|
||||
ServerMediaTypesResponseDto,
|
||||
@ -22,6 +23,12 @@ export class ServerInfoController {
|
||||
private versionService: VersionService,
|
||||
) {}
|
||||
|
||||
@Get('about')
|
||||
@Authenticated()
|
||||
getAboutInfo(): Promise<ServerAboutResponseDto> {
|
||||
return this.service.getAboutInfo();
|
||||
}
|
||||
|
||||
@Get('storage')
|
||||
@Authenticated()
|
||||
getStorage(): Promise<ServerStorageResponseDto> {
|
||||
|
@ -7,6 +7,29 @@ export class ServerPingResponse {
|
||||
res!: string;
|
||||
}
|
||||
|
||||
export class ServerAboutResponseDto {
|
||||
version!: string;
|
||||
versionUrl!: string;
|
||||
|
||||
repository?: string;
|
||||
repositoryUrl?: string;
|
||||
|
||||
sourceRef?: string;
|
||||
sourceCommit?: string;
|
||||
sourceUrl?: string;
|
||||
|
||||
build?: string;
|
||||
buildUrl?: string;
|
||||
buildImage?: string;
|
||||
buildImageUrl?: string;
|
||||
|
||||
nodejs?: string;
|
||||
ffmpeg?: string;
|
||||
imagemagick?: string;
|
||||
libvips?: string;
|
||||
exiftool?: string;
|
||||
}
|
||||
|
||||
export class ServerStorageResponseDto {
|
||||
diskSize!: string;
|
||||
diskUse!: string;
|
||||
|
@ -8,8 +8,17 @@ export interface GitHubRelease {
|
||||
body: string;
|
||||
}
|
||||
|
||||
export interface ServerBuildVersions {
|
||||
nodejs: string;
|
||||
ffmpeg: string;
|
||||
libvips: string;
|
||||
exiftool: string;
|
||||
imagemagick: string;
|
||||
}
|
||||
|
||||
export const IServerInfoRepository = 'IServerInfoRepository';
|
||||
|
||||
export interface IServerInfoRepository {
|
||||
getGitHubRelease(): Promise<GitHubRelease>;
|
||||
getBuildVersions(): Promise<ServerBuildVersions>;
|
||||
}
|
||||
|
@ -1,10 +1,45 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { GitHubRelease, IServerInfoRepository } from 'src/interfaces/server-info.interface';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { exiftool } from 'exiftool-vendored';
|
||||
import { exec as execCallback } from 'node:child_process';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { promisify } from 'node:util';
|
||||
import sharp from 'sharp';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { GitHubRelease, IServerInfoRepository, ServerBuildVersions } from 'src/interfaces/server-info.interface';
|
||||
import { Instrumentation } from 'src/utils/instrumentation';
|
||||
|
||||
const exec = promisify(execCallback);
|
||||
const maybeFirstLine = async (command: string): Promise<string> => {
|
||||
try {
|
||||
const { stdout } = await exec(command);
|
||||
return stdout.trim().split('\n')[0] || '';
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
type BuildLockfile = {
|
||||
sources: Array<{ name: string; version: string }>;
|
||||
packages: Array<{ name: string; version: string }>;
|
||||
};
|
||||
|
||||
const getLockfileVersion = (name: string, lockfile?: BuildLockfile) => {
|
||||
if (!lockfile) {
|
||||
return;
|
||||
}
|
||||
|
||||
const items = [...(lockfile.sources || []), ...(lockfile?.packages || [])];
|
||||
const item = items.find((item) => item.name === name);
|
||||
return item?.version;
|
||||
};
|
||||
|
||||
@Instrumentation()
|
||||
@Injectable()
|
||||
export class ServerInfoRepository implements IServerInfoRepository {
|
||||
constructor(@Inject(ILoggerRepository) private logger: ILoggerRepository) {
|
||||
this.logger.setContext(ServerInfoRepository.name);
|
||||
}
|
||||
|
||||
async getGitHubRelease(): Promise<GitHubRelease> {
|
||||
try {
|
||||
const response = await fetch('https://api.github.com/repos/immich-app/immich/releases/latest');
|
||||
@ -18,4 +53,25 @@ export class ServerInfoRepository implements IServerInfoRepository {
|
||||
throw new Error(`Failed to fetch GitHub release: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
async getBuildVersions(): Promise<ServerBuildVersions> {
|
||||
const [nodejsOutput, ffmpegOutput, magickOutput] = await Promise.all([
|
||||
maybeFirstLine('node --version'),
|
||||
maybeFirstLine('ffmpeg -version'),
|
||||
maybeFirstLine('convert --version'),
|
||||
]);
|
||||
|
||||
const lockfile = await readFile('build-lock.json')
|
||||
.then((buffer) => JSON.parse(buffer.toString()))
|
||||
.catch(() => this.logger.warn('Failed to read build-lock.json'));
|
||||
|
||||
return {
|
||||
nodejs: nodejsOutput || process.env.NODE_VERSION || '',
|
||||
exiftool: await exiftool.version(),
|
||||
ffmpeg: getLockfileVersion('ffmpeg', lockfile) || ffmpegOutput.replaceAll('ffmpeg version', '') || '',
|
||||
libvips: getLockfileVersion('libvips', lockfile) || sharp.versions.vips,
|
||||
imagemagick:
|
||||
getLockfileVersion('imagemagick', lockfile) || magickOutput.replaceAll('Version: ImageMagick ', '') || '',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,11 @@
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
|
||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||
import { ServerInfoService } from 'src/services/server-info.service';
|
||||
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
||||
import { newServerInfoRepositoryMock } from 'test/repositories/server-info.repository.mock';
|
||||
import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock';
|
||||
import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
|
||||
import { newUserRepositoryMock } from 'test/repositories/user.repository.mock';
|
||||
@ -13,16 +15,18 @@ describe(ServerInfoService.name, () => {
|
||||
let sut: ServerInfoService;
|
||||
let storageMock: Mocked<IStorageRepository>;
|
||||
let userMock: Mocked<IUserRepository>;
|
||||
let serverInfoMock: Mocked<IServerInfoRepository>;
|
||||
let systemMock: Mocked<ISystemMetadataRepository>;
|
||||
let loggerMock: Mocked<ILoggerRepository>;
|
||||
|
||||
beforeEach(() => {
|
||||
storageMock = newStorageRepositoryMock();
|
||||
userMock = newUserRepositoryMock();
|
||||
serverInfoMock = newServerInfoRepositoryMock();
|
||||
systemMock = newSystemMetadataRepositoryMock();
|
||||
loggerMock = newLoggerRepositoryMock();
|
||||
|
||||
sut = new ServerInfoService(userMock, storageMock, systemMock, loggerMock);
|
||||
sut = new ServerInfoService(userMock, storageMock, systemMock, serverInfoMock, loggerMock);
|
||||
});
|
||||
|
||||
it('should work', () => {
|
||||
|
@ -1,7 +1,10 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { getBuildMetadata } from 'src/config';
|
||||
import { serverVersion } from 'src/constants';
|
||||
import { StorageCore, StorageFolder } from 'src/cores/storage.core';
|
||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
||||
import {
|
||||
ServerAboutResponseDto,
|
||||
ServerConfigDto,
|
||||
ServerFeaturesDto,
|
||||
ServerMediaTypesResponseDto,
|
||||
@ -12,6 +15,7 @@ import {
|
||||
} from 'src/dtos/server-info.dto';
|
||||
import { SystemMetadataKey } from 'src/entities/system-metadata.entity';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
|
||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
import { IUserRepository, UserStatsQueryResponse } from 'src/interfaces/user.interface';
|
||||
@ -27,6 +31,7 @@ export class ServerInfoService {
|
||||
@Inject(IUserRepository) private userRepository: IUserRepository,
|
||||
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
||||
@Inject(ISystemMetadataRepository) private systemMetadataRepository: ISystemMetadataRepository,
|
||||
@Inject(IServerInfoRepository) private serverInfoRepository: IServerInfoRepository,
|
||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||
) {
|
||||
this.logger.setContext(ServerInfoService.name);
|
||||
@ -42,6 +47,19 @@ export class ServerInfoService {
|
||||
}
|
||||
}
|
||||
|
||||
async getAboutInfo(): Promise<ServerAboutResponseDto> {
|
||||
const version = serverVersion.toString();
|
||||
const buildMetadata = getBuildMetadata();
|
||||
const buildVersions = await this.serverInfoRepository.getBuildVersions();
|
||||
|
||||
return {
|
||||
version,
|
||||
versionUrl: `https://github.com/immich-app/immich/releases/tag/${version}`,
|
||||
...buildMetadata,
|
||||
...buildVersions,
|
||||
};
|
||||
}
|
||||
|
||||
async getStorage(): Promise<ServerStorageResponseDto> {
|
||||
const libraryBase = StorageCore.getBaseFolder(StorageFolder.LIBRARY);
|
||||
const diskInfo = await this.storageRepository.checkDiskUsage(libraryBase);
|
||||
|
@ -10,7 +10,7 @@ import { VersionService } from 'src/services/version.service';
|
||||
import { newEventRepositoryMock } from 'test/repositories/event.repository.mock';
|
||||
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
|
||||
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
||||
import { newServerInfoRepositoryMock } from 'test/repositories/system-info.repository.mock';
|
||||
import { newServerInfoRepositoryMock } from 'test/repositories/server-info.repository.mock';
|
||||
import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
|
||||
import { Mocked } from 'vitest';
|
||||
|
||||
|
@ -4,5 +4,6 @@ import { Mocked, vitest } from 'vitest';
|
||||
export const newServerInfoRepositoryMock = (): Mocked<IServerInfoRepository> => {
|
||||
return {
|
||||
getGitHubRelease: vitest.fn(),
|
||||
getBuildVersions: vitest.fn(),
|
||||
};
|
||||
};
|
@ -0,0 +1,156 @@
|
||||
<script lang="ts">
|
||||
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
|
||||
import Portal from '$lib/components/shared-components/portal/portal.svelte';
|
||||
import { type ServerAboutResponseDto } from '@immich/sdk';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let onClose: () => void;
|
||||
|
||||
export let info: ServerAboutResponseDto;
|
||||
</script>
|
||||
|
||||
<Portal>
|
||||
<FullScreenModal title={$t('about')} {onClose}>
|
||||
<div
|
||||
class="immich-scrollbar max-h-[500px] overflow-y-auto flex flex-col sm:grid sm:grid-cols-2 gap-1 text-immich-primary dark:text-immich-dark-primary"
|
||||
>
|
||||
<div>
|
||||
<label class="font-medium text-immich-primary dark:text-immich-dark-primary text-sm" for="version-desc"
|
||||
>Immich</label
|
||||
>
|
||||
<div>
|
||||
<a
|
||||
href={info.versionUrl}
|
||||
class="underline text-sm immich-form-label"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
id="version-desc"
|
||||
>
|
||||
{info.version}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="font-medium text-immich-primary dark:text-immich-dark-primary text-sm" for="ffmpeg-desc"
|
||||
>ExifTool</label
|
||||
>
|
||||
<p class="immich-form-label pb-2 text-sm" id="ffmpeg-desc">
|
||||
{info.exiftool}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="font-medium text-immich-primary dark:text-immich-dark-primary text-sm" for="nodejs-desc"
|
||||
>Node.js</label
|
||||
>
|
||||
<p class="immich-form-label pb-2 text-sm" id="nodejs-desc">
|
||||
{info.nodejs}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="font-medium text-immich-primary dark:text-immich-dark-primary text-sm" for="vips-desc"
|
||||
>Libvips</label
|
||||
>
|
||||
<p class="immich-form-label pb-2 text-sm" id="vips-desc">
|
||||
{info.libvips}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class={(info.imagemagick?.length || 0) > 10 ? 'col-span-2' : ''}>
|
||||
<label class="font-medium text-immich-primary dark:text-immich-dark-primary text-sm" for="imagemagick-desc"
|
||||
>ImageMagick</label
|
||||
>
|
||||
<p class="immich-form-label pb-2 text-sm" id="imagemagick-desc">
|
||||
{info.imagemagick}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class={(info.ffmpeg?.length || 0) > 10 ? 'col-span-2' : ''}>
|
||||
<label class="font-medium text-immich-primary dark:text-immich-dark-primary text-sm" for="ffmpeg-desc"
|
||||
>FFmpeg</label
|
||||
>
|
||||
<p class="immich-form-label pb-2 text-sm" id="ffmpeg-desc">
|
||||
{info.ffmpeg}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{#if info.repository && info.repositoryUrl}
|
||||
<div>
|
||||
<label class="font-medium text-immich-primary dark:text-immich-dark-primary text-sm" for="version-desc"
|
||||
>{$t('repository')}</label
|
||||
>
|
||||
<div>
|
||||
<a
|
||||
href={info.repositoryUrl}
|
||||
class="underline text-sm immich-form-label"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
id="version-desc"
|
||||
>
|
||||
{info.repository}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if info.sourceRef && info.sourceCommit && info.sourceUrl}
|
||||
<div>
|
||||
<label class="font-medium text-immich-primary dark:text-immich-dark-primary text-sm" for="git-desc"
|
||||
>{$t('source')}</label
|
||||
>
|
||||
<div>
|
||||
<a
|
||||
href={info.sourceUrl}
|
||||
class="underline text-sm immich-form-label"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
id="git-desc"
|
||||
>
|
||||
{info.sourceRef}@{info.sourceCommit.slice(0, 9)}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if info.build && info.buildUrl}
|
||||
<div>
|
||||
<label class="font-medium text-immich-primary dark:text-immich-dark-primary text-sm" for="build-desc"
|
||||
>{$t('build')}</label
|
||||
>
|
||||
<div>
|
||||
<a
|
||||
href={info.buildUrl}
|
||||
class="underline text-sm immich-form-label"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
id="build-desc"
|
||||
>
|
||||
{info.build}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if info.buildImage && info.buildImage}
|
||||
<div>
|
||||
<label class="font-medium text-immich-primary dark:text-immich-dark-primary text-sm" for="build-image-desc"
|
||||
>{$t('build_image')}</label
|
||||
>
|
||||
<div>
|
||||
<a
|
||||
href={info.buildImageUrl}
|
||||
class="underline text-sm immich-form-label"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
id="build-image-desc"
|
||||
>
|
||||
{info.buildImage}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</FullScreenModal>
|
||||
</Portal>
|
@ -1,19 +1,22 @@
|
||||
<script lang="ts">
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import ServerAboutModal from '$lib/components/shared-components/server-about-modal.svelte';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { websocketStore } from '$lib/stores/websocket';
|
||||
import { onMount } from 'svelte';
|
||||
import { getByteUnitString } from '../../utils/byte-units';
|
||||
import LoadingSpinner from './loading-spinner.svelte';
|
||||
import { mdiChartPie, mdiDns } from '@mdi/js';
|
||||
import { serverInfo } from '$lib/stores/server-info.store';
|
||||
import { user } from '$lib/stores/user.store';
|
||||
import { websocketStore } from '$lib/stores/websocket';
|
||||
import { requestServerInfo } from '$lib/utils/auth';
|
||||
import { mdiChartPie, mdiDns } from '@mdi/js';
|
||||
import { onMount } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { getByteUnitString } from '../../utils/byte-units';
|
||||
import LoadingSpinner from './loading-spinner.svelte';
|
||||
import { getAboutInfo, type ServerAboutResponseDto } from '@immich/sdk';
|
||||
|
||||
const { serverVersion, connected } = websocketStore;
|
||||
|
||||
let usageClasses = '';
|
||||
let isOpen = false;
|
||||
|
||||
$: version = $serverVersion ? `v${$serverVersion.major}.${$serverVersion.minor}.${$serverVersion.patch}` : null;
|
||||
$: hasQuota = $user?.quotaSizeInBytes !== null;
|
||||
@ -21,6 +24,8 @@
|
||||
$: usedBytes = (hasQuota ? $user?.quotaUsageInBytes : $serverInfo?.diskUseRaw) || 0;
|
||||
$: usedPercentage = Math.round((usedBytes / availableBytes) * 100);
|
||||
|
||||
let aboutInfo: ServerAboutResponseDto;
|
||||
|
||||
const onUpdate = () => {
|
||||
usageClasses = getUsageClass();
|
||||
};
|
||||
@ -41,9 +46,14 @@
|
||||
|
||||
onMount(async () => {
|
||||
await requestServerInfo();
|
||||
aboutInfo = await getAboutInfo();
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if isOpen}
|
||||
<ServerAboutModal onClose={() => (isOpen = false)} info={aboutInfo} />
|
||||
{/if}
|
||||
|
||||
<div class="dark:text-immich-dark-fg">
|
||||
<div
|
||||
class="storage-status grid grid-cols-[64px_auto]"
|
||||
@ -96,13 +106,11 @@
|
||||
<div class="mt-2 flex justify-between justify-items-center">
|
||||
<p>{$t('version')}</p>
|
||||
{#if $connected && version}
|
||||
<a
|
||||
href="https://github.com/immich-app/immich/releases"
|
||||
class="font-medium text-immich-primary dark:text-immich-dark-primary"
|
||||
target="_blank"
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => (isOpen = true)}
|
||||
class="font-medium text-immich-primary dark:text-immich-dark-primary">{version}</button
|
||||
>
|
||||
{version}
|
||||
</a>
|
||||
{:else}
|
||||
<p class="font-medium text-red-500">{$t('unknown')}</p>
|
||||
{/if}
|
||||
|
@ -1,4 +1,5 @@
|
||||
{
|
||||
"about": "About",
|
||||
"account": "Account",
|
||||
"account_settings": "Account Settings",
|
||||
"acknowledge": "Acknowledge",
|
||||
@ -380,6 +381,8 @@
|
||||
"birthdate_saved": "Date of birth saved successfully",
|
||||
"birthdate_set_description": "Date of birth is used to calculate the age of this person at the time of a photo.",
|
||||
"blurred_background": "Blurred background",
|
||||
"build": "Build",
|
||||
"build_image": "Build Image",
|
||||
"bulk_delete_duplicates_confirmation": "Are you sure you want to bulk delete {count, plural, one {# duplicate asset} other {# duplicate assets}}? This will keep the largest asset of each group and permanently delete all other duplicates. You cannot undo this action!",
|
||||
"bulk_keep_duplicates_confirmation": "Are you sure you want to keep {count, plural, one {# duplicate asset} other {# duplicate assets}}? This will resolve all duplicate groups without deleting anything.",
|
||||
"bulk_trash_duplicates_confirmation": "Are you sure you want to bulk trash {count, plural, one {# duplicate asset} other {# duplicate assets}}? This will keep the largest asset of each group and trash all other duplicates.",
|
||||
@ -904,6 +907,7 @@
|
||||
"repair": "Repair",
|
||||
"repair_no_results_message": "Untracked and missing files will show up here",
|
||||
"replace_with_upload": "Replace with upload",
|
||||
"repository": "Repository",
|
||||
"require_password": "Require password",
|
||||
"require_user_to_change_password_on_first_login": "Require user to change password on first login",
|
||||
"reset": "Reset",
|
||||
@ -1016,6 +1020,7 @@
|
||||
"sort_oldest": "Oldest photo",
|
||||
"sort_recent": "Most recent photo",
|
||||
"sort_title": "Title",
|
||||
"source": "Source",
|
||||
"stack": "Stack",
|
||||
"stack_selected_photos": "Stack selected photos",
|
||||
"stacked_assets_count": "Stacked {count, plural, one {# asset} other {# assets}}",
|
||||
|
Loading…
Reference in New Issue
Block a user