feat(web,server): offline/untracked files admin tool (#4447)

* feat: admin repair orphans tool

* chore: open api

* fix: include upload folder

* fix: bugs

* feat: empty placeholder

* fix: checks

* feat: move buttons to top of page

* feat: styling and clipboard

* styling

* better clicking hitbox

* fix: show title on hover

* feat: download report

* restrict file access to immich related files

* Add description

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
This commit is contained in:
Jason Rasmussen 2023-10-14 13:12:59 -04:00 committed by GitHub
parent ed386dd12a
commit d2807b8d6a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
53 changed files with 3104 additions and 87 deletions

View file

@ -1604,6 +1604,109 @@ export interface ExifResponseDto {
*/
'timeZone'?: string | null;
}
/**
*
* @export
* @interface FileChecksumDto
*/
export interface FileChecksumDto {
/**
*
* @type {Array<string>}
* @memberof FileChecksumDto
*/
'filenames': Array<string>;
}
/**
*
* @export
* @interface FileChecksumResponseDto
*/
export interface FileChecksumResponseDto {
/**
*
* @type {string}
* @memberof FileChecksumResponseDto
*/
'checksum': string;
/**
*
* @type {string}
* @memberof FileChecksumResponseDto
*/
'filename': string;
}
/**
*
* @export
* @interface FileReportDto
*/
export interface FileReportDto {
/**
*
* @type {Array<string>}
* @memberof FileReportDto
*/
'extras': Array<string>;
/**
*
* @type {Array<FileReportItemDto>}
* @memberof FileReportDto
*/
'orphans': Array<FileReportItemDto>;
}
/**
*
* @export
* @interface FileReportFixDto
*/
export interface FileReportFixDto {
/**
*
* @type {Array<FileReportItemDto>}
* @memberof FileReportFixDto
*/
'items': Array<FileReportItemDto>;
}
/**
*
* @export
* @interface FileReportItemDto
*/
export interface FileReportItemDto {
/**
*
* @type {string}
* @memberof FileReportItemDto
*/
'checksum'?: string;
/**
*
* @type {string}
* @memberof FileReportItemDto
*/
'entityId': string;
/**
*
* @type {PathEntityType}
* @memberof FileReportItemDto
*/
'entityType': PathEntityType;
/**
*
* @type {PathType}
* @memberof FileReportItemDto
*/
'pathType': PathType;
/**
*
* @type {string}
* @memberof FileReportItemDto
*/
'pathValue': string;
}
/**
*
* @export
@ -2186,6 +2289,40 @@ export interface OAuthConfigResponseDto {
*/
'url'?: string;
}
/**
*
* @export
* @enum {string}
*/
export const PathEntityType = {
Asset: 'asset',
Person: 'person',
User: 'user'
} as const;
export type PathEntityType = typeof PathEntityType[keyof typeof PathEntityType];
/**
*
* @export
* @enum {string}
*/
export const PathType = {
Original: 'original',
JpegThumbnail: 'jpeg_thumbnail',
WebpThumbnail: 'webp_thumbnail',
EncodedVideo: 'encoded_video',
Sidecar: 'sidecar',
Face: 'face',
Profile: 'profile'
} as const;
export type PathType = typeof PathType[keyof typeof PathType];
/**
*
* @export
@ -8821,6 +8958,50 @@ export class AssetApi extends BaseAPI {
*/
export const AuditApiAxiosParamCreator = function (configuration?: Configuration) {
return {
/**
*
* @param {FileReportFixDto} fileReportFixDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
fixAuditFiles: async (fileReportFixDto: FileReportFixDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'fileReportFixDto' is not null or undefined
assertParamExists('fixAuditFiles', 'fileReportFixDto', fileReportFixDto)
const localVarPath = `/audit/file-report/fix`;
// 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: 'POST', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication cookie required
// authentication api_key required
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
// authentication bearer required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
localVarHeaderParameter['Content-Type'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(fileReportFixDto, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {EntityType} entityType
@ -8875,6 +9056,88 @@ export const AuditApiAxiosParamCreator = function (configuration?: Configuration
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}
*/
getAuditFiles: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/audit/file-report`;
// 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 cookie required
// authentication api_key required
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
// authentication bearer required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {FileChecksumDto} fileChecksumDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getFileChecksums: async (fileChecksumDto: FileChecksumDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'fileChecksumDto' is not null or undefined
assertParamExists('getFileChecksums', 'fileChecksumDto', fileChecksumDto)
const localVarPath = `/audit/file-report/checksum`;
// 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: 'POST', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication cookie required
// authentication api_key required
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
// authentication bearer required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
localVarHeaderParameter['Content-Type'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(fileChecksumDto, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
@ -8890,6 +9153,16 @@ export const AuditApiAxiosParamCreator = function (configuration?: Configuration
export const AuditApiFp = function(configuration?: Configuration) {
const localVarAxiosParamCreator = AuditApiAxiosParamCreator(configuration)
return {
/**
*
* @param {FileReportFixDto} fileReportFixDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async fixAuditFiles(fileReportFixDto: FileReportFixDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.fixAuditFiles(fileReportFixDto, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {EntityType} entityType
@ -8902,6 +9175,25 @@ export const AuditApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.getAuditDeletes(entityType, after, userId, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getAuditFiles(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<FileReportDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getAuditFiles(options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {FileChecksumDto} fileChecksumDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getFileChecksums(fileChecksumDto: FileChecksumDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<FileChecksumResponseDto>>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getFileChecksums(fileChecksumDto, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
}
};
@ -8912,6 +9204,15 @@ export const AuditApiFp = function(configuration?: Configuration) {
export const AuditApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
const localVarFp = AuditApiFp(configuration)
return {
/**
*
* @param {AuditApiFixAuditFilesRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
fixAuditFiles(requestParameters: AuditApiFixAuditFilesRequest, options?: AxiosRequestConfig): AxiosPromise<void> {
return localVarFp.fixAuditFiles(requestParameters.fileReportFixDto, options).then((request) => request(axios, basePath));
},
/**
*
* @param {AuditApiGetAuditDeletesRequest} requestParameters Request parameters.
@ -8921,9 +9222,40 @@ export const AuditApiFactory = function (configuration?: Configuration, basePath
getAuditDeletes(requestParameters: AuditApiGetAuditDeletesRequest, options?: AxiosRequestConfig): AxiosPromise<AuditDeletesResponseDto> {
return localVarFp.getAuditDeletes(requestParameters.entityType, requestParameters.after, requestParameters.userId, options).then((request) => request(axios, basePath));
},
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getAuditFiles(options?: AxiosRequestConfig): AxiosPromise<FileReportDto> {
return localVarFp.getAuditFiles(options).then((request) => request(axios, basePath));
},
/**
*
* @param {AuditApiGetFileChecksumsRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getFileChecksums(requestParameters: AuditApiGetFileChecksumsRequest, options?: AxiosRequestConfig): AxiosPromise<Array<FileChecksumResponseDto>> {
return localVarFp.getFileChecksums(requestParameters.fileChecksumDto, options).then((request) => request(axios, basePath));
},
};
};
/**
* Request parameters for fixAuditFiles operation in AuditApi.
* @export
* @interface AuditApiFixAuditFilesRequest
*/
export interface AuditApiFixAuditFilesRequest {
/**
*
* @type {FileReportFixDto}
* @memberof AuditApiFixAuditFiles
*/
readonly fileReportFixDto: FileReportFixDto
}
/**
* Request parameters for getAuditDeletes operation in AuditApi.
* @export
@ -8952,6 +9284,20 @@ export interface AuditApiGetAuditDeletesRequest {
readonly userId?: string
}
/**
* Request parameters for getFileChecksums operation in AuditApi.
* @export
* @interface AuditApiGetFileChecksumsRequest
*/
export interface AuditApiGetFileChecksumsRequest {
/**
*
* @type {FileChecksumDto}
* @memberof AuditApiGetFileChecksums
*/
readonly fileChecksumDto: FileChecksumDto
}
/**
* AuditApi - object-oriented interface
* @export
@ -8959,6 +9305,17 @@ export interface AuditApiGetAuditDeletesRequest {
* @extends {BaseAPI}
*/
export class AuditApi extends BaseAPI {
/**
*
* @param {AuditApiFixAuditFilesRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof AuditApi
*/
public fixAuditFiles(requestParameters: AuditApiFixAuditFilesRequest, options?: AxiosRequestConfig) {
return AuditApiFp(this.configuration).fixAuditFiles(requestParameters.fileReportFixDto, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {AuditApiGetAuditDeletesRequest} requestParameters Request parameters.
@ -8969,6 +9326,27 @@ export class AuditApi extends BaseAPI {
public getAuditDeletes(requestParameters: AuditApiGetAuditDeletesRequest, options?: AxiosRequestConfig) {
return AuditApiFp(this.configuration).getAuditDeletes(requestParameters.entityType, requestParameters.after, requestParameters.userId, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof AuditApi
*/
public getAuditFiles(options?: AxiosRequestConfig) {
return AuditApiFp(this.configuration).getAuditFiles(options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {AuditApiGetFileChecksumsRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof AuditApi
*/
public getFileChecksums(requestParameters: AuditApiGetFileChecksumsRequest, options?: AxiosRequestConfig) {
return AuditApiFp(this.configuration).getFileChecksums(requestParameters.fileChecksumDto, options).then((request) => request(this.axios, this.basePath));
}
}

View file

@ -59,6 +59,11 @@ doc/DownloadInfoDto.md
doc/DownloadResponseDto.md
doc/EntityType.md
doc/ExifResponseDto.md
doc/FileChecksumDto.md
doc/FileChecksumResponseDto.md
doc/FileReportDto.md
doc/FileReportFixDto.md
doc/FileReportItemDto.md
doc/ImportAssetDto.md
doc/JobApi.md
doc/JobCommand.md
@ -84,6 +89,8 @@ doc/OAuthCallbackDto.md
doc/OAuthConfigDto.md
doc/OAuthConfigResponseDto.md
doc/PartnerApi.md
doc/PathEntityType.md
doc/PathType.md
doc/PeopleResponseDto.md
doc/PeopleUpdateDto.md
doc/PeopleUpdateItem.md
@ -227,6 +234,11 @@ lib/model/download_info_dto.dart
lib/model/download_response_dto.dart
lib/model/entity_type.dart
lib/model/exif_response_dto.dart
lib/model/file_checksum_dto.dart
lib/model/file_checksum_response_dto.dart
lib/model/file_report_dto.dart
lib/model/file_report_fix_dto.dart
lib/model/file_report_item_dto.dart
lib/model/import_asset_dto.dart
lib/model/job_command.dart
lib/model/job_command_dto.dart
@ -248,6 +260,8 @@ lib/model/o_auth_authorize_response_dto.dart
lib/model/o_auth_callback_dto.dart
lib/model/o_auth_config_dto.dart
lib/model/o_auth_config_response_dto.dart
lib/model/path_entity_type.dart
lib/model/path_type.dart
lib/model/people_response_dto.dart
lib/model/people_update_dto.dart
lib/model/people_update_item.dart
@ -364,6 +378,11 @@ test/download_info_dto_test.dart
test/download_response_dto_test.dart
test/entity_type_test.dart
test/exif_response_dto_test.dart
test/file_checksum_dto_test.dart
test/file_checksum_response_dto_test.dart
test/file_report_dto_test.dart
test/file_report_fix_dto_test.dart
test/file_report_item_dto_test.dart
test/import_asset_dto_test.dart
test/job_api_test.dart
test/job_command_dto_test.dart
@ -389,6 +408,8 @@ test/o_auth_callback_dto_test.dart
test/o_auth_config_dto_test.dart
test/o_auth_config_response_dto_test.dart
test/partner_api_test.dart
test/path_entity_type_test.dart
test/path_type_test.dart
test/people_response_dto_test.dart
test/people_update_dto_test.dart
test/people_update_item_test.dart

View file

@ -117,7 +117,10 @@ Class | Method | HTTP request | Description
*AssetApi* | [**updateAsset**](doc//AssetApi.md#updateasset) | **PUT** /asset/{id} |
*AssetApi* | [**updateAssets**](doc//AssetApi.md#updateassets) | **PUT** /asset |
*AssetApi* | [**uploadFile**](doc//AssetApi.md#uploadfile) | **POST** /asset/upload |
*AuditApi* | [**fixAuditFiles**](doc//AuditApi.md#fixauditfiles) | **POST** /audit/file-report/fix |
*AuditApi* | [**getAuditDeletes**](doc//AuditApi.md#getauditdeletes) | **GET** /audit/deletes |
*AuditApi* | [**getAuditFiles**](doc//AuditApi.md#getauditfiles) | **GET** /audit/file-report |
*AuditApi* | [**getFileChecksums**](doc//AuditApi.md#getfilechecksums) | **POST** /audit/file-report/checksum |
*AuthenticationApi* | [**adminSignUp**](doc//AuthenticationApi.md#adminsignup) | **POST** /auth/admin-sign-up |
*AuthenticationApi* | [**changePassword**](doc//AuthenticationApi.md#changepassword) | **POST** /auth/change-password |
*AuthenticationApi* | [**getAuthDevices**](doc//AuthenticationApi.md#getauthdevices) | **GET** /auth/devices |
@ -247,6 +250,11 @@ Class | Method | HTTP request | Description
- [DownloadResponseDto](doc//DownloadResponseDto.md)
- [EntityType](doc//EntityType.md)
- [ExifResponseDto](doc//ExifResponseDto.md)
- [FileChecksumDto](doc//FileChecksumDto.md)
- [FileChecksumResponseDto](doc//FileChecksumResponseDto.md)
- [FileReportDto](doc//FileReportDto.md)
- [FileReportFixDto](doc//FileReportFixDto.md)
- [FileReportItemDto](doc//FileReportItemDto.md)
- [ImportAssetDto](doc//ImportAssetDto.md)
- [JobCommand](doc//JobCommand.md)
- [JobCommandDto](doc//JobCommandDto.md)
@ -268,6 +276,8 @@ Class | Method | HTTP request | Description
- [OAuthCallbackDto](doc//OAuthCallbackDto.md)
- [OAuthConfigDto](doc//OAuthConfigDto.md)
- [OAuthConfigResponseDto](doc//OAuthConfigResponseDto.md)
- [PathEntityType](doc//PathEntityType.md)
- [PathType](doc//PathType.md)
- [PeopleResponseDto](doc//PeopleResponseDto.md)
- [PeopleUpdateDto](doc//PeopleUpdateDto.md)
- [PeopleUpdateItem](doc//PeopleUpdateItem.md)

View file

@ -9,9 +9,66 @@ All URIs are relative to */api*
Method | HTTP request | Description
------------- | ------------- | -------------
[**fixAuditFiles**](AuditApi.md#fixauditfiles) | **POST** /audit/file-report/fix |
[**getAuditDeletes**](AuditApi.md#getauditdeletes) | **GET** /audit/deletes |
[**getAuditFiles**](AuditApi.md#getauditfiles) | **GET** /audit/file-report |
[**getFileChecksums**](AuditApi.md#getfilechecksums) | **POST** /audit/file-report/checksum |
# **fixAuditFiles**
> fixAuditFiles(fileReportFixDto)
### Example
```dart
import 'package:openapi/api.dart';
// TODO Configure API key authorization: cookie
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
// TODO Configure API key authorization: api_key
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
// TODO Configure HTTP Bearer authorization: bearer
// Case 1. Use String Token
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
// Case 2. Use Function which generate token.
// String yourTokenGeneratorFunction() { ... }
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = AuditApi();
final fileReportFixDto = FileReportFixDto(); // FileReportFixDto |
try {
api_instance.fixAuditFiles(fileReportFixDto);
} catch (e) {
print('Exception when calling AuditApi->fixAuditFiles: $e\n');
}
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**fileReportFixDto** | [**FileReportFixDto**](FileReportFixDto.md)| |
### Return type
void (empty response body)
### Authorization
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
### HTTP request headers
- **Content-Type**: application/json
- **Accept**: Not defined
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **getAuditDeletes**
> AuditDeletesResponseDto getAuditDeletes(entityType, after, userId)
@ -71,3 +128,109 @@ Name | Type | Description | Notes
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **getAuditFiles**
> FileReportDto getAuditFiles()
### Example
```dart
import 'package:openapi/api.dart';
// TODO Configure API key authorization: cookie
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
// TODO Configure API key authorization: api_key
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
// TODO Configure HTTP Bearer authorization: bearer
// Case 1. Use String Token
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
// Case 2. Use Function which generate token.
// String yourTokenGeneratorFunction() { ... }
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = AuditApi();
try {
final result = api_instance.getAuditFiles();
print(result);
} catch (e) {
print('Exception when calling AuditApi->getAuditFiles: $e\n');
}
```
### Parameters
This endpoint does not need any parameter.
### Return type
[**FileReportDto**](FileReportDto.md)
### Authorization
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: application/json
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **getFileChecksums**
> List<FileChecksumResponseDto> getFileChecksums(fileChecksumDto)
### Example
```dart
import 'package:openapi/api.dart';
// TODO Configure API key authorization: cookie
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
// TODO Configure API key authorization: api_key
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
// TODO Configure HTTP Bearer authorization: bearer
// Case 1. Use String Token
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
// Case 2. Use Function which generate token.
// String yourTokenGeneratorFunction() { ... }
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = AuditApi();
final fileChecksumDto = FileChecksumDto(); // FileChecksumDto |
try {
final result = api_instance.getFileChecksums(fileChecksumDto);
print(result);
} catch (e) {
print('Exception when calling AuditApi->getFileChecksums: $e\n');
}
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**fileChecksumDto** | [**FileChecksumDto**](FileChecksumDto.md)| |
### Return type
[**List<FileChecksumResponseDto>**](FileChecksumResponseDto.md)
### Authorization
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
### HTTP request headers
- **Content-Type**: application/json
- **Accept**: application/json
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)

15
mobile/openapi/doc/FileChecksumDto.md generated Normal file
View file

@ -0,0 +1,15 @@
# openapi.model.FileChecksumDto
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**filenames** | **List<String>** | | [default to const []]
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View file

@ -0,0 +1,16 @@
# openapi.model.FileChecksumResponseDto
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**checksum** | **String** | |
**filename** | **String** | |
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

16
mobile/openapi/doc/FileReportDto.md generated Normal file
View file

@ -0,0 +1,16 @@
# openapi.model.FileReportDto
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**extras** | **List<String>** | | [default to const []]
**orphans** | [**List<FileReportItemDto>**](FileReportItemDto.md) | | [default to const []]
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

15
mobile/openapi/doc/FileReportFixDto.md generated Normal file
View file

@ -0,0 +1,15 @@
# openapi.model.FileReportFixDto
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**items** | [**List<FileReportItemDto>**](FileReportItemDto.md) | | [default to const []]
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

19
mobile/openapi/doc/FileReportItemDto.md generated Normal file
View file

@ -0,0 +1,19 @@
# openapi.model.FileReportItemDto
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**checksum** | **String** | | [optional]
**entityId** | **String** | |
**entityType** | [**PathEntityType**](PathEntityType.md) | |
**pathType** | [**PathType**](PathType.md) | |
**pathValue** | **String** | |
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

14
mobile/openapi/doc/PathEntityType.md generated Normal file
View file

@ -0,0 +1,14 @@
# openapi.model.PathEntityType
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

14
mobile/openapi/doc/PathType.md generated Normal file
View file

@ -0,0 +1,14 @@
# openapi.model.PathType
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View file

@ -96,6 +96,11 @@ part 'model/download_info_dto.dart';
part 'model/download_response_dto.dart';
part 'model/entity_type.dart';
part 'model/exif_response_dto.dart';
part 'model/file_checksum_dto.dart';
part 'model/file_checksum_response_dto.dart';
part 'model/file_report_dto.dart';
part 'model/file_report_fix_dto.dart';
part 'model/file_report_item_dto.dart';
part 'model/import_asset_dto.dart';
part 'model/job_command.dart';
part 'model/job_command_dto.dart';
@ -117,6 +122,8 @@ part 'model/o_auth_authorize_response_dto.dart';
part 'model/o_auth_callback_dto.dart';
part 'model/o_auth_config_dto.dart';
part 'model/o_auth_config_response_dto.dart';
part 'model/path_entity_type.dart';
part 'model/path_type.dart';
part 'model/people_response_dto.dart';
part 'model/people_update_dto.dart';
part 'model/people_update_item.dart';

View file

@ -16,6 +16,45 @@ class AuditApi {
final ApiClient apiClient;
/// Performs an HTTP 'POST /audit/file-report/fix' operation and returns the [Response].
/// Parameters:
///
/// * [FileReportFixDto] fileReportFixDto (required):
Future<Response> fixAuditFilesWithHttpInfo(FileReportFixDto fileReportFixDto,) async {
// ignore: prefer_const_declarations
final path = r'/audit/file-report/fix';
// ignore: prefer_final_locals
Object? postBody = fileReportFixDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
path,
'POST',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [FileReportFixDto] fileReportFixDto (required):
Future<void> fixAuditFiles(FileReportFixDto fileReportFixDto,) async {
final response = await fixAuditFilesWithHttpInfo(fileReportFixDto,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
}
/// Performs an HTTP 'GET /audit/deletes' operation and returns the [Response].
/// Parameters:
///
@ -76,4 +115,95 @@ class AuditApi {
}
return null;
}
/// Performs an HTTP 'GET /audit/file-report' operation and returns the [Response].
Future<Response> getAuditFilesWithHttpInfo() async {
// ignore: prefer_const_declarations
final path = r'/audit/file-report';
// 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<FileReportDto?> getAuditFiles() async {
final response = await getAuditFilesWithHttpInfo();
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'FileReportDto',) as FileReportDto;
}
return null;
}
/// Performs an HTTP 'POST /audit/file-report/checksum' operation and returns the [Response].
/// Parameters:
///
/// * [FileChecksumDto] fileChecksumDto (required):
Future<Response> getFileChecksumsWithHttpInfo(FileChecksumDto fileChecksumDto,) async {
// ignore: prefer_const_declarations
final path = r'/audit/file-report/checksum';
// ignore: prefer_final_locals
Object? postBody = fileChecksumDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
path,
'POST',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [FileChecksumDto] fileChecksumDto (required):
Future<List<FileChecksumResponseDto>?> getFileChecksums(FileChecksumDto fileChecksumDto,) async {
final response = await getFileChecksumsWithHttpInfo(fileChecksumDto,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
final responseBody = await _decodeBodyBytes(response);
return (await apiClient.deserializeAsync(responseBody, 'List<FileChecksumResponseDto>') as List)
.cast<FileChecksumResponseDto>()
.toList();
}
return null;
}
}

View file

@ -283,6 +283,16 @@ class ApiClient {
return EntityTypeTypeTransformer().decode(value);
case 'ExifResponseDto':
return ExifResponseDto.fromJson(value);
case 'FileChecksumDto':
return FileChecksumDto.fromJson(value);
case 'FileChecksumResponseDto':
return FileChecksumResponseDto.fromJson(value);
case 'FileReportDto':
return FileReportDto.fromJson(value);
case 'FileReportFixDto':
return FileReportFixDto.fromJson(value);
case 'FileReportItemDto':
return FileReportItemDto.fromJson(value);
case 'ImportAssetDto':
return ImportAssetDto.fromJson(value);
case 'JobCommand':
@ -325,6 +335,10 @@ class ApiClient {
return OAuthConfigDto.fromJson(value);
case 'OAuthConfigResponseDto':
return OAuthConfigResponseDto.fromJson(value);
case 'PathEntityType':
return PathEntityTypeTypeTransformer().decode(value);
case 'PathType':
return PathTypeTypeTransformer().decode(value);
case 'PeopleResponseDto':
return PeopleResponseDto.fromJson(value);
case 'PeopleUpdateDto':

View file

@ -91,6 +91,12 @@ String parameterToString(dynamic value) {
if (value is ModelType) {
return ModelTypeTypeTransformer().encode(value).toString();
}
if (value is PathEntityType) {
return PathEntityTypeTypeTransformer().encode(value).toString();
}
if (value is PathType) {
return PathTypeTypeTransformer().encode(value).toString();
}
if (value is SharedLinkType) {
return SharedLinkTypeTypeTransformer().encode(value).toString();
}

View file

@ -0,0 +1,100 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class FileChecksumDto {
/// Returns a new [FileChecksumDto] instance.
FileChecksumDto({
this.filenames = const [],
});
List<String> filenames;
@override
bool operator ==(Object other) => identical(this, other) || other is FileChecksumDto &&
other.filenames == filenames;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(filenames.hashCode);
@override
String toString() => 'FileChecksumDto[filenames=$filenames]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'filenames'] = this.filenames;
return json;
}
/// Returns a new [FileChecksumDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static FileChecksumDto? fromJson(dynamic value) {
if (value is Map) {
final json = value.cast<String, dynamic>();
return FileChecksumDto(
filenames: json[r'filenames'] is List
? (json[r'filenames'] as List).cast<String>()
: const [],
);
}
return null;
}
static List<FileChecksumDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <FileChecksumDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = FileChecksumDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, FileChecksumDto> mapFromJson(dynamic json) {
final map = <String, FileChecksumDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = FileChecksumDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of FileChecksumDto-objects as value to a dart map
static Map<String, List<FileChecksumDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<FileChecksumDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = FileChecksumDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'filenames',
};
}

View file

@ -0,0 +1,106 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class FileChecksumResponseDto {
/// Returns a new [FileChecksumResponseDto] instance.
FileChecksumResponseDto({
required this.checksum,
required this.filename,
});
String checksum;
String filename;
@override
bool operator ==(Object other) => identical(this, other) || other is FileChecksumResponseDto &&
other.checksum == checksum &&
other.filename == filename;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(checksum.hashCode) +
(filename.hashCode);
@override
String toString() => 'FileChecksumResponseDto[checksum=$checksum, filename=$filename]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'checksum'] = this.checksum;
json[r'filename'] = this.filename;
return json;
}
/// Returns a new [FileChecksumResponseDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static FileChecksumResponseDto? fromJson(dynamic value) {
if (value is Map) {
final json = value.cast<String, dynamic>();
return FileChecksumResponseDto(
checksum: mapValueOfType<String>(json, r'checksum')!,
filename: mapValueOfType<String>(json, r'filename')!,
);
}
return null;
}
static List<FileChecksumResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <FileChecksumResponseDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = FileChecksumResponseDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, FileChecksumResponseDto> mapFromJson(dynamic json) {
final map = <String, FileChecksumResponseDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = FileChecksumResponseDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of FileChecksumResponseDto-objects as value to a dart map
static Map<String, List<FileChecksumResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<FileChecksumResponseDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = FileChecksumResponseDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'checksum',
'filename',
};
}

View file

@ -0,0 +1,108 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class FileReportDto {
/// Returns a new [FileReportDto] instance.
FileReportDto({
this.extras = const [],
this.orphans = const [],
});
List<String> extras;
List<FileReportItemDto> orphans;
@override
bool operator ==(Object other) => identical(this, other) || other is FileReportDto &&
other.extras == extras &&
other.orphans == orphans;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(extras.hashCode) +
(orphans.hashCode);
@override
String toString() => 'FileReportDto[extras=$extras, orphans=$orphans]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'extras'] = this.extras;
json[r'orphans'] = this.orphans;
return json;
}
/// Returns a new [FileReportDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static FileReportDto? fromJson(dynamic value) {
if (value is Map) {
final json = value.cast<String, dynamic>();
return FileReportDto(
extras: json[r'extras'] is List
? (json[r'extras'] as List).cast<String>()
: const [],
orphans: FileReportItemDto.listFromJson(json[r'orphans']),
);
}
return null;
}
static List<FileReportDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <FileReportDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = FileReportDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, FileReportDto> mapFromJson(dynamic json) {
final map = <String, FileReportDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = FileReportDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of FileReportDto-objects as value to a dart map
static Map<String, List<FileReportDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<FileReportDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = FileReportDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'extras',
'orphans',
};
}

View file

@ -0,0 +1,98 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class FileReportFixDto {
/// Returns a new [FileReportFixDto] instance.
FileReportFixDto({
this.items = const [],
});
List<FileReportItemDto> items;
@override
bool operator ==(Object other) => identical(this, other) || other is FileReportFixDto &&
other.items == items;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(items.hashCode);
@override
String toString() => 'FileReportFixDto[items=$items]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'items'] = this.items;
return json;
}
/// Returns a new [FileReportFixDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static FileReportFixDto? fromJson(dynamic value) {
if (value is Map) {
final json = value.cast<String, dynamic>();
return FileReportFixDto(
items: FileReportItemDto.listFromJson(json[r'items']),
);
}
return null;
}
static List<FileReportFixDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <FileReportFixDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = FileReportFixDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, FileReportFixDto> mapFromJson(dynamic json) {
final map = <String, FileReportFixDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = FileReportFixDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of FileReportFixDto-objects as value to a dart map
static Map<String, List<FileReportFixDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<FileReportFixDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = FileReportFixDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'items',
};
}

View file

@ -0,0 +1,139 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class FileReportItemDto {
/// Returns a new [FileReportItemDto] instance.
FileReportItemDto({
this.checksum,
required this.entityId,
required this.entityType,
required this.pathType,
required this.pathValue,
});
///
/// 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? checksum;
String entityId;
PathEntityType entityType;
PathType pathType;
String pathValue;
@override
bool operator ==(Object other) => identical(this, other) || other is FileReportItemDto &&
other.checksum == checksum &&
other.entityId == entityId &&
other.entityType == entityType &&
other.pathType == pathType &&
other.pathValue == pathValue;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(checksum == null ? 0 : checksum!.hashCode) +
(entityId.hashCode) +
(entityType.hashCode) +
(pathType.hashCode) +
(pathValue.hashCode);
@override
String toString() => 'FileReportItemDto[checksum=$checksum, entityId=$entityId, entityType=$entityType, pathType=$pathType, pathValue=$pathValue]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
if (this.checksum != null) {
json[r'checksum'] = this.checksum;
} else {
// json[r'checksum'] = null;
}
json[r'entityId'] = this.entityId;
json[r'entityType'] = this.entityType;
json[r'pathType'] = this.pathType;
json[r'pathValue'] = this.pathValue;
return json;
}
/// Returns a new [FileReportItemDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static FileReportItemDto? fromJson(dynamic value) {
if (value is Map) {
final json = value.cast<String, dynamic>();
return FileReportItemDto(
checksum: mapValueOfType<String>(json, r'checksum'),
entityId: mapValueOfType<String>(json, r'entityId')!,
entityType: PathEntityType.fromJson(json[r'entityType'])!,
pathType: PathType.fromJson(json[r'pathType'])!,
pathValue: mapValueOfType<String>(json, r'pathValue')!,
);
}
return null;
}
static List<FileReportItemDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <FileReportItemDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = FileReportItemDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, FileReportItemDto> mapFromJson(dynamic json) {
final map = <String, FileReportItemDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = FileReportItemDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of FileReportItemDto-objects as value to a dart map
static Map<String, List<FileReportItemDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<FileReportItemDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = FileReportItemDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'entityId',
'entityType',
'pathType',
'pathValue',
};
}

View file

@ -0,0 +1,88 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class PathEntityType {
/// Instantiate a new enum with the provided [value].
const PathEntityType._(this.value);
/// The underlying value of this enum member.
final String value;
@override
String toString() => value;
String toJson() => value;
static const asset = PathEntityType._(r'asset');
static const person = PathEntityType._(r'person');
static const user = PathEntityType._(r'user');
/// List of all possible values in this [enum][PathEntityType].
static const values = <PathEntityType>[
asset,
person,
user,
];
static PathEntityType? fromJson(dynamic value) => PathEntityTypeTypeTransformer().decode(value);
static List<PathEntityType>? listFromJson(dynamic json, {bool growable = false,}) {
final result = <PathEntityType>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = PathEntityType.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
}
/// Transformation class that can [encode] an instance of [PathEntityType] to String,
/// and [decode] dynamic data back to [PathEntityType].
class PathEntityTypeTypeTransformer {
factory PathEntityTypeTypeTransformer() => _instance ??= const PathEntityTypeTypeTransformer._();
const PathEntityTypeTypeTransformer._();
String encode(PathEntityType data) => data.value;
/// Decodes a [dynamic value][data] to a PathEntityType.
///
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
///
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
/// and users are still using an old app with the old code.
PathEntityType? decode(dynamic data, {bool allowNull = true}) {
if (data != null) {
switch (data) {
case r'asset': return PathEntityType.asset;
case r'person': return PathEntityType.person;
case r'user': return PathEntityType.user;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');
}
}
}
return null;
}
/// Singleton [PathEntityTypeTypeTransformer] instance.
static PathEntityTypeTypeTransformer? _instance;
}

100
mobile/openapi/lib/model/path_type.dart generated Normal file
View file

@ -0,0 +1,100 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class PathType {
/// Instantiate a new enum with the provided [value].
const PathType._(this.value);
/// The underlying value of this enum member.
final String value;
@override
String toString() => value;
String toJson() => value;
static const original = PathType._(r'original');
static const jpegThumbnail = PathType._(r'jpeg_thumbnail');
static const webpThumbnail = PathType._(r'webp_thumbnail');
static const encodedVideo = PathType._(r'encoded_video');
static const sidecar = PathType._(r'sidecar');
static const face = PathType._(r'face');
static const profile = PathType._(r'profile');
/// List of all possible values in this [enum][PathType].
static const values = <PathType>[
original,
jpegThumbnail,
webpThumbnail,
encodedVideo,
sidecar,
face,
profile,
];
static PathType? fromJson(dynamic value) => PathTypeTypeTransformer().decode(value);
static List<PathType>? listFromJson(dynamic json, {bool growable = false,}) {
final result = <PathType>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = PathType.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
}
/// Transformation class that can [encode] an instance of [PathType] to String,
/// and [decode] dynamic data back to [PathType].
class PathTypeTypeTransformer {
factory PathTypeTypeTransformer() => _instance ??= const PathTypeTypeTransformer._();
const PathTypeTypeTransformer._();
String encode(PathType data) => data.value;
/// Decodes a [dynamic value][data] to a PathType.
///
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
///
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
/// and users are still using an old app with the old code.
PathType? decode(dynamic data, {bool allowNull = true}) {
if (data != null) {
switch (data) {
case r'original': return PathType.original;
case r'jpeg_thumbnail': return PathType.jpegThumbnail;
case r'webp_thumbnail': return PathType.webpThumbnail;
case r'encoded_video': return PathType.encodedVideo;
case r'sidecar': return PathType.sidecar;
case r'face': return PathType.face;
case r'profile': return PathType.profile;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');
}
}
}
return null;
}
/// Singleton [PathTypeTypeTransformer] instance.
static PathTypeTypeTransformer? _instance;
}

View file

@ -17,10 +17,25 @@ void main() {
// final instance = AuditApi();
group('tests for AuditApi', () {
//Future fixAuditFiles(FileReportFixDto fileReportFixDto) async
test('test fixAuditFiles', () async {
// TODO
});
//Future<AuditDeletesResponseDto> getAuditDeletes(EntityType entityType, DateTime after, { String userId }) async
test('test getAuditDeletes', () async {
// TODO
});
//Future<FileReportDto> getAuditFiles() async
test('test getAuditFiles', () async {
// TODO
});
//Future<List<FileChecksumResponseDto>> getFileChecksums(FileChecksumDto fileChecksumDto) async
test('test getFileChecksums', () async {
// TODO
});
});
}

View file

@ -0,0 +1,27 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
import 'package:openapi/api.dart';
import 'package:test/test.dart';
// tests for FileChecksumDto
void main() {
// final instance = FileChecksumDto();
group('test FileChecksumDto', () {
// List<String> filenames (default value: const [])
test('to test the property `filenames`', () async {
// TODO
});
});
}

View file

@ -0,0 +1,32 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
import 'package:openapi/api.dart';
import 'package:test/test.dart';
// tests for FileChecksumResponseDto
void main() {
// final instance = FileChecksumResponseDto();
group('test FileChecksumResponseDto', () {
// String checksum
test('to test the property `checksum`', () async {
// TODO
});
// String filename
test('to test the property `filename`', () async {
// TODO
});
});
}

View file

@ -0,0 +1,32 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
import 'package:openapi/api.dart';
import 'package:test/test.dart';
// tests for FileReportDto
void main() {
// final instance = FileReportDto();
group('test FileReportDto', () {
// List<String> extras (default value: const [])
test('to test the property `extras`', () async {
// TODO
});
// List<FileReportItemDto> orphans (default value: const [])
test('to test the property `orphans`', () async {
// TODO
});
});
}

View file

@ -0,0 +1,27 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
import 'package:openapi/api.dart';
import 'package:test/test.dart';
// tests for FileReportFixDto
void main() {
// final instance = FileReportFixDto();
group('test FileReportFixDto', () {
// List<FileReportItemDto> items (default value: const [])
test('to test the property `items`', () async {
// TODO
});
});
}

View file

@ -0,0 +1,47 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
import 'package:openapi/api.dart';
import 'package:test/test.dart';
// tests for FileReportItemDto
void main() {
// final instance = FileReportItemDto();
group('test FileReportItemDto', () {
// String checksum
test('to test the property `checksum`', () async {
// TODO
});
// String entityId
test('to test the property `entityId`', () async {
// TODO
});
// PathEntityType entityType
test('to test the property `entityType`', () async {
// TODO
});
// PathType pathType
test('to test the property `pathType`', () async {
// TODO
});
// String pathValue
test('to test the property `pathValue`', () async {
// TODO
});
});
}

View file

@ -0,0 +1,21 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
import 'package:openapi/api.dart';
import 'package:test/test.dart';
// tests for PathEntityType
void main() {
group('test PathEntityType', () {
});
}

21
mobile/openapi/test/path_type_test.dart generated Normal file
View file

@ -0,0 +1,21 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
import 'package:openapi/api.dart';
import 'package:test/test.dart';
// tests for PathType
void main() {
group('test PathType', () {
});
}

View file

@ -2286,6 +2286,118 @@
]
}
},
"/audit/file-report": {
"get": {
"operationId": "getAuditFiles",
"parameters": [],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/FileReportDto"
}
}
},
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"tags": [
"Audit"
]
}
},
"/audit/file-report/checksum": {
"post": {
"operationId": "getFileChecksums",
"parameters": [],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/FileChecksumDto"
}
}
},
"required": true
},
"responses": {
"201": {
"content": {
"application/json": {
"schema": {
"items": {
"$ref": "#/components/schemas/FileChecksumResponseDto"
},
"type": "array"
}
}
},
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"tags": [
"Audit"
]
}
},
"/audit/file-report/fix": {
"post": {
"operationId": "fixAuditFiles",
"parameters": [],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/FileReportFixDto"
}
}
},
"required": true
},
"responses": {
"201": {
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"tags": [
"Audit"
]
}
},
"/auth/admin-sign-up": {
"post": {
"operationId": "adminSignUp",
@ -6580,6 +6692,97 @@
},
"type": "object"
},
"FileChecksumDto": {
"properties": {
"filenames": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"filenames"
],
"type": "object"
},
"FileChecksumResponseDto": {
"properties": {
"checksum": {
"type": "string"
},
"filename": {
"type": "string"
}
},
"required": [
"filename",
"checksum"
],
"type": "object"
},
"FileReportDto": {
"properties": {
"extras": {
"items": {
"type": "string"
},
"type": "array"
},
"orphans": {
"items": {
"$ref": "#/components/schemas/FileReportItemDto"
},
"type": "array"
}
},
"required": [
"orphans",
"extras"
],
"type": "object"
},
"FileReportFixDto": {
"properties": {
"items": {
"items": {
"$ref": "#/components/schemas/FileReportItemDto"
},
"type": "array"
}
},
"required": [
"items"
],
"type": "object"
},
"FileReportItemDto": {
"properties": {
"checksum": {
"type": "string"
},
"entityId": {
"format": "uuid",
"type": "string"
},
"entityType": {
"$ref": "#/components/schemas/PathEntityType"
},
"pathType": {
"$ref": "#/components/schemas/PathType"
},
"pathValue": {
"type": "string"
}
},
"required": [
"entityId",
"entityType",
"pathType",
"pathValue"
],
"type": "object"
},
"ImportAssetDto": {
"properties": {
"assetPath": {
@ -7027,6 +7230,26 @@
],
"type": "object"
},
"PathEntityType": {
"enum": [
"asset",
"person",
"user"
],
"type": "string"
},
"PathType": {
"enum": [
"original",
"jpeg_thumbnail",
"webp_thumbnail",
"encoded_video",
"sidecar",
"face",
"profile"
],
"type": "string"
},
"PeopleResponseDto": {
"properties": {
"people": {

View file

@ -1,8 +1,10 @@
import { EntityType } from '@app/infra/entities';
import { AssetPathType, EntityType, PathType, PersonPathType, UserPathType } from '@app/infra/entities';
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsDate, IsEnum, IsUUID } from 'class-validator';
import { Optional } from '../domain.util';
import { IsArray, IsDate, IsEnum, IsString, IsUUID, ValidateNested } from 'class-validator';
import { Optional, ValidateUUID } from '../domain.util';
const PathEnum = Object.values({ ...AssetPathType, ...PersonPathType, ...UserPathType });
export class AuditDeletesDto {
@IsDate()
@ -19,7 +21,54 @@ export class AuditDeletesDto {
userId?: string;
}
export enum PathEntityType {
ASSET = 'asset',
PERSON = 'person',
USER = 'user',
}
export class AuditDeletesResponseDto {
needsFullSync!: boolean;
ids!: string[];
}
export class FileReportDto {
orphans!: FileReportItemDto[];
extras!: string[];
}
export class FileChecksumDto {
@IsString({ each: true })
filenames!: string[];
}
export class FileChecksumResponseDto {
filename!: string;
checksum!: string;
}
export class FileReportFixDto {
@IsArray()
@ValidateNested({ each: true })
@Type(() => FileReportItemDto)
items!: FileReportItemDto[];
}
// used both as request and response dto
export class FileReportItemDto {
@ValidateUUID()
entityId!: string;
@ApiProperty({ enumName: 'PathEntityType', enum: PathEntityType })
@IsEnum(PathEntityType)
entityType!: PathEntityType;
@ApiProperty({ enumName: 'PathType', enum: PathEnum })
@IsEnum(PathEnum)
pathType!: PathType;
@IsString()
pathValue!: string;
checksum?: string;
}

View file

@ -1,17 +1,45 @@
import { DatabaseAction, EntityType } from '@app/infra/entities';
import { IAccessRepositoryMock, auditStub, authStub, newAccessRepositoryMock, newAuditRepositoryMock } from '@test';
import { IAuditRepository } from '../repositories';
import {
IAccessRepositoryMock,
auditStub,
authStub,
newAccessRepositoryMock,
newAssetRepositoryMock,
newAuditRepositoryMock,
newCryptoRepositoryMock,
newPersonRepositoryMock,
newStorageRepositoryMock,
newUserRepositoryMock,
} from '@test';
import {
IAssetRepository,
IAuditRepository,
ICryptoRepository,
IPersonRepository,
IStorageRepository,
IUserRepository,
} from '../repositories';
import { AuditService } from './audit.service';
describe(AuditService.name, () => {
let sut: AuditService;
let accessMock: IAccessRepositoryMock;
let assetMock: jest.Mocked<IAssetRepository>;
let auditMock: jest.Mocked<IAuditRepository>;
let cryptoMock: jest.Mocked<ICryptoRepository>;
let personMock: jest.Mocked<IPersonRepository>;
let storageMock: jest.Mocked<IStorageRepository>;
let userMock: jest.Mocked<IUserRepository>;
beforeEach(async () => {
accessMock = newAccessRepositoryMock();
assetMock = newAssetRepositoryMock();
cryptoMock = newCryptoRepositoryMock();
auditMock = newAuditRepositoryMock();
sut = new AuditService(accessMock, auditMock);
personMock = newPersonRepositoryMock();
storageMock = newStorageRepositoryMock();
userMock = newUserRepositoryMock();
sut = new AuditService(accessMock, assetMock, cryptoMock, personMock, auditMock, storageMock, userMock);
});
it('should work', () => {

View file

@ -1,19 +1,44 @@
import { DatabaseAction } from '@app/infra/entities';
import { Inject, Injectable } from '@nestjs/common';
import { AssetPathType, DatabaseAction, PersonPathType, UserPathType } from '@app/infra/entities';
import { BadRequestException, Inject, Injectable, Logger } from '@nestjs/common';
import { DateTime } from 'luxon';
import { resolve } from 'node:path';
import { AccessCore, Permission } from '../access';
import { AuthUserDto } from '../auth';
import { AUDIT_LOG_MAX_DURATION } from '../domain.constant';
import { IAccessRepository, IAuditRepository } from '../repositories';
import { AuditDeletesDto, AuditDeletesResponseDto } from './audit.dto';
import { usePagination } from '../domain.util';
import { JOBS_ASSET_PAGINATION_SIZE } from '../job';
import {
IAccessRepository,
IAssetRepository,
IAuditRepository,
ICryptoRepository,
IPersonRepository,
IStorageRepository,
IUserRepository,
} from '../repositories';
import { StorageCore, StorageFolder } from '../storage';
import {
AuditDeletesDto,
AuditDeletesResponseDto,
FileChecksumDto,
FileChecksumResponseDto,
FileReportItemDto,
PathEntityType,
} from './audit.dto';
@Injectable()
export class AuditService {
private access: AccessCore;
private logger = new Logger(AuditService.name);
constructor(
@Inject(IAccessRepository) accessRepository: IAccessRepository,
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
@Inject(IPersonRepository) private personRepository: IPersonRepository,
@Inject(IAuditRepository) private repository: IAuditRepository,
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
@Inject(IUserRepository) private userRepository: IUserRepository,
) {
this.access = new AccessCore(accessRepository);
}
@ -40,4 +65,160 @@ export class AuditService {
ids: audits.map(({ entityId }) => entityId),
};
}
async getChecksums(dto: FileChecksumDto) {
const results: FileChecksumResponseDto[] = [];
for (const filename of dto.filenames) {
if (!StorageCore.isImmichPath(filename)) {
throw new BadRequestException(
`Could not get the checksum of ${filename} because the file isn't accessible by Immich`,
);
}
const checksum = await this.cryptoRepository.hashFile(filename);
results.push({ filename, checksum: checksum.toString('base64') });
}
return results;
}
async fixItems(items: FileReportItemDto[]) {
for (const { entityId: id, pathType, pathValue } of items) {
if (!StorageCore.isImmichPath(pathValue)) {
throw new BadRequestException(
`Could not fix item ${id} with path ${pathValue} because the file isn't accessible by Immich`,
);
}
switch (pathType) {
case AssetPathType.ENCODED_VIDEO:
await this.assetRepository.save({ id, encodedVideoPath: pathValue });
break;
case AssetPathType.JPEG_THUMBNAIL:
await this.assetRepository.save({ id, resizePath: pathValue });
break;
case AssetPathType.WEBP_THUMBNAIL:
await this.assetRepository.save({ id, webpPath: pathValue });
break;
case AssetPathType.ORIGINAL:
await this.assetRepository.save({ id, originalPath: pathValue });
break;
case AssetPathType.SIDECAR:
await this.assetRepository.save({ id, sidecarPath: pathValue });
break;
case PersonPathType.FACE:
await this.personRepository.update({ id, thumbnailPath: pathValue });
break;
case UserPathType.PROFILE:
await this.userRepository.update(id, { profileImagePath: pathValue });
break;
}
}
}
async getFileReport() {
const fullPath = (filename: string) => resolve(filename);
const hasFile = (items: Set<string>, filename: string) => items.has(filename) || items.has(fullPath(filename));
const crawl = async (folder: StorageFolder) =>
new Set(await this.storageRepository.crawl({ pathsToCrawl: [StorageCore.getBaseFolder(folder)] }));
const uploadFiles = await crawl(StorageFolder.UPLOAD);
const libraryFiles = await crawl(StorageFolder.LIBRARY);
const thumbFiles = await crawl(StorageFolder.THUMBNAILS);
const videoFiles = await crawl(StorageFolder.ENCODED_VIDEO);
const profileFiles = await crawl(StorageFolder.PROFILE);
const allFiles = new Set<string>();
for (const list of [libraryFiles, thumbFiles, videoFiles, profileFiles, uploadFiles]) {
for (const item of list) {
allFiles.add(item);
}
}
const track = (filename: string | null) => {
if (!filename) {
return;
}
allFiles.delete(filename);
allFiles.delete(fullPath(filename));
};
this.logger.log(
`Found ${libraryFiles.size} original files, ${thumbFiles.size} thumbnails, ${videoFiles.size} encoded videos, ${profileFiles.size} profile files`,
);
const pagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (options) =>
this.assetRepository.getAll(options, { withDeleted: true }),
);
let assetCount = 0;
const orphans: FileReportItemDto[] = [];
for await (const assets of pagination) {
assetCount += assets.length;
for (const { id, originalPath, resizePath, encodedVideoPath, webpPath, isExternal, checksum } of assets) {
for (const file of [originalPath, resizePath, encodedVideoPath, webpPath]) {
track(file);
}
const entity = { entityId: id, entityType: PathEntityType.ASSET, checksum: checksum.toString('base64') };
if (
originalPath &&
!hasFile(libraryFiles, originalPath) &&
!hasFile(uploadFiles, originalPath) &&
// Android motion assets
!hasFile(videoFiles, originalPath) &&
// ignore external library assets
!isExternal
) {
orphans.push({ ...entity, pathType: AssetPathType.ORIGINAL, pathValue: originalPath });
}
if (resizePath && !hasFile(thumbFiles, resizePath)) {
orphans.push({ ...entity, pathType: AssetPathType.JPEG_THUMBNAIL, pathValue: resizePath });
}
if (webpPath && !hasFile(thumbFiles, webpPath)) {
orphans.push({ ...entity, pathType: AssetPathType.WEBP_THUMBNAIL, pathValue: webpPath });
}
if (encodedVideoPath && !hasFile(videoFiles, encodedVideoPath)) {
orphans.push({ ...entity, pathType: AssetPathType.WEBP_THUMBNAIL, pathValue: encodedVideoPath });
}
}
}
const users = await this.userRepository.getList();
for (const { id, profileImagePath } of users) {
track(profileImagePath);
const entity = { entityId: id, entityType: PathEntityType.USER };
if (profileImagePath && !hasFile(profileFiles, profileImagePath)) {
orphans.push({ ...entity, pathType: UserPathType.PROFILE, pathValue: profileImagePath });
}
}
const people = await this.personRepository.getAll();
for (const { id, thumbnailPath } of people) {
track(thumbnailPath);
const entity = { entityId: id, entityType: PathEntityType.PERSON };
if (thumbnailPath && !hasFile(thumbFiles, thumbnailPath)) {
orphans.push({ ...entity, pathType: PersonPathType.FACE, pathValue: thumbnailPath });
}
}
this.logger.log(`Found ${assetCount} assets, ${users.length} users, ${people.length} people`);
const extras: string[] = [];
for (const file of allFiles) {
extras.push(file);
}
// send as absolute paths
for (const orphan of orphans) {
orphan.pathValue = fullPath(orphan.pathValue);
}
return { orphans, extras };
}
}

View file

@ -289,6 +289,9 @@ export class MetadataService {
});
const checksum = this.cryptoRepository.hashSha1(video);
const motionPath = this.storageCore.getAndroidMotionPath(asset);
this.storageCore.ensureFolders(motionPath);
let motionAsset = await this.assetRepository.getByChecksum(asset.ownerId, checksum);
if (!motionAsset) {
const createdAt = asset.fileCreatedAt ?? asset.createdAt;
@ -300,7 +303,7 @@ export class MetadataService {
localDateTime: createdAt,
checksum,
ownerId: asset.ownerId,
originalPath: this.storageCore.getAndroidMotionPath(asset),
originalPath: motionPath,
originalFileName: asset.originalFileName,
isVisible: false,
isReadOnly: false,

View file

@ -14,6 +14,7 @@ export interface AssetSearchOptions {
trashedBefore?: Date;
type?: AssetType;
order?: 'ASC' | 'DESC';
withDeleted?: boolean;
}
export interface LivePhotoSearchOptions {

View file

@ -1,40 +1,20 @@
import {
newAssetRepositoryMock,
newMoveRepositoryMock,
newPersonRepositoryMock,
newStorageRepositoryMock,
newSystemConfigRepositoryMock,
newUserRepositoryMock,
} from '@test';
import { newStorageRepositoryMock, newSystemConfigRepositoryMock, newUserRepositoryMock } from '@test';
import { serverVersion } from '../domain.constant';
import {
IAssetRepository,
IMoveRepository,
IPersonRepository,
IStorageRepository,
ISystemConfigRepository,
IUserRepository,
} from '../repositories';
import { IStorageRepository, ISystemConfigRepository, IUserRepository } from '../repositories';
import { ServerInfoService } from './server-info.service';
describe(ServerInfoService.name, () => {
let sut: ServerInfoService;
let assetMock: jest.Mocked<IAssetRepository>;
let configMock: jest.Mocked<ISystemConfigRepository>;
let moveMock: jest.Mocked<IMoveRepository>;
let personMock: jest.Mocked<IPersonRepository>;
let storageMock: jest.Mocked<IStorageRepository>;
let userMock: jest.Mocked<IUserRepository>;
beforeEach(() => {
assetMock = newAssetRepositoryMock();
configMock = newSystemConfigRepositoryMock();
moveMock = newMoveRepositoryMock();
personMock = newPersonRepositoryMock();
storageMock = newStorageRepositoryMock();
userMock = newUserRepositoryMock();
sut = new ServerInfoService(assetMock, configMock, moveMock, personMock, userMock, storageMock);
sut = new ServerInfoService(configMock, userMock, storageMock);
});
it('should work', () => {

View file

@ -1,15 +1,7 @@
import { Inject, Injectable } from '@nestjs/common';
import { mimeTypes, serverVersion } from '../domain.constant';
import { asHumanReadable } from '../domain.util';
import {
IAssetRepository,
IMoveRepository,
IPersonRepository,
IStorageRepository,
ISystemConfigRepository,
IUserRepository,
UserStatsQueryResponse,
} from '../repositories';
import { IStorageRepository, ISystemConfigRepository, IUserRepository, UserStatsQueryResponse } from '../repositories';
import { StorageCore, StorageFolder } from '../storage';
import { SystemConfigCore } from '../system-config';
import {
@ -25,22 +17,17 @@ import {
@Injectable()
export class ServerInfoService {
private configCore: SystemConfigCore;
private storageCore: StorageCore;
constructor(
@Inject(IAssetRepository) assetRepository: IAssetRepository,
@Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
@Inject(IMoveRepository) moveRepository: IMoveRepository,
@Inject(IPersonRepository) personRepository: IPersonRepository,
@Inject(IUserRepository) private userRepository: IUserRepository,
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
) {
this.configCore = SystemConfigCore.create(configRepository);
this.storageCore = new StorageCore(storageRepository, assetRepository, moveRepository, personRepository);
}
async getInfo(): Promise<ServerInfoResponseDto> {
const libraryBase = this.storageCore.getBaseFolder(StorageFolder.LIBRARY);
const libraryBase = StorageCore.getBaseFolder(StorageFolder.LIBRARY);
const diskInfo = await this.storageRepository.checkDiskUsage(libraryBase);
const usagePercentage = (((diskInfo.total - diskInfo.free) / diskInfo.total) * 100).toFixed(2);

View file

@ -90,7 +90,7 @@ export class StorageTemplateService {
}
this.logger.debug('Cleaning up empty directories...');
const libraryFolder = this.storageCore.getBaseFolder(StorageFolder.LIBRARY);
const libraryFolder = StorageCore.getBaseFolder(StorageFolder.LIBRARY);
await this.storageRepository.removeEmptyDirs(libraryFolder);
this.logger.log('Finished storage template migration');

View file

@ -1,6 +1,6 @@
import { AssetEntity, AssetPathType, PathType, PersonEntity, PersonPathType } from '@app/infra/entities';
import { Logger } from '@nestjs/common';
import { dirname, join } from 'node:path';
import { dirname, join, resolve } from 'node:path';
import { APP_MEDIA_LOCATION } from '../domain.constant';
import { IAssetRepository, IMoveRepository, IPersonRepository, IStorageRepository } from '../repositories';
@ -32,14 +32,14 @@ export class StorageCore {
) {}
getFolderLocation(folder: StorageFolder, userId: string) {
return join(this.getBaseFolder(folder), userId);
return join(StorageCore.getBaseFolder(folder), userId);
}
getLibraryFolder(user: { storageLabel: string | null; id: string }) {
return join(this.getBaseFolder(StorageFolder.LIBRARY), user.storageLabel || user.id);
return join(StorageCore.getBaseFolder(StorageFolder.LIBRARY), user.storageLabel || user.id);
}
getBaseFolder(folder: StorageFolder) {
static getBaseFolder(folder: StorageFolder) {
return join(APP_MEDIA_LOCATION, folder);
}
@ -64,7 +64,11 @@ export class StorageCore {
}
isAndroidMotionPath(originalPath: string) {
return originalPath.startsWith(this.getBaseFolder(StorageFolder.ENCODED_VIDEO));
return originalPath.startsWith(StorageCore.getBaseFolder(StorageFolder.ENCODED_VIDEO));
}
static isImmichPath(path: string) {
return resolve(path).startsWith(resolve(APP_MEDIA_LOCATION));
}
async moveAssetFile(asset: AssetEntity, pathType: GeneratedAssetPath) {
@ -135,7 +139,7 @@ export class StorageCore {
}
removeEmptyDirs(folder: StorageFolder) {
return this.repository.removeEmptyDirs(this.getBaseFolder(folder));
return this.repository.removeEmptyDirs(StorageCore.getBaseFolder(folder));
}
private savePath(pathType: PathType, id: string, newPath: string) {

View file

@ -1,25 +1,14 @@
import {
newAssetRepositoryMock,
newMoveRepositoryMock,
newPersonRepositoryMock,
newStorageRepositoryMock,
} from '@test';
import { IAssetRepository, IMoveRepository, IPersonRepository, IStorageRepository } from '../repositories';
import { newStorageRepositoryMock } from '@test';
import { IStorageRepository } from '../repositories';
import { StorageService } from './storage.service';
describe(StorageService.name, () => {
let sut: StorageService;
let assetMock: jest.Mocked<IAssetRepository>;
let moveMock: jest.Mocked<IMoveRepository>;
let personMock: jest.Mocked<IPersonRepository>;
let storageMock: jest.Mocked<IStorageRepository>;
beforeEach(async () => {
assetMock = newAssetRepositoryMock();
moveMock = newMoveRepositoryMock();
personMock = newPersonRepositoryMock();
storageMock = newStorageRepositoryMock();
sut = new StorageService(assetMock, moveMock, personMock, storageMock);
sut = new StorageService(storageMock);
});
it('should work', () => {

View file

@ -1,24 +1,16 @@
import { Inject, Injectable, Logger } from '@nestjs/common';
import { IDeleteFilesJob } from '../job';
import { IAssetRepository, IMoveRepository, IPersonRepository, IStorageRepository } from '../repositories';
import { IStorageRepository } from '../repositories';
import { StorageCore, StorageFolder } from './storage.core';
@Injectable()
export class StorageService {
private logger = new Logger(StorageService.name);
private storageCore: StorageCore;
constructor(
@Inject(IAssetRepository) assetRepository: IAssetRepository,
@Inject(IMoveRepository) private moveRepository: IMoveRepository,
@Inject(IPersonRepository) personRepository: IPersonRepository,
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
) {
this.storageCore = new StorageCore(storageRepository, assetRepository, moveRepository, personRepository);
}
constructor(@Inject(IStorageRepository) private storageRepository: IStorageRepository) {}
init() {
const libraryBase = this.storageCore.getBaseFolder(StorageFolder.LIBRARY);
const libraryBase = StorageCore.getBaseFolder(StorageFolder.LIBRARY);
this.storageRepository.mkdirSync(libraryBase);
}

View file

@ -1,7 +1,16 @@
import { AuditDeletesDto, AuditDeletesResponseDto, AuditService, AuthUserDto } from '@app/domain';
import { Controller, Get, Query } from '@nestjs/common';
import {
AuditDeletesDto,
AuditDeletesResponseDto,
AuditService,
AuthUserDto,
FileChecksumDto,
FileChecksumResponseDto,
FileReportDto,
FileReportFixDto,
} from '@app/domain';
import { Body, Controller, Get, Post, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { AuthUser, Authenticated } from '../app.guard';
import { AdminRoute, AuthUser, Authenticated } from '../app.guard';
import { UseValidation } from '../app.utils';
@ApiTags('Audit')
@ -15,4 +24,22 @@ export class AuditController {
getAuditDeletes(@AuthUser() authUser: AuthUserDto, @Query() dto: AuditDeletesDto): Promise<AuditDeletesResponseDto> {
return this.service.getDeletes(authUser, dto);
}
@AdminRoute()
@Get('file-report')
getAuditFiles(): Promise<FileReportDto> {
return this.service.getFileReport();
}
@AdminRoute()
@Post('file-report/checksum')
getFileChecksums(@Body() dto: FileChecksumDto): Promise<FileChecksumResponseDto[]> {
return this.service.getChecksums(dto);
}
@AdminRoute()
@Post('file-report/fix')
fixAuditFiles(@Body() dto: FileReportFixDto): Promise<void> {
return this.service.fixItems(dto.items);
}
}

View file

@ -34,4 +34,8 @@ export enum PersonPathType {
FACE = 'face',
}
export type PathType = AssetPathType | PersonPathType;
export enum UserPathType {
PROFILE = 'profile',
}
export type PathType = AssetPathType | PersonPathType | UserPathType;

View file

@ -174,7 +174,7 @@ export class AssetRepository implements IAssetRepository {
person: true,
},
},
withDeleted: !!options.trashedBefore,
withDeleted: options.withDeleted ?? !!options.trashedBefore,
order: {
// Ensures correct order when paginating
createdAt: options.order ?? 'ASC',

View file

@ -19,6 +19,7 @@ import {
SystemConfigApi,
UserApi,
UserApiFp,
AuditApi,
} from './open-api';
import { BASE_PATH } from './open-api/base';
import { DUMMY_BASE_URL, toPathString } from './open-api/common';
@ -28,6 +29,7 @@ export class ImmichApi {
public albumApi: AlbumApi;
public libraryApi: LibraryApi;
public assetApi: AssetApi;
public auditApi: AuditApi;
public authenticationApi: AuthenticationApi;
public jobApi: JobApi;
public keyApi: APIKeyApi;
@ -51,6 +53,7 @@ export class ImmichApi {
this.config = new Configuration(params);
this.albumApi = new AlbumApi(this.config);
this.auditApi = new AuditApi(this.config);
this.libraryApi = new LibraryApi(this.config);
this.assetApi = new AssetApi(this.config);
this.authenticationApi = new AuthenticationApi(this.config);

View file

@ -1604,6 +1604,109 @@ export interface ExifResponseDto {
*/
'timeZone'?: string | null;
}
/**
*
* @export
* @interface FileChecksumDto
*/
export interface FileChecksumDto {
/**
*
* @type {Array<string>}
* @memberof FileChecksumDto
*/
'filenames': Array<string>;
}
/**
*
* @export
* @interface FileChecksumResponseDto
*/
export interface FileChecksumResponseDto {
/**
*
* @type {string}
* @memberof FileChecksumResponseDto
*/
'checksum': string;
/**
*
* @type {string}
* @memberof FileChecksumResponseDto
*/
'filename': string;
}
/**
*
* @export
* @interface FileReportDto
*/
export interface FileReportDto {
/**
*
* @type {Array<string>}
* @memberof FileReportDto
*/
'extras': Array<string>;
/**
*
* @type {Array<FileReportItemDto>}
* @memberof FileReportDto
*/
'orphans': Array<FileReportItemDto>;
}
/**
*
* @export
* @interface FileReportFixDto
*/
export interface FileReportFixDto {
/**
*
* @type {Array<FileReportItemDto>}
* @memberof FileReportFixDto
*/
'items': Array<FileReportItemDto>;
}
/**
*
* @export
* @interface FileReportItemDto
*/
export interface FileReportItemDto {
/**
*
* @type {string}
* @memberof FileReportItemDto
*/
'checksum'?: string;
/**
*
* @type {string}
* @memberof FileReportItemDto
*/
'entityId': string;
/**
*
* @type {PathEntityType}
* @memberof FileReportItemDto
*/
'entityType': PathEntityType;
/**
*
* @type {PathType}
* @memberof FileReportItemDto
*/
'pathType': PathType;
/**
*
* @type {string}
* @memberof FileReportItemDto
*/
'pathValue': string;
}
/**
*
* @export
@ -2186,6 +2289,40 @@ export interface OAuthConfigResponseDto {
*/
'url'?: string;
}
/**
*
* @export
* @enum {string}
*/
export const PathEntityType = {
Asset: 'asset',
Person: 'person',
User: 'user'
} as const;
export type PathEntityType = typeof PathEntityType[keyof typeof PathEntityType];
/**
*
* @export
* @enum {string}
*/
export const PathType = {
Original: 'original',
JpegThumbnail: 'jpeg_thumbnail',
WebpThumbnail: 'webp_thumbnail',
EncodedVideo: 'encoded_video',
Sidecar: 'sidecar',
Face: 'face',
Profile: 'profile'
} as const;
export type PathType = typeof PathType[keyof typeof PathType];
/**
*
* @export
@ -8821,6 +8958,50 @@ export class AssetApi extends BaseAPI {
*/
export const AuditApiAxiosParamCreator = function (configuration?: Configuration) {
return {
/**
*
* @param {FileReportFixDto} fileReportFixDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
fixAuditFiles: async (fileReportFixDto: FileReportFixDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'fileReportFixDto' is not null or undefined
assertParamExists('fixAuditFiles', 'fileReportFixDto', fileReportFixDto)
const localVarPath = `/audit/file-report/fix`;
// 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: 'POST', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication cookie required
// authentication api_key required
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
// authentication bearer required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
localVarHeaderParameter['Content-Type'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(fileReportFixDto, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {EntityType} entityType
@ -8875,6 +9056,88 @@ export const AuditApiAxiosParamCreator = function (configuration?: Configuration
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}
*/
getAuditFiles: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/audit/file-report`;
// 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 cookie required
// authentication api_key required
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
// authentication bearer required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {FileChecksumDto} fileChecksumDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getFileChecksums: async (fileChecksumDto: FileChecksumDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'fileChecksumDto' is not null or undefined
assertParamExists('getFileChecksums', 'fileChecksumDto', fileChecksumDto)
const localVarPath = `/audit/file-report/checksum`;
// 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: 'POST', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication cookie required
// authentication api_key required
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
// authentication bearer required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
localVarHeaderParameter['Content-Type'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(fileChecksumDto, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
@ -8890,6 +9153,16 @@ export const AuditApiAxiosParamCreator = function (configuration?: Configuration
export const AuditApiFp = function(configuration?: Configuration) {
const localVarAxiosParamCreator = AuditApiAxiosParamCreator(configuration)
return {
/**
*
* @param {FileReportFixDto} fileReportFixDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async fixAuditFiles(fileReportFixDto: FileReportFixDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.fixAuditFiles(fileReportFixDto, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {EntityType} entityType
@ -8902,6 +9175,25 @@ export const AuditApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.getAuditDeletes(entityType, after, userId, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getAuditFiles(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<FileReportDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getAuditFiles(options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {FileChecksumDto} fileChecksumDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getFileChecksums(fileChecksumDto: FileChecksumDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<FileChecksumResponseDto>>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getFileChecksums(fileChecksumDto, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
}
};
@ -8912,6 +9204,15 @@ export const AuditApiFp = function(configuration?: Configuration) {
export const AuditApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
const localVarFp = AuditApiFp(configuration)
return {
/**
*
* @param {AuditApiFixAuditFilesRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
fixAuditFiles(requestParameters: AuditApiFixAuditFilesRequest, options?: AxiosRequestConfig): AxiosPromise<void> {
return localVarFp.fixAuditFiles(requestParameters.fileReportFixDto, options).then((request) => request(axios, basePath));
},
/**
*
* @param {AuditApiGetAuditDeletesRequest} requestParameters Request parameters.
@ -8921,9 +9222,40 @@ export const AuditApiFactory = function (configuration?: Configuration, basePath
getAuditDeletes(requestParameters: AuditApiGetAuditDeletesRequest, options?: AxiosRequestConfig): AxiosPromise<AuditDeletesResponseDto> {
return localVarFp.getAuditDeletes(requestParameters.entityType, requestParameters.after, requestParameters.userId, options).then((request) => request(axios, basePath));
},
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getAuditFiles(options?: AxiosRequestConfig): AxiosPromise<FileReportDto> {
return localVarFp.getAuditFiles(options).then((request) => request(axios, basePath));
},
/**
*
* @param {AuditApiGetFileChecksumsRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getFileChecksums(requestParameters: AuditApiGetFileChecksumsRequest, options?: AxiosRequestConfig): AxiosPromise<Array<FileChecksumResponseDto>> {
return localVarFp.getFileChecksums(requestParameters.fileChecksumDto, options).then((request) => request(axios, basePath));
},
};
};
/**
* Request parameters for fixAuditFiles operation in AuditApi.
* @export
* @interface AuditApiFixAuditFilesRequest
*/
export interface AuditApiFixAuditFilesRequest {
/**
*
* @type {FileReportFixDto}
* @memberof AuditApiFixAuditFiles
*/
readonly fileReportFixDto: FileReportFixDto
}
/**
* Request parameters for getAuditDeletes operation in AuditApi.
* @export
@ -8952,6 +9284,20 @@ export interface AuditApiGetAuditDeletesRequest {
readonly userId?: string
}
/**
* Request parameters for getFileChecksums operation in AuditApi.
* @export
* @interface AuditApiGetFileChecksumsRequest
*/
export interface AuditApiGetFileChecksumsRequest {
/**
*
* @type {FileChecksumDto}
* @memberof AuditApiGetFileChecksums
*/
readonly fileChecksumDto: FileChecksumDto
}
/**
* AuditApi - object-oriented interface
* @export
@ -8959,6 +9305,17 @@ export interface AuditApiGetAuditDeletesRequest {
* @extends {BaseAPI}
*/
export class AuditApi extends BaseAPI {
/**
*
* @param {AuditApiFixAuditFilesRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof AuditApi
*/
public fixAuditFiles(requestParameters: AuditApiFixAuditFilesRequest, options?: AxiosRequestConfig) {
return AuditApiFp(this.configuration).fixAuditFiles(requestParameters.fileReportFixDto, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {AuditApiGetAuditDeletesRequest} requestParameters Request parameters.
@ -8969,6 +9326,27 @@ export class AuditApi extends BaseAPI {
public getAuditDeletes(requestParameters: AuditApiGetAuditDeletesRequest, options?: AxiosRequestConfig) {
return AuditApiFp(this.configuration).getAuditDeletes(requestParameters.entityType, requestParameters.after, requestParameters.userId, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof AuditApi
*/
public getAuditFiles(options?: AxiosRequestConfig) {
return AuditApiFp(this.configuration).getAuditFiles(options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {AuditApiGetFileChecksumsRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof AuditApi
*/
public getFileChecksums(requestParameters: AuditApiGetFileChecksumsRequest, options?: AxiosRequestConfig) {
return AuditApiFp(this.configuration).getFileChecksums(requestParameters.fileChecksumDto, options).then((request) => request(this.axios, this.basePath));
}
}

View file

@ -0,0 +1 @@
<svg width="900" height="600" viewBox="0 0 900 600" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill="transparent" d="M0 0h900v600H0z"/><path d="M214.359 475.389c16.42 16.712 47.124 13.189 47.124 13.189s4.064-30.62-12.372-47.322c-16.419-16.712-47.109-13.198-47.109-13.198s-4.063 30.619 12.357 47.331z" fill="url(#a)"/><path d="M639.439 125.517c-17.194 9.808-41.345-.121-41.345-.121s3.743-25.827 20.946-35.623c17.194-9.808 41.335.11 41.335.11s-3.743 25.827-20.936 35.634z" fill="url(#b)"/><path d="M324.812 156.133c-17.672 17.987-50.72 14.194-50.72 14.194s-4.373-32.955 13.316-50.931c17.673-17.987 50.704-14.206 50.704-14.206s4.373 32.956-13.3 50.943z" fill="url(#c)"/><ellipse rx="15.17" ry="15.928" transform="matrix(1 0 0 -1 228.07 341.957)" fill="#E1E4E5"/><circle r="8.5" transform="matrix(1 0 0 -1 478.5 509.5)" fill="#9d9ea3"/><circle r="17.518" transform="matrix(1 0 0 -1 693.518 420.518)" fill="#9d9ea3"/><circle cx="708.183" cy="266.183" r="14.183" fill="#4F4F51"/><circle cx="247.603" cy="225.621" r="12.136" fill="#F8AE9D"/><ellipse cx="316.324" cy="510.867" rx="7.324" ry="6.867" fill="#E1E4E5"/><ellipse cx="664.796" cy="371.388" rx="9.796" ry="9.388" fill="#E1E4E5"/><circle cx="625.378" cy="479.378" r="11.377" fill="#E1E4E5"/><ellipse cx="401.025" cy="114.39" rx="5.309" ry="6.068" fill="#E1E4E5"/><circle cx="661.834" cy="300.834" r="5.58" transform="rotate(105 661.834 300.834)" fill="#E1E4E5"/><circle cx="654.769" cy="226.082" r="7.585" fill="#E1E4E5"/><ellipse cx="254.159" cy="284.946" rx="5.309" ry="4.551" fill="#E1E4E5"/><circle cx="521.363" cy="106.27" r="11.613" transform="rotate(105 521.363 106.27)" fill="#E1E4E5"/><path d="M162.314 308.103h-.149C161.284 320.589 152 320.781 152 320.781s10.238.2 10.238 14.628c0-14.428 10.238-14.628 10.238-14.628s-9.281-.192-10.162-12.678zm531.83-158.512h-.256c-1.518 21.504-17.507 21.835-17.507 21.835s17.632.345 17.632 25.192c0-24.847 17.632-25.192 17.632-25.192s-15.983-.331-17.501-21.835z" fill="#E1E4E5"/><path fill-rule="evenodd" clip-rule="evenodd" d="M553.714 397.505v56.123c0 20.672-16.743 37.416-37.415 37.416H329.22c-20.672 0-37.415-16.744-37.415-37.416V266.55c0-20.672 16.743-37.416 37.415-37.416h56.124" fill="url(#d)"/><path fill-rule="evenodd" clip-rule="evenodd" d="M363.07 155.431h214.049c26.28 0 47.566 21.286 47.566 47.566v214.049c0 26.28-21.286 47.566-47.566 47.566H363.07c-26.28 0-47.566-21.286-47.566-47.566V202.997c0-26.28 21.286-47.566 47.566-47.566z" fill="#9d9ea3"/><path d="m425.113 307.765 33.925 33.924 74.038-74.059" stroke="#fff" stroke-width="32.125" stroke-linecap="round" stroke-linejoin="round"/><defs><linearGradient id="a" x1="279.871" y1="532.474" x2="161.165" y2="346.391" gradientUnits="userSpaceOnUse"><stop stop-color="#fff"/><stop offset="1" stop-color="#EEE"/></linearGradient><linearGradient id="b" x1="573.046" y1="156.85" x2="712.364" y2="32.889" gradientUnits="userSpaceOnUse"><stop stop-color="#fff"/><stop offset="1" stop-color="#EEE"/></linearGradient><linearGradient id="c" x1="254.302" y1="217.573" x2="382.065" y2="17.293" gradientUnits="userSpaceOnUse"><stop stop-color="#fff"/><stop offset="1" stop-color="#EEE"/></linearGradient><linearGradient id="d" x1="417.175" y1="82.293" x2="425.251" y2="775.957" gradientUnits="userSpaceOnUse"><stop stop-color="#fff"/><stop offset="1" stop-color="#EEE"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

View file

@ -6,8 +6,9 @@
import Button from './button.svelte';
export let color: Color = 'transparent-gray';
export let disabled = false;
</script>
<Button size="link" {color} shadow={false} rounded="lg" on:click>
<Button size="link" {color} shadow={false} rounded="lg" {disabled} on:click>
<slot />
</Button>

View file

@ -7,6 +7,7 @@
import AccountMultipleOutline from 'svelte-material-icons/AccountMultipleOutline.svelte';
import Cog from 'svelte-material-icons/Cog.svelte';
import Server from 'svelte-material-icons/Server.svelte';
import Tools from 'svelte-material-icons/Tools.svelte';
import Sync from 'svelte-material-icons/Sync.svelte';
</script>
@ -27,6 +28,9 @@
<a data-sveltekit-preload-data="hover" href={AppRoute.ADMIN_STATS} draggable="false">
<SideBarButton title="Server Stats" logo={Server} isSelected={$page.route.id === AppRoute.ADMIN_STATS} />
</a>
<a data-sveltekit-preload-data="hover" href={AppRoute.ADMIN_REPAIR} draggable="false">
<SideBarButton title="Repair" logo={Tools} isSelected={$page.route.id === AppRoute.ADMIN_REPAIR} />
</a>
<div class="mb-6 mt-auto">
<StatusBox />
</div>

View file

@ -12,6 +12,7 @@ export enum AppRoute {
ADMIN_SETTINGS = '/admin/system-settings',
ADMIN_STATS = '/admin/server-status',
ADMIN_JOBS = '/admin/jobs-status',
ADMIN_REPAIR = '/admin/repair',
ALBUMS = '/albums',
LIBRARIES = '/libraries',

View file

@ -0,0 +1,26 @@
import { AppRoute } from '$lib/constants';
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load = (async ({ parent, locals: { api } }) => {
const { user } = await parent();
if (!user) {
throw redirect(302, AppRoute.AUTH_LOGIN);
} else if (!user.isAdmin) {
throw redirect(302, AppRoute.PHOTOS);
}
const {
data: { orphans, extras },
} = await api.auditApi.getAuditFiles();
return {
user,
orphans,
extras,
meta: {
title: 'Repair',
},
};
}) satisfies PageServerLoad;

View file

@ -0,0 +1,336 @@
<script lang="ts">
import empty4Url from '$lib/assets/empty-4.svg';
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
import LinkButton from '$lib/components/elements/buttons/link-button.svelte';
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
import {
NotificationType,
notificationController,
} from '$lib/components/shared-components/notification/notification';
import { downloadManager } from '$lib/stores/download';
import { downloadBlob } from '$lib/utils/asset-utils';
import { handleError } from '$lib/utils/handle-error';
import { FileReportItemDto, api, copyToClipboard } from '@api';
import CheckAll from 'svelte-material-icons/CheckAll.svelte';
import ContentCopy from 'svelte-material-icons/ContentCopy.svelte';
import Download from 'svelte-material-icons/Download.svelte';
import Refresh from 'svelte-material-icons/Refresh.svelte';
import Wrench from 'svelte-material-icons/Wrench.svelte';
import type { PageData } from './$types';
export let data: PageData;
interface UntrackedFile {
filename: string;
checksum: string | null;
}
interface Match {
orphan: FileReportItemDto;
extra: UntrackedFile;
}
const normalize = (filenames: string[]) => filenames.map((filename) => ({ filename, checksum: null }));
let checking = false;
let repairing = false;
let orphans: FileReportItemDto[] = data.orphans;
let extras: UntrackedFile[] = normalize(data.extras);
let matches: Match[] = [];
const handleDownload = () => {
if (extras.length > 0) {
const blob = new Blob([extras.map(({ filename }) => filename).join('\n')], { type: 'text/plain' });
const downloadKey = 'untracked.txt';
downloadManager.add(downloadKey, blob.size);
downloadManager.update(downloadKey, blob.size);
downloadBlob(blob, downloadKey);
setTimeout(() => downloadManager.clear(downloadKey), 5_000);
}
if (orphans.length > 0) {
const blob = new Blob([JSON.stringify(orphans, null, 4)], { type: 'application/json' });
const downloadKey = 'orphans.json';
downloadManager.add(downloadKey, blob.size);
downloadManager.update(downloadKey, blob.size);
downloadBlob(blob, downloadKey);
setTimeout(() => downloadManager.clear(downloadKey), 5_000);
}
};
const handleRepair = async () => {
if (matches.length === 0) {
return;
}
repairing = true;
try {
await api.auditApi.fixAuditFiles({
fileReportFixDto: {
items: matches.map(({ orphan, extra }) => ({
entityId: orphan.entityId,
entityType: orphan.entityType,
pathType: orphan.pathType,
pathValue: extra.filename,
})),
},
});
notificationController.show({
type: NotificationType.Info,
message: `Repaired ${matches.length} items`,
});
matches = [];
} catch (error) {
handleError(error, 'Unable to repair items');
} finally {
repairing = false;
}
};
const handleSplit = (match: Match) => {
matches = matches.filter((_match) => _match !== match);
orphans = [match.orphan, ...orphans];
extras = [match.extra, ...extras];
};
const handleRefresh = async () => {
matches = [];
orphans = [];
extras = [];
try {
const { data: report } = await api.auditApi.getAuditFiles();
orphans = report.orphans;
extras = normalize(report.extras);
notificationController.show({ message: 'Refreshed', type: NotificationType.Info });
} catch (error) {
handleError(error, 'Unable to load items');
}
};
const handleCheckOne = async (filename: string) => {
try {
const matched = await loadAndMatch([filename]);
if (matched) {
notificationController.show({ message: `Matched 1 item`, type: NotificationType.Info });
}
} catch (error) {
handleError(error, 'Unable to check item');
}
};
const handleCheckAll = async () => {
checking = true;
let count = 0;
try {
const chunkSize = 10;
const filenames = [...extras.filter(({ checksum }) => !checksum).map(({ filename }) => filename)];
for (let i = 0; i < filenames.length; i += chunkSize) {
count += await loadAndMatch(filenames.slice(i, i + chunkSize));
}
} catch (error) {
handleError(error, 'Unable to check items');
} finally {
checking = false;
}
notificationController.show({ message: `Matched ${count} items`, type: NotificationType.Info });
};
const loadAndMatch = async (filenames: string[]) => {
const { data: items } = await api.auditApi.getFileChecksums({
fileChecksumDto: { filenames },
});
let count = 0;
for (const { checksum, filename } of items) {
const extra = extras.find((extra) => extra.filename === filename);
if (extra) {
extra.checksum = checksum;
extras = [...extras];
}
const orphan = orphans.find((orphan) => orphan.checksum === checksum);
if (orphan) {
count++;
matches = [...matches, { orphan, extra: { filename, checksum } }];
orphans = orphans.filter((_orphan) => _orphan !== orphan);
extras = extras.filter((extra) => extra.filename !== filename);
}
}
return count;
};
</script>
<UserPageLayout user={data.user} title={data.meta.title} admin>
<svelte:fragment slot="sidebar" />
<div class="flex justify-end gap-2" slot="buttons">
<LinkButton on:click={() => handleRepair()} disabled={matches.length === 0 || repairing}>
<div class="flex place-items-center gap-2 text-sm">
<Wrench size="18" />
Repair All
</div>
</LinkButton>
<LinkButton on:click={() => handleCheckAll()} disabled={extras.length === 0 || checking}>
<div class="flex place-items-center gap-2 text-sm">
<CheckAll size="18" />
Check All
</div>
</LinkButton>
<LinkButton on:click={() => handleDownload()} disabled={extras.length + orphans.length === 0}>
<div class="flex place-items-center gap-2 text-sm">
<Download size="18" />
Export
</div>
</LinkButton>
<LinkButton on:click={() => handleRefresh()}>
<div class="flex place-items-center gap-2 text-sm">
<Refresh size="18" />
Refresh
</div>
</LinkButton>
</div>
<section id="setting-content" class="flex place-content-center sm:mx-4">
<section class="w-full pb-28 sm:w-5/6 md:w-[850px]">
{#if matches.length + extras.length + orphans.length === 0}
<div class="w-full">
<EmptyPlaceholder
fullWidth
text="Untracked and missing files will show up here"
alt="Empty report"
src={empty4Url}
/>
</div>
{:else}
<div class="gap-2">
<table class="table-fixed mt-5 w-full text-left">
<thead
class="mb-4 flex w-full rounded-md border bg-gray-50 text-immich-primary dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-primary"
>
<tr class="flex w-full place-items-center p-2 md:p-5">
<th class="w-full text-sm place-items-center font-medium flex justify-between" colspan="2">
<div class="px-3">
<p>MATCHES {matches.length ? `(${matches.length})` : ''}</p>
<p class="text-gray-600 dark:text-gray-300 mt-1">These files are matched by their checksums</p>
</div>
</th>
</tr>
</thead>
<tbody
class="w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray dark:text-immich-dark-fg max-h-[500px] block overflow-x-hidden"
>
{#each matches as match (match.extra.filename)}
<tr
class="w-full h-[75px] place-items-center border-[3px] border-transparent p-2 odd:bg-immich-gray even:bg-immich-bg hover:cursor-pointer hover:border-immich-primary/75 odd:dark:bg-immich-dark-gray/75 even:dark:bg-immich-dark-gray/50 dark:hover:border-immich-dark-primary/75 md:p-5 flex justify-between"
tabindex="0"
on:click={() => handleSplit(match)}
>
<td class="text-sm text-ellipsis flex flex-col gap-1 font-mono">
<span>{match.orphan.pathValue} =></span>
<span>{match.extra.filename}</span>
</td>
<td class="text-sm text-ellipsis d-flex font-mono">
<span>({match.orphan.entityType}/{match.orphan.pathType})</span>
</td>
</tr>
{/each}
</tbody>
</table>
<table class="table-fixed mt-5 w-full text-left">
<thead
class="mb-4 flex w-full rounded-md border bg-gray-50 text-immich-primary dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-primary"
>
<tr class="flex w-full place-items-center p-1 md:p-5">
<th class="w-full text-sm font-medium justify-between place-items-center flex" colspan="2">
<div class="px-3">
<p>OFFLINE PATHS {orphans.length ? `(${orphans.length})` : ''}</p>
<p class="text-gray-600 dark:text-gray-300 mt-1">
These files are the results of manually deletion of the default upload library
</p>
</div>
</th>
</tr>
</thead>
<tbody
class="w-full rounded-md border dark:border-immich-dark-gray dark:text-immich-dark-fg overflow-y-auto max-h-[500px] block overflow-x-hidden"
>
{#each orphans as orphan, index (index)}
<tr
class="w-full h-[50px] place-items-center border-[3px] border-transparent odd:bg-immich-gray even:bg-immich-bg hover:cursor-pointer hover:border-immich-primary/75 odd:dark:bg-immich-dark-gray/75 even:dark:bg-immich-dark-gray/50 dark:hover:border-immich-dark-primary/75 md:p-5 flex justify-between"
tabindex="0"
title={orphan.pathValue}
>
<td on:click={() => copyToClipboard(orphan.pathValue)}>
<CircleIconButton logo={ContentCopy} size="18" />
</td>
<td class="truncate text-sm font-mono text-left" title={orphan.pathValue}>
{orphan.pathValue}
</td>
<td class="text-sm font-mono">
<span>({orphan.entityType})</span>
</td>
</tr>
{/each}
</tbody>
</table>
<table class="table-fixed mt-5 w-full text-left max-h-[300px]">
<thead
class="mb-4 flex w-full rounded-md border bg-gray-50 text-immich-primary dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-primary"
>
<tr class="flex w-full place-items-center p-2 md:p-5">
<th class="w-full text-sm font-medium place-items-center flex justify-between" colspan="2">
<div class="px-3">
<p>UNTRACKS FILES {extras.length ? `(${extras.length})` : ''}</p>
<p class="text-gray-600 dark:text-gray-300 mt-1">
These files are not tracked by the application. They can be the results of failed moves,
interrupted uploads, or left behind due to a bug
</p>
</div>
</th>
</tr>
</thead>
<tbody
class="w-full rounded-md border-2 dark:border-immich-dark-gray dark:text-immich-dark-fg overflow-y-auto max-h-[500px] block overflow-x-hidden"
>
{#each extras as extra (extra.filename)}
<tr
class="flex h-[50px] w-full place-items-center border-[3px] border-transparent p-1 odd:bg-immich-gray even:bg-immich-bg hover:cursor-pointer hover:border-immich-primary/75 odd:dark:bg-immich-dark-gray/75 even:dark:bg-immich-dark-gray/50 dark:hover:border-immich-dark-primary/75 md:p-5 justify-between"
tabindex="0"
on:click={() => handleCheckOne(extra.filename)}
title={extra.filename}
>
<td on:click={() => copyToClipboard(extra.filename)}>
<CircleIconButton logo={ContentCopy} size="18" />
</td>
<td class="w-full text-md text-ellipsis flex justify-between pr-5">
<span class="text-ellipsis grow truncate font-mono text-sm pr-5" title={extra.filename}
>{extra.filename}</span
>
<span class="text-sm font-mono dark:text-immich-dark-primary text-immich-primary pr-5">
{#if extra.checksum}
[sha1:{extra.checksum}]
{/if}
</span>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
</section>
</section>
</UserPageLayout>