0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-03-11 02:23:09 -05:00

feat: better mobile sync

This commit is contained in:
Jason Rasmussen 2024-10-23 17:21:28 -04:00
parent 62e0658e5a
commit e095c96fa6
No known key found for this signature in database
GPG key ID: 2EF24B77EAFA4A41
35 changed files with 3127 additions and 16 deletions

View file

@ -0,0 +1,83 @@
import { LoginResponseDto, login, signUpAdmin } from '@immich/sdk';
import { loginDto, signupDto, uuidDto } from 'src/fixtures';
import { errorDto } from 'src/responses';
import { app, utils } from 'src/utils';
import request from 'supertest';
import { beforeAll, describe, expect, it } from 'vitest';
describe('/sync', () => {
let admin: LoginResponseDto;
beforeAll(async () => {
await utils.resetDatabase();
await signUpAdmin({ signUpDto: signupDto.admin });
admin = await login({ loginCredentialDto: loginDto.admin });
});
describe('GET /sync/acknowledge', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).post('/sync/acknowledge');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should work', async () => {
const { status } = await request(app)
.post('/sync/acknowledge')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({});
expect(status).toBe(204);
});
it('should work with an album sync date', async () => {
const { status } = await request(app)
.post('/sync/acknowledge')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({
album: {
id: uuidDto.dummy,
timestamp: '2024-10-23T21:01:07.732Z',
},
});
expect(status).toBe(204);
});
});
describe('GET /sync/stream', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).post('/sync/stream');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require at least type', async () => {
const { status, body } = await request(app)
.post('/sync/stream')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ types: [] });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest());
});
it('should require valid types', async () => {
const { status, body } = await request(app)
.post('/sync/stream')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ types: ['invalid'] });
expect(status).toBe(400);
expect(body).toEqual(
errorDto.badRequest([expect.stringContaining('each value in types must be one of the following values')]),
);
});
it('should accept a valid type', async () => {
const response = await request(app)
.post('/sync/stream')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ types: ['asset'] });
expect(response.status).toBe(200);
expect(response.get('Content-Type')).toBe('application/jsonlines+json; charset=utf-8');
expect(response.body).toEqual('');
});
});
});

View file

@ -2,6 +2,7 @@ export const uuidDto = {
invalid: 'invalid-uuid',
// valid uuid v4
notFound: '00000000-0000-4000-a000-000000000000',
dummy: '00000000-0000-4000-a000-000000000000',
};
const adminLoginDto = {

View file

@ -201,8 +201,10 @@ Class | Method | HTTP request | Description
*StacksApi* | [**getStack**](doc//StacksApi.md#getstack) | **GET** /stacks/{id} |
*StacksApi* | [**searchStacks**](doc//StacksApi.md#searchstacks) | **GET** /stacks |
*StacksApi* | [**updateStack**](doc//StacksApi.md#updatestack) | **PUT** /stacks/{id} |
*SyncApi* | [**ackSync**](doc//SyncApi.md#acksync) | **POST** /sync/acknowledge |
*SyncApi* | [**getDeltaSync**](doc//SyncApi.md#getdeltasync) | **POST** /sync/delta-sync |
*SyncApi* | [**getFullSyncForUser**](doc//SyncApi.md#getfullsyncforuser) | **POST** /sync/full-sync |
*SyncApi* | [**getSyncStream**](doc//SyncApi.md#getsyncstream) | **POST** /sync/stream |
*SystemConfigApi* | [**getConfig**](doc//SystemConfigApi.md#getconfig) | **GET** /system-config |
*SystemConfigApi* | [**getConfigDefaults**](doc//SystemConfigApi.md#getconfigdefaults) | **GET** /system-config/defaults |
*SystemConfigApi* | [**getStorageTemplateOptions**](doc//SystemConfigApi.md#getstoragetemplateoptions) | **GET** /system-config/storage-template-options |
@ -259,6 +261,7 @@ Class | Method | HTTP request | Description
- [ActivityStatisticsResponseDto](doc//ActivityStatisticsResponseDto.md)
- [AddUsersDto](doc//AddUsersDto.md)
- [AdminOnboardingUpdateDto](doc//AdminOnboardingUpdateDto.md)
- [AlbumAssetResponseDto](doc//AlbumAssetResponseDto.md)
- [AlbumResponseDto](doc//AlbumResponseDto.md)
- [AlbumStatisticsResponseDto](doc//AlbumStatisticsResponseDto.md)
- [AlbumUserAddDto](doc//AlbumUserAddDto.md)
@ -413,6 +416,13 @@ Class | Method | HTTP request | Description
- [StackCreateDto](doc//StackCreateDto.md)
- [StackResponseDto](doc//StackResponseDto.md)
- [StackUpdateDto](doc//StackUpdateDto.md)
- [SyncAcknowledgeDto](doc//SyncAcknowledgeDto.md)
- [SyncAction](doc//SyncAction.md)
- [SyncCheckpointDto](doc//SyncCheckpointDto.md)
- [SyncStreamDto](doc//SyncStreamDto.md)
- [SyncStreamResponseDto](doc//SyncStreamResponseDto.md)
- [SyncStreamResponseDtoData](doc//SyncStreamResponseDtoData.md)
- [SyncType](doc//SyncType.md)
- [SystemConfigDto](doc//SystemConfigDto.md)
- [SystemConfigFFmpegDto](doc//SystemConfigFFmpegDto.md)
- [SystemConfigFacesDto](doc//SystemConfigFacesDto.md)

View file

@ -73,6 +73,7 @@ part 'model/activity_response_dto.dart';
part 'model/activity_statistics_response_dto.dart';
part 'model/add_users_dto.dart';
part 'model/admin_onboarding_update_dto.dart';
part 'model/album_asset_response_dto.dart';
part 'model/album_response_dto.dart';
part 'model/album_statistics_response_dto.dart';
part 'model/album_user_add_dto.dart';
@ -227,6 +228,13 @@ part 'model/source_type.dart';
part 'model/stack_create_dto.dart';
part 'model/stack_response_dto.dart';
part 'model/stack_update_dto.dart';
part 'model/sync_acknowledge_dto.dart';
part 'model/sync_action.dart';
part 'model/sync_checkpoint_dto.dart';
part 'model/sync_stream_dto.dart';
part 'model/sync_stream_response_dto.dart';
part 'model/sync_stream_response_dto_data.dart';
part 'model/sync_type.dart';
part 'model/system_config_dto.dart';
part 'model/system_config_f_fmpeg_dto.dart';
part 'model/system_config_faces_dto.dart';

View file

@ -16,6 +16,45 @@ class SyncApi {
final ApiClient apiClient;
/// Performs an HTTP 'POST /sync/acknowledge' operation and returns the [Response].
/// Parameters:
///
/// * [SyncAcknowledgeDto] syncAcknowledgeDto (required):
Future<Response> ackSyncWithHttpInfo(SyncAcknowledgeDto syncAcknowledgeDto,) async {
// ignore: prefer_const_declarations
final path = r'/sync/acknowledge';
// ignore: prefer_final_locals
Object? postBody = syncAcknowledgeDto;
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:
///
/// * [SyncAcknowledgeDto] syncAcknowledgeDto (required):
Future<void> ackSync(SyncAcknowledgeDto syncAcknowledgeDto,) async {
final response = await ackSyncWithHttpInfo(syncAcknowledgeDto,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
}
/// Performs an HTTP 'POST /sync/delta-sync' operation and returns the [Response].
/// Parameters:
///
@ -112,4 +151,54 @@ class SyncApi {
}
return null;
}
/// Performs an HTTP 'POST /sync/stream' operation and returns the [Response].
/// Parameters:
///
/// * [SyncStreamDto] syncStreamDto (required):
Future<Response> getSyncStreamWithHttpInfo(SyncStreamDto syncStreamDto,) async {
// ignore: prefer_const_declarations
final path = r'/sync/stream';
// ignore: prefer_final_locals
Object? postBody = syncStreamDto;
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:
///
/// * [SyncStreamDto] syncStreamDto (required):
Future<List<SyncStreamResponseDto>?> getSyncStream(SyncStreamDto syncStreamDto,) async {
final response = await getSyncStreamWithHttpInfo(syncStreamDto,);
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<SyncStreamResponseDto>') as List)
.cast<SyncStreamResponseDto>()
.toList(growable: false);
}
return null;
}
}

View file

@ -200,6 +200,8 @@ class ApiClient {
return AddUsersDto.fromJson(value);
case 'AdminOnboardingUpdateDto':
return AdminOnboardingUpdateDto.fromJson(value);
case 'AlbumAssetResponseDto':
return AlbumAssetResponseDto.fromJson(value);
case 'AlbumResponseDto':
return AlbumResponseDto.fromJson(value);
case 'AlbumStatisticsResponseDto':
@ -508,6 +510,20 @@ class ApiClient {
return StackResponseDto.fromJson(value);
case 'StackUpdateDto':
return StackUpdateDto.fromJson(value);
case 'SyncAcknowledgeDto':
return SyncAcknowledgeDto.fromJson(value);
case 'SyncAction':
return SyncActionTypeTransformer().decode(value);
case 'SyncCheckpointDto':
return SyncCheckpointDto.fromJson(value);
case 'SyncStreamDto':
return SyncStreamDto.fromJson(value);
case 'SyncStreamResponseDto':
return SyncStreamResponseDto.fromJson(value);
case 'SyncStreamResponseDtoData':
return SyncStreamResponseDtoData.fromJson(value);
case 'SyncType':
return SyncTypeTypeTransformer().decode(value);
case 'SystemConfigDto':
return SystemConfigDto.fromJson(value);
case 'SystemConfigFFmpegDto':

View file

@ -130,6 +130,12 @@ String parameterToString(dynamic value) {
if (value is SourceType) {
return SourceTypeTypeTransformer().encode(value).toString();
}
if (value is SyncAction) {
return SyncActionTypeTransformer().encode(value).toString();
}
if (value is SyncType) {
return SyncTypeTypeTransformer().encode(value).toString();
}
if (value is TimeBucketSize) {
return TimeBucketSizeTypeTransformer().encode(value).toString();
}

View file

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

View file

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

View file

@ -0,0 +1,85 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class SyncAction {
/// Instantiate a new enum with the provided [value].
const SyncAction._(this.value);
/// The underlying value of this enum member.
final String value;
@override
String toString() => value;
String toJson() => value;
static const upsert = SyncAction._(r'upsert');
static const delete = SyncAction._(r'delete');
/// List of all possible values in this [enum][SyncAction].
static const values = <SyncAction>[
upsert,
delete,
];
static SyncAction? fromJson(dynamic value) => SyncActionTypeTransformer().decode(value);
static List<SyncAction> listFromJson(dynamic json, {bool growable = false,}) {
final result = <SyncAction>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = SyncAction.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
}
/// Transformation class that can [encode] an instance of [SyncAction] to String,
/// and [decode] dynamic data back to [SyncAction].
class SyncActionTypeTransformer {
factory SyncActionTypeTransformer() => _instance ??= const SyncActionTypeTransformer._();
const SyncActionTypeTransformer._();
String encode(SyncAction data) => data.value;
/// Decodes a [dynamic value][data] to a SyncAction.
///
/// 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.
SyncAction? decode(dynamic data, {bool allowNull = true}) {
if (data != null) {
switch (data) {
case r'upsert': return SyncAction.upsert;
case r'delete': return SyncAction.delete;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');
}
}
}
return null;
}
/// Singleton [SyncActionTypeTransformer] instance.
static SyncActionTypeTransformer? _instance;
}

View file

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

View file

@ -0,0 +1,209 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class SyncStreamDto {
/// Returns a new [SyncStreamDto] instance.
SyncStreamDto({
this.types = const [],
});
List<SyncStreamDtoTypesEnum> types;
@override
bool operator ==(Object other) => identical(this, other) || other is SyncStreamDto &&
_deepEquality.equals(other.types, types);
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(types.hashCode);
@override
String toString() => 'SyncStreamDto[types=$types]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'types'] = this.types;
return json;
}
/// Returns a new [SyncStreamDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static SyncStreamDto? fromJson(dynamic value) {
upgradeDto(value, "SyncStreamDto");
if (value is Map) {
final json = value.cast<String, dynamic>();
return SyncStreamDto(
types: SyncStreamDtoTypesEnum.listFromJson(json[r'types']),
);
}
return null;
}
static List<SyncStreamDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <SyncStreamDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = SyncStreamDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, SyncStreamDto> mapFromJson(dynamic json) {
final map = <String, SyncStreamDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = SyncStreamDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of SyncStreamDto-objects as value to a dart map
static Map<String, List<SyncStreamDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<SyncStreamDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = SyncStreamDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'types',
};
}
class SyncStreamDtoTypesEnum {
/// Instantiate a new enum with the provided [value].
const SyncStreamDtoTypesEnum._(this.value);
/// The underlying value of this enum member.
final String value;
@override
String toString() => value;
String toJson() => value;
static const asset = SyncStreamDtoTypesEnum._(r'asset');
static const assetPeriodPartner = SyncStreamDtoTypesEnum._(r'asset.partner');
static const assetAlbum = SyncStreamDtoTypesEnum._(r'assetAlbum');
static const album = SyncStreamDtoTypesEnum._(r'album');
static const albumAsset = SyncStreamDtoTypesEnum._(r'albumAsset');
static const albumUser = SyncStreamDtoTypesEnum._(r'albumUser');
static const activity = SyncStreamDtoTypesEnum._(r'activity');
static const memory = SyncStreamDtoTypesEnum._(r'memory');
static const partner = SyncStreamDtoTypesEnum._(r'partner');
static const person = SyncStreamDtoTypesEnum._(r'person');
static const sharedLink = SyncStreamDtoTypesEnum._(r'sharedLink');
static const stack = SyncStreamDtoTypesEnum._(r'stack');
static const tag = SyncStreamDtoTypesEnum._(r'tag');
static const user = SyncStreamDtoTypesEnum._(r'user');
/// List of all possible values in this [enum][SyncStreamDtoTypesEnum].
static const values = <SyncStreamDtoTypesEnum>[
asset,
assetPeriodPartner,
assetAlbum,
album,
albumAsset,
albumUser,
activity,
memory,
partner,
person,
sharedLink,
stack,
tag,
user,
];
static SyncStreamDtoTypesEnum? fromJson(dynamic value) => SyncStreamDtoTypesEnumTypeTransformer().decode(value);
static List<SyncStreamDtoTypesEnum> listFromJson(dynamic json, {bool growable = false,}) {
final result = <SyncStreamDtoTypesEnum>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = SyncStreamDtoTypesEnum.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
}
/// Transformation class that can [encode] an instance of [SyncStreamDtoTypesEnum] to String,
/// and [decode] dynamic data back to [SyncStreamDtoTypesEnum].
class SyncStreamDtoTypesEnumTypeTransformer {
factory SyncStreamDtoTypesEnumTypeTransformer() => _instance ??= const SyncStreamDtoTypesEnumTypeTransformer._();
const SyncStreamDtoTypesEnumTypeTransformer._();
String encode(SyncStreamDtoTypesEnum data) => data.value;
/// Decodes a [dynamic value][data] to a SyncStreamDtoTypesEnum.
///
/// 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.
SyncStreamDtoTypesEnum? decode(dynamic data, {bool allowNull = true}) {
if (data != null) {
switch (data) {
case r'asset': return SyncStreamDtoTypesEnum.asset;
case r'asset.partner': return SyncStreamDtoTypesEnum.assetPeriodPartner;
case r'assetAlbum': return SyncStreamDtoTypesEnum.assetAlbum;
case r'album': return SyncStreamDtoTypesEnum.album;
case r'albumAsset': return SyncStreamDtoTypesEnum.albumAsset;
case r'albumUser': return SyncStreamDtoTypesEnum.albumUser;
case r'activity': return SyncStreamDtoTypesEnum.activity;
case r'memory': return SyncStreamDtoTypesEnum.memory;
case r'partner': return SyncStreamDtoTypesEnum.partner;
case r'person': return SyncStreamDtoTypesEnum.person;
case r'sharedLink': return SyncStreamDtoTypesEnum.sharedLink;
case r'stack': return SyncStreamDtoTypesEnum.stack;
case r'tag': return SyncStreamDtoTypesEnum.tag;
case r'user': return SyncStreamDtoTypesEnum.user;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');
}
}
}
return null;
}
/// Singleton [SyncStreamDtoTypesEnumTypeTransformer] instance.
static SyncStreamDtoTypesEnumTypeTransformer? _instance;
}

View file

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

View file

@ -0,0 +1,830 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class SyncStreamResponseDtoData {
/// Returns a new [SyncStreamResponseDtoData] instance.
SyncStreamResponseDtoData({
required this.checksum,
required this.deviceAssetId,
required this.deviceId,
this.duplicateId,
required this.duration,
this.exifInfo,
required this.fileCreatedAt,
required this.fileModifiedAt,
required this.hasMetadata,
required this.id,
required this.isArchived,
required this.isFavorite,
required this.isOffline,
required this.isTrashed,
this.libraryId,
this.livePhotoVideoId,
required this.localDateTime,
required this.originalFileName,
this.originalMimeType,
required this.originalPath,
required this.owner,
required this.ownerId,
this.people = const [],
this.resized,
this.smartInfo,
this.stack,
this.tags = const [],
required this.thumbhash,
required this.type,
this.unassignedFaces = const [],
required this.updatedAt,
required this.albumName,
required this.albumThumbnailAssetId,
this.albumUsers = const [],
required this.assetCount,
this.assets = const [],
required this.createdAt,
required this.description,
this.endDate,
required this.hasSharedLink,
required this.isActivityEnabled,
this.lastModifiedAssetTimestamp,
this.order,
required this.shared,
this.startDate,
required this.albumId,
required this.assetId,
this.comment,
required this.user,
required this.data,
this.deletedAt,
required this.isSaved,
required this.memoryAt,
this.seenAt,
required this.avatarColor,
required this.email,
this.inTimeline,
required this.name,
required this.profileChangedAt,
required this.profileImagePath,
required this.birthDate,
required this.isHidden,
required this.thumbnailPath,
this.album,
required this.allowDownload,
required this.allowUpload,
required this.expiresAt,
required this.key,
required this.password,
required this.showMetadata,
this.token,
required this.userId,
required this.primaryAssetId,
});
/// base64 encoded sha1 hash
String checksum;
String deviceAssetId;
String deviceId;
String? duplicateId;
String duration;
///
/// 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.
///
ExifResponseDto? exifInfo;
DateTime fileCreatedAt;
DateTime fileModifiedAt;
bool hasMetadata;
String id;
bool isArchived;
bool isFavorite;
bool isOffline;
bool isTrashed;
/// This property was deprecated in v1.106.0
String? libraryId;
String? livePhotoVideoId;
DateTime localDateTime;
String originalFileName;
///
/// 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? originalMimeType;
String originalPath;
UserResponseDto owner;
String ownerId;
List<PersonWithFacesResponseDto> people;
/// This property was deprecated in v1.113.0
///
/// 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.
///
bool? resized;
///
/// 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.
///
SmartInfoResponseDto? smartInfo;
AssetStackResponseDto? stack;
List<TagResponseDto> tags;
String? thumbhash;
SharedLinkType type;
List<AssetFaceWithoutPersonResponseDto> unassignedFaces;
/// This property was added in v1.107.0
DateTime updatedAt;
String albumName;
String? albumThumbnailAssetId;
List<AlbumUserResponseDto> albumUsers;
int assetCount;
List<AssetResponseDto> assets;
DateTime createdAt;
String? description;
///
/// 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.
///
DateTime? endDate;
bool hasSharedLink;
bool isActivityEnabled;
///
/// 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.
///
DateTime? lastModifiedAssetTimestamp;
///
/// 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.
///
AssetOrder? order;
bool shared;
///
/// 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.
///
DateTime? startDate;
String albumId;
String? assetId;
String? comment;
UserResponseDto user;
OnThisDayDto data;
///
/// 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.
///
DateTime? deletedAt;
bool isSaved;
DateTime memoryAt;
///
/// 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.
///
DateTime? seenAt;
UserAvatarColor avatarColor;
String email;
///
/// 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.
///
bool? inTimeline;
String name;
DateTime profileChangedAt;
String profileImagePath;
DateTime? birthDate;
bool isHidden;
String thumbnailPath;
///
/// 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.
///
AlbumResponseDto? album;
bool allowDownload;
bool allowUpload;
DateTime? expiresAt;
String key;
String? password;
bool showMetadata;
String? token;
String userId;
String primaryAssetId;
@override
bool operator ==(Object other) => identical(this, other) || other is SyncStreamResponseDtoData &&
other.checksum == checksum &&
other.deviceAssetId == deviceAssetId &&
other.deviceId == deviceId &&
other.duplicateId == duplicateId &&
other.duration == duration &&
other.exifInfo == exifInfo &&
other.fileCreatedAt == fileCreatedAt &&
other.fileModifiedAt == fileModifiedAt &&
other.hasMetadata == hasMetadata &&
other.id == id &&
other.isArchived == isArchived &&
other.isFavorite == isFavorite &&
other.isOffline == isOffline &&
other.isTrashed == isTrashed &&
other.libraryId == libraryId &&
other.livePhotoVideoId == livePhotoVideoId &&
other.localDateTime == localDateTime &&
other.originalFileName == originalFileName &&
other.originalMimeType == originalMimeType &&
other.originalPath == originalPath &&
other.owner == owner &&
other.ownerId == ownerId &&
_deepEquality.equals(other.people, people) &&
other.resized == resized &&
other.smartInfo == smartInfo &&
other.stack == stack &&
_deepEquality.equals(other.tags, tags) &&
other.thumbhash == thumbhash &&
other.type == type &&
_deepEquality.equals(other.unassignedFaces, unassignedFaces) &&
other.updatedAt == updatedAt &&
other.albumName == albumName &&
other.albumThumbnailAssetId == albumThumbnailAssetId &&
_deepEquality.equals(other.albumUsers, albumUsers) &&
other.assetCount == assetCount &&
_deepEquality.equals(other.assets, assets) &&
other.createdAt == createdAt &&
other.description == description &&
other.endDate == endDate &&
other.hasSharedLink == hasSharedLink &&
other.isActivityEnabled == isActivityEnabled &&
other.lastModifiedAssetTimestamp == lastModifiedAssetTimestamp &&
other.order == order &&
other.shared == shared &&
other.startDate == startDate &&
other.albumId == albumId &&
other.assetId == assetId &&
other.comment == comment &&
other.user == user &&
other.data == data &&
other.deletedAt == deletedAt &&
other.isSaved == isSaved &&
other.memoryAt == memoryAt &&
other.seenAt == seenAt &&
other.avatarColor == avatarColor &&
other.email == email &&
other.inTimeline == inTimeline &&
other.name == name &&
other.profileChangedAt == profileChangedAt &&
other.profileImagePath == profileImagePath &&
other.birthDate == birthDate &&
other.isHidden == isHidden &&
other.thumbnailPath == thumbnailPath &&
other.album == album &&
other.allowDownload == allowDownload &&
other.allowUpload == allowUpload &&
other.expiresAt == expiresAt &&
other.key == key &&
other.password == password &&
other.showMetadata == showMetadata &&
other.token == token &&
other.userId == userId &&
other.primaryAssetId == primaryAssetId;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(checksum.hashCode) +
(deviceAssetId.hashCode) +
(deviceId.hashCode) +
(duplicateId == null ? 0 : duplicateId!.hashCode) +
(duration.hashCode) +
(exifInfo == null ? 0 : exifInfo!.hashCode) +
(fileCreatedAt.hashCode) +
(fileModifiedAt.hashCode) +
(hasMetadata.hashCode) +
(id.hashCode) +
(isArchived.hashCode) +
(isFavorite.hashCode) +
(isOffline.hashCode) +
(isTrashed.hashCode) +
(libraryId == null ? 0 : libraryId!.hashCode) +
(livePhotoVideoId == null ? 0 : livePhotoVideoId!.hashCode) +
(localDateTime.hashCode) +
(originalFileName.hashCode) +
(originalMimeType == null ? 0 : originalMimeType!.hashCode) +
(originalPath.hashCode) +
(owner.hashCode) +
(ownerId.hashCode) +
(people.hashCode) +
(resized == null ? 0 : resized!.hashCode) +
(smartInfo == null ? 0 : smartInfo!.hashCode) +
(stack == null ? 0 : stack!.hashCode) +
(tags.hashCode) +
(thumbhash == null ? 0 : thumbhash!.hashCode) +
(type.hashCode) +
(unassignedFaces.hashCode) +
(updatedAt.hashCode) +
(albumName.hashCode) +
(albumThumbnailAssetId == null ? 0 : albumThumbnailAssetId!.hashCode) +
(albumUsers.hashCode) +
(assetCount.hashCode) +
(assets.hashCode) +
(createdAt.hashCode) +
(description == null ? 0 : description!.hashCode) +
(endDate == null ? 0 : endDate!.hashCode) +
(hasSharedLink.hashCode) +
(isActivityEnabled.hashCode) +
(lastModifiedAssetTimestamp == null ? 0 : lastModifiedAssetTimestamp!.hashCode) +
(order == null ? 0 : order!.hashCode) +
(shared.hashCode) +
(startDate == null ? 0 : startDate!.hashCode) +
(albumId.hashCode) +
(assetId == null ? 0 : assetId!.hashCode) +
(comment == null ? 0 : comment!.hashCode) +
(user.hashCode) +
(data.hashCode) +
(deletedAt == null ? 0 : deletedAt!.hashCode) +
(isSaved.hashCode) +
(memoryAt.hashCode) +
(seenAt == null ? 0 : seenAt!.hashCode) +
(avatarColor.hashCode) +
(email.hashCode) +
(inTimeline == null ? 0 : inTimeline!.hashCode) +
(name.hashCode) +
(profileChangedAt.hashCode) +
(profileImagePath.hashCode) +
(birthDate == null ? 0 : birthDate!.hashCode) +
(isHidden.hashCode) +
(thumbnailPath.hashCode) +
(album == null ? 0 : album!.hashCode) +
(allowDownload.hashCode) +
(allowUpload.hashCode) +
(expiresAt == null ? 0 : expiresAt!.hashCode) +
(key.hashCode) +
(password == null ? 0 : password!.hashCode) +
(showMetadata.hashCode) +
(token == null ? 0 : token!.hashCode) +
(userId.hashCode) +
(primaryAssetId.hashCode);
@override
String toString() => 'SyncStreamResponseDtoData[checksum=$checksum, deviceAssetId=$deviceAssetId, deviceId=$deviceId, duplicateId=$duplicateId, duration=$duration, exifInfo=$exifInfo, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, hasMetadata=$hasMetadata, id=$id, isArchived=$isArchived, isFavorite=$isFavorite, isOffline=$isOffline, isTrashed=$isTrashed, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, originalMimeType=$originalMimeType, originalPath=$originalPath, owner=$owner, ownerId=$ownerId, people=$people, resized=$resized, smartInfo=$smartInfo, stack=$stack, tags=$tags, thumbhash=$thumbhash, type=$type, unassignedFaces=$unassignedFaces, updatedAt=$updatedAt, albumName=$albumName, albumThumbnailAssetId=$albumThumbnailAssetId, albumUsers=$albumUsers, assetCount=$assetCount, assets=$assets, createdAt=$createdAt, description=$description, endDate=$endDate, hasSharedLink=$hasSharedLink, isActivityEnabled=$isActivityEnabled, lastModifiedAssetTimestamp=$lastModifiedAssetTimestamp, order=$order, shared=$shared, startDate=$startDate, albumId=$albumId, assetId=$assetId, comment=$comment, user=$user, data=$data, deletedAt=$deletedAt, isSaved=$isSaved, memoryAt=$memoryAt, seenAt=$seenAt, avatarColor=$avatarColor, email=$email, inTimeline=$inTimeline, name=$name, profileChangedAt=$profileChangedAt, profileImagePath=$profileImagePath, birthDate=$birthDate, isHidden=$isHidden, thumbnailPath=$thumbnailPath, album=$album, allowDownload=$allowDownload, allowUpload=$allowUpload, expiresAt=$expiresAt, key=$key, password=$password, showMetadata=$showMetadata, token=$token, userId=$userId, primaryAssetId=$primaryAssetId]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'checksum'] = this.checksum;
json[r'deviceAssetId'] = this.deviceAssetId;
json[r'deviceId'] = this.deviceId;
if (this.duplicateId != null) {
json[r'duplicateId'] = this.duplicateId;
} else {
// json[r'duplicateId'] = null;
}
json[r'duration'] = this.duration;
if (this.exifInfo != null) {
json[r'exifInfo'] = this.exifInfo;
} else {
// json[r'exifInfo'] = null;
}
json[r'fileCreatedAt'] = this.fileCreatedAt.toUtc().toIso8601String();
json[r'fileModifiedAt'] = this.fileModifiedAt.toUtc().toIso8601String();
json[r'hasMetadata'] = this.hasMetadata;
json[r'id'] = this.id;
json[r'isArchived'] = this.isArchived;
json[r'isFavorite'] = this.isFavorite;
json[r'isOffline'] = this.isOffline;
json[r'isTrashed'] = this.isTrashed;
if (this.libraryId != null) {
json[r'libraryId'] = this.libraryId;
} else {
// json[r'libraryId'] = null;
}
if (this.livePhotoVideoId != null) {
json[r'livePhotoVideoId'] = this.livePhotoVideoId;
} else {
// json[r'livePhotoVideoId'] = null;
}
json[r'localDateTime'] = this.localDateTime.toUtc().toIso8601String();
json[r'originalFileName'] = this.originalFileName;
if (this.originalMimeType != null) {
json[r'originalMimeType'] = this.originalMimeType;
} else {
// json[r'originalMimeType'] = null;
}
json[r'originalPath'] = this.originalPath;
json[r'owner'] = this.owner;
json[r'ownerId'] = this.ownerId;
json[r'people'] = this.people;
if (this.resized != null) {
json[r'resized'] = this.resized;
} else {
// json[r'resized'] = null;
}
if (this.smartInfo != null) {
json[r'smartInfo'] = this.smartInfo;
} else {
// json[r'smartInfo'] = null;
}
if (this.stack != null) {
json[r'stack'] = this.stack;
} else {
// json[r'stack'] = null;
}
json[r'tags'] = this.tags;
if (this.thumbhash != null) {
json[r'thumbhash'] = this.thumbhash;
} else {
// json[r'thumbhash'] = null;
}
json[r'type'] = this.type;
json[r'unassignedFaces'] = this.unassignedFaces;
json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String();
json[r'albumName'] = this.albumName;
if (this.albumThumbnailAssetId != null) {
json[r'albumThumbnailAssetId'] = this.albumThumbnailAssetId;
} else {
// json[r'albumThumbnailAssetId'] = null;
}
json[r'albumUsers'] = this.albumUsers;
json[r'assetCount'] = this.assetCount;
json[r'assets'] = this.assets;
json[r'createdAt'] = this.createdAt.toUtc().toIso8601String();
if (this.description != null) {
json[r'description'] = this.description;
} else {
// json[r'description'] = null;
}
if (this.endDate != null) {
json[r'endDate'] = this.endDate!.toUtc().toIso8601String();
} else {
// json[r'endDate'] = null;
}
json[r'hasSharedLink'] = this.hasSharedLink;
json[r'isActivityEnabled'] = this.isActivityEnabled;
if (this.lastModifiedAssetTimestamp != null) {
json[r'lastModifiedAssetTimestamp'] = this.lastModifiedAssetTimestamp!.toUtc().toIso8601String();
} else {
// json[r'lastModifiedAssetTimestamp'] = null;
}
if (this.order != null) {
json[r'order'] = this.order;
} else {
// json[r'order'] = null;
}
json[r'shared'] = this.shared;
if (this.startDate != null) {
json[r'startDate'] = this.startDate!.toUtc().toIso8601String();
} else {
// json[r'startDate'] = null;
}
json[r'albumId'] = this.albumId;
if (this.assetId != null) {
json[r'assetId'] = this.assetId;
} else {
// json[r'assetId'] = null;
}
if (this.comment != null) {
json[r'comment'] = this.comment;
} else {
// json[r'comment'] = null;
}
json[r'user'] = this.user;
json[r'data'] = this.data;
if (this.deletedAt != null) {
json[r'deletedAt'] = this.deletedAt!.toUtc().toIso8601String();
} else {
// json[r'deletedAt'] = null;
}
json[r'isSaved'] = this.isSaved;
json[r'memoryAt'] = this.memoryAt.toUtc().toIso8601String();
if (this.seenAt != null) {
json[r'seenAt'] = this.seenAt!.toUtc().toIso8601String();
} else {
// json[r'seenAt'] = null;
}
json[r'avatarColor'] = this.avatarColor;
json[r'email'] = this.email;
if (this.inTimeline != null) {
json[r'inTimeline'] = this.inTimeline;
} else {
// json[r'inTimeline'] = null;
}
json[r'name'] = this.name;
json[r'profileChangedAt'] = this.profileChangedAt.toUtc().toIso8601String();
json[r'profileImagePath'] = this.profileImagePath;
if (this.birthDate != null) {
json[r'birthDate'] = _dateFormatter.format(this.birthDate!.toUtc());
} else {
// json[r'birthDate'] = null;
}
json[r'isHidden'] = this.isHidden;
json[r'thumbnailPath'] = this.thumbnailPath;
if (this.album != null) {
json[r'album'] = this.album;
} else {
// json[r'album'] = null;
}
json[r'allowDownload'] = this.allowDownload;
json[r'allowUpload'] = this.allowUpload;
if (this.expiresAt != null) {
json[r'expiresAt'] = this.expiresAt!.toUtc().toIso8601String();
} else {
// json[r'expiresAt'] = null;
}
json[r'key'] = this.key;
if (this.password != null) {
json[r'password'] = this.password;
} else {
// json[r'password'] = null;
}
json[r'showMetadata'] = this.showMetadata;
if (this.token != null) {
json[r'token'] = this.token;
} else {
// json[r'token'] = null;
}
json[r'userId'] = this.userId;
json[r'primaryAssetId'] = this.primaryAssetId;
return json;
}
/// Returns a new [SyncStreamResponseDtoData] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static SyncStreamResponseDtoData? fromJson(dynamic value) {
upgradeDto(value, "SyncStreamResponseDtoData");
if (value is Map) {
final json = value.cast<String, dynamic>();
return SyncStreamResponseDtoData(
checksum: mapValueOfType<String>(json, r'checksum')!,
deviceAssetId: mapValueOfType<String>(json, r'deviceAssetId')!,
deviceId: mapValueOfType<String>(json, r'deviceId')!,
duplicateId: mapValueOfType<String>(json, r'duplicateId'),
duration: mapValueOfType<String>(json, r'duration')!,
exifInfo: ExifResponseDto.fromJson(json[r'exifInfo']),
fileCreatedAt: mapDateTime(json, r'fileCreatedAt', r'')!,
fileModifiedAt: mapDateTime(json, r'fileModifiedAt', r'')!,
hasMetadata: mapValueOfType<bool>(json, r'hasMetadata')!,
id: mapValueOfType<String>(json, r'id')!,
isArchived: mapValueOfType<bool>(json, r'isArchived')!,
isFavorite: mapValueOfType<bool>(json, r'isFavorite')!,
isOffline: mapValueOfType<bool>(json, r'isOffline')!,
isTrashed: mapValueOfType<bool>(json, r'isTrashed')!,
libraryId: mapValueOfType<String>(json, r'libraryId'),
livePhotoVideoId: mapValueOfType<String>(json, r'livePhotoVideoId'),
localDateTime: mapDateTime(json, r'localDateTime', r'')!,
originalFileName: mapValueOfType<String>(json, r'originalFileName')!,
originalMimeType: mapValueOfType<String>(json, r'originalMimeType'),
originalPath: mapValueOfType<String>(json, r'originalPath')!,
owner: UserResponseDto.fromJson(json[r'owner'])!,
ownerId: mapValueOfType<String>(json, r'ownerId')!,
people: PersonWithFacesResponseDto.listFromJson(json[r'people']),
resized: mapValueOfType<bool>(json, r'resized'),
smartInfo: SmartInfoResponseDto.fromJson(json[r'smartInfo']),
stack: AssetStackResponseDto.fromJson(json[r'stack']),
tags: TagResponseDto.listFromJson(json[r'tags']),
thumbhash: mapValueOfType<String>(json, r'thumbhash'),
type: SharedLinkType.fromJson(json[r'type'])!,
unassignedFaces: AssetFaceWithoutPersonResponseDto.listFromJson(json[r'unassignedFaces']),
updatedAt: mapDateTime(json, r'updatedAt', r'')!,
albumName: mapValueOfType<String>(json, r'albumName')!,
albumThumbnailAssetId: mapValueOfType<String>(json, r'albumThumbnailAssetId'),
albumUsers: AlbumUserResponseDto.listFromJson(json[r'albumUsers']),
assetCount: mapValueOfType<int>(json, r'assetCount')!,
assets: AssetResponseDto.listFromJson(json[r'assets']),
createdAt: mapDateTime(json, r'createdAt', r'')!,
description: mapValueOfType<String>(json, r'description'),
endDate: mapDateTime(json, r'endDate', r''),
hasSharedLink: mapValueOfType<bool>(json, r'hasSharedLink')!,
isActivityEnabled: mapValueOfType<bool>(json, r'isActivityEnabled')!,
lastModifiedAssetTimestamp: mapDateTime(json, r'lastModifiedAssetTimestamp', r''),
order: AssetOrder.fromJson(json[r'order']),
shared: mapValueOfType<bool>(json, r'shared')!,
startDate: mapDateTime(json, r'startDate', r''),
albumId: mapValueOfType<String>(json, r'albumId')!,
assetId: mapValueOfType<String>(json, r'assetId'),
comment: mapValueOfType<String>(json, r'comment'),
user: UserResponseDto.fromJson(json[r'user'])!,
data: OnThisDayDto.fromJson(json[r'data'])!,
deletedAt: mapDateTime(json, r'deletedAt', r''),
isSaved: mapValueOfType<bool>(json, r'isSaved')!,
memoryAt: mapDateTime(json, r'memoryAt', r'')!,
seenAt: mapDateTime(json, r'seenAt', r''),
avatarColor: UserAvatarColor.fromJson(json[r'avatarColor'])!,
email: mapValueOfType<String>(json, r'email')!,
inTimeline: mapValueOfType<bool>(json, r'inTimeline'),
name: mapValueOfType<String>(json, r'name')!,
profileChangedAt: mapDateTime(json, r'profileChangedAt', r'')!,
profileImagePath: mapValueOfType<String>(json, r'profileImagePath')!,
birthDate: mapDateTime(json, r'birthDate', r''),
isHidden: mapValueOfType<bool>(json, r'isHidden')!,
thumbnailPath: mapValueOfType<String>(json, r'thumbnailPath')!,
album: AlbumResponseDto.fromJson(json[r'album']),
allowDownload: mapValueOfType<bool>(json, r'allowDownload')!,
allowUpload: mapValueOfType<bool>(json, r'allowUpload')!,
expiresAt: mapDateTime(json, r'expiresAt', r''),
key: mapValueOfType<String>(json, r'key')!,
password: mapValueOfType<String>(json, r'password'),
showMetadata: mapValueOfType<bool>(json, r'showMetadata')!,
token: mapValueOfType<String>(json, r'token'),
userId: mapValueOfType<String>(json, r'userId')!,
primaryAssetId: mapValueOfType<String>(json, r'primaryAssetId')!,
);
}
return null;
}
static List<SyncStreamResponseDtoData> listFromJson(dynamic json, {bool growable = false,}) {
final result = <SyncStreamResponseDtoData>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = SyncStreamResponseDtoData.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, SyncStreamResponseDtoData> mapFromJson(dynamic json) {
final map = <String, SyncStreamResponseDtoData>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = SyncStreamResponseDtoData.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of SyncStreamResponseDtoData-objects as value to a dart map
static Map<String, List<SyncStreamResponseDtoData>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<SyncStreamResponseDtoData>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = SyncStreamResponseDtoData.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'checksum',
'deviceAssetId',
'deviceId',
'duration',
'fileCreatedAt',
'fileModifiedAt',
'hasMetadata',
'id',
'isArchived',
'isFavorite',
'isOffline',
'isTrashed',
'localDateTime',
'originalFileName',
'originalPath',
'owner',
'ownerId',
'thumbhash',
'type',
'updatedAt',
'albumName',
'albumThumbnailAssetId',
'albumUsers',
'assetCount',
'assets',
'createdAt',
'description',
'hasSharedLink',
'isActivityEnabled',
'shared',
'albumId',
'assetId',
'user',
'data',
'isSaved',
'memoryAt',
'avatarColor',
'email',
'name',
'profileChangedAt',
'profileImagePath',
'birthDate',
'isHidden',
'thumbnailPath',
'allowDownload',
'allowUpload',
'expiresAt',
'key',
'password',
'showMetadata',
'userId',
'primaryAssetId',
};
}

121
mobile/openapi/lib/model/sync_type.dart generated Normal file
View file

@ -0,0 +1,121 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class SyncType {
/// Instantiate a new enum with the provided [value].
const SyncType._(this.value);
/// The underlying value of this enum member.
final String value;
@override
String toString() => value;
String toJson() => value;
static const asset = SyncType._(r'asset');
static const assetPeriodPartner = SyncType._(r'asset.partner');
static const assetAlbum = SyncType._(r'assetAlbum');
static const album = SyncType._(r'album');
static const albumAsset = SyncType._(r'albumAsset');
static const albumUser = SyncType._(r'albumUser');
static const activity = SyncType._(r'activity');
static const memory = SyncType._(r'memory');
static const partner = SyncType._(r'partner');
static const person = SyncType._(r'person');
static const sharedLink = SyncType._(r'sharedLink');
static const stack = SyncType._(r'stack');
static const tag = SyncType._(r'tag');
static const user = SyncType._(r'user');
/// List of all possible values in this [enum][SyncType].
static const values = <SyncType>[
asset,
assetPeriodPartner,
assetAlbum,
album,
albumAsset,
albumUser,
activity,
memory,
partner,
person,
sharedLink,
stack,
tag,
user,
];
static SyncType? fromJson(dynamic value) => SyncTypeTypeTransformer().decode(value);
static List<SyncType> listFromJson(dynamic json, {bool growable = false,}) {
final result = <SyncType>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = SyncType.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
}
/// Transformation class that can [encode] an instance of [SyncType] to String,
/// and [decode] dynamic data back to [SyncType].
class SyncTypeTypeTransformer {
factory SyncTypeTypeTransformer() => _instance ??= const SyncTypeTypeTransformer._();
const SyncTypeTypeTransformer._();
String encode(SyncType data) => data.value;
/// Decodes a [dynamic value][data] to a SyncType.
///
/// 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.
SyncType? decode(dynamic data, {bool allowNull = true}) {
if (data != null) {
switch (data) {
case r'asset': return SyncType.asset;
case r'asset.partner': return SyncType.assetPeriodPartner;
case r'assetAlbum': return SyncType.assetAlbum;
case r'album': return SyncType.album;
case r'albumAsset': return SyncType.albumAsset;
case r'albumUser': return SyncType.albumUser;
case r'activity': return SyncType.activity;
case r'memory': return SyncType.memory;
case r'partner': return SyncType.partner;
case r'person': return SyncType.person;
case r'sharedLink': return SyncType.sharedLink;
case r'stack': return SyncType.stack;
case r'tag': return SyncType.tag;
case r'user': return SyncType.user;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');
}
}
}
return null;
}
/// Singleton [SyncTypeTypeTransformer] instance.
static SyncTypeTypeTransformer? _instance;
}

View file

@ -5778,6 +5778,41 @@
]
}
},
"/sync/acknowledge": {
"post": {
"operationId": "ackSync",
"parameters": [],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SyncAcknowledgeDto"
}
}
},
"required": true
},
"responses": {
"204": {
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"tags": [
"Sync"
]
}
},
"/sync/delta-sync": {
"post": {
"operationId": "getDeltaSync",
@ -5865,6 +5900,51 @@
]
}
},
"/sync/stream": {
"post": {
"operationId": "getSyncStream",
"parameters": [],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SyncStreamDto"
}
}
},
"required": true
},
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"items": {
"$ref": "#/components/schemas/SyncStreamResponseDto"
},
"type": "array"
}
}
},
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"tags": [
"Sync"
]
}
},
"/system-config": {
"get": {
"operationId": "getConfig",
@ -7581,6 +7661,23 @@
],
"type": "object"
},
"AlbumAssetResponseDto": {
"properties": {
"albumId": {
"format": "uuid",
"type": "string"
},
"assetId": {
"format": "uuid",
"type": "string"
}
},
"required": [
"albumId",
"assetId"
],
"type": "object"
},
"AlbumResponseDto": {
"properties": {
"albumName": {
@ -11456,6 +11553,175 @@
},
"type": "object"
},
"SyncAcknowledgeDto": {
"properties": {
"activity": {
"$ref": "#/components/schemas/SyncCheckpointDto"
},
"album": {
"$ref": "#/components/schemas/SyncCheckpointDto"
},
"albumAsset": {
"$ref": "#/components/schemas/SyncCheckpointDto"
},
"albumUser": {
"$ref": "#/components/schemas/SyncCheckpointDto"
},
"asset": {
"$ref": "#/components/schemas/SyncCheckpointDto"
},
"assetAlbum": {
"$ref": "#/components/schemas/SyncCheckpointDto"
},
"assetPartner": {
"$ref": "#/components/schemas/SyncCheckpointDto"
},
"memory": {
"$ref": "#/components/schemas/SyncCheckpointDto"
},
"partner": {
"$ref": "#/components/schemas/SyncCheckpointDto"
},
"person": {
"$ref": "#/components/schemas/SyncCheckpointDto"
},
"sharedLink": {
"$ref": "#/components/schemas/SyncCheckpointDto"
},
"stack": {
"$ref": "#/components/schemas/SyncCheckpointDto"
},
"tag": {
"$ref": "#/components/schemas/SyncCheckpointDto"
},
"user": {
"$ref": "#/components/schemas/SyncCheckpointDto"
}
},
"type": "object"
},
"SyncAction": {
"enum": [
"upsert",
"delete"
],
"type": "string"
},
"SyncCheckpointDto": {
"properties": {
"id": {
"format": "uuid",
"type": "string"
},
"timestamp": {
"type": "string"
}
},
"required": [
"id",
"timestamp"
],
"type": "object"
},
"SyncStreamDto": {
"properties": {
"types": {
"items": {
"enum": [
"asset",
"asset.partner",
"assetAlbum",
"album",
"albumAsset",
"albumUser",
"activity",
"memory",
"partner",
"person",
"sharedLink",
"stack",
"tag",
"user"
],
"type": "string"
},
"type": "array"
}
},
"required": [
"types"
],
"type": "object"
},
"SyncStreamResponseDto": {
"properties": {
"action": {
"$ref": "#/components/schemas/SyncAction"
},
"data": {
"anyOf": [
{
"$ref": "#/components/schemas/AssetResponseDto"
},
{
"$ref": "#/components/schemas/AlbumResponseDto"
},
{
"$ref": "#/components/schemas/AlbumAssetResponseDto"
},
{
"$ref": "#/components/schemas/ActivityResponseDto"
},
{
"$ref": "#/components/schemas/MemoryResponseDto"
},
{
"$ref": "#/components/schemas/PartnerResponseDto"
},
{
"$ref": "#/components/schemas/PersonResponseDto"
},
{
"$ref": "#/components/schemas/SharedLinkResponseDto"
},
{
"$ref": "#/components/schemas/StackResponseDto"
},
{
"$ref": "#/components/schemas/UserResponseDto"
}
]
},
"type": {
"$ref": "#/components/schemas/SyncType"
}
},
"required": [
"action",
"data",
"type"
],
"type": "object"
},
"SyncType": {
"enum": [
"asset",
"asset.partner",
"assetAlbum",
"album",
"albumAsset",
"albumUser",
"activity",
"memory",
"partner",
"person",
"sharedLink",
"stack",
"tag",
"user"
],
"type": "string"
},
"SystemConfigDto": {
"properties": {
"ffmpeg": {

View file

@ -1069,6 +1069,26 @@ export type StackCreateDto = {
export type StackUpdateDto = {
primaryAssetId?: string;
};
export type SyncCheckpointDto = {
id: string;
timestamp: string;
};
export type SyncAcknowledgeDto = {
activity?: SyncCheckpointDto;
album?: SyncCheckpointDto;
albumAsset?: SyncCheckpointDto;
albumUser?: SyncCheckpointDto;
asset?: SyncCheckpointDto;
assetAlbum?: SyncCheckpointDto;
assetPartner?: SyncCheckpointDto;
memory?: SyncCheckpointDto;
partner?: SyncCheckpointDto;
person?: SyncCheckpointDto;
sharedLink?: SyncCheckpointDto;
stack?: SyncCheckpointDto;
tag?: SyncCheckpointDto;
user?: SyncCheckpointDto;
};
export type AssetDeltaSyncDto = {
updatedAfter: string;
userIds: string[];
@ -1084,6 +1104,18 @@ export type AssetFullSyncDto = {
updatedUntil: string;
userId?: string;
};
export type SyncStreamDto = {
types: ("asset" | "asset.partner" | "assetAlbum" | "album" | "albumAsset" | "albumUser" | "activity" | "memory" | "partner" | "person" | "sharedLink" | "stack" | "tag" | "user")[];
};
export type AlbumAssetResponseDto = {
albumId: string;
assetId: string;
};
export type SyncStreamResponseDto = {
action: SyncAction;
data: AssetResponseDto | AlbumResponseDto | AlbumAssetResponseDto | ActivityResponseDto | MemoryResponseDto | PartnerResponseDto | PersonResponseDto | SharedLinkResponseDto | StackResponseDto | UserResponseDto;
"type": SyncType;
};
export type SystemConfigFFmpegDto = {
accel: TranscodeHWAccel;
accelDecode: boolean;
@ -2852,6 +2884,15 @@ export function updateStack({ id, stackUpdateDto }: {
body: stackUpdateDto
})));
}
export function ackSync({ syncAcknowledgeDto }: {
syncAcknowledgeDto: SyncAcknowledgeDto;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchText("/sync/acknowledge", oazapfts.json({
...opts,
method: "POST",
body: syncAcknowledgeDto
})));
}
export function getDeltaSync({ assetDeltaSyncDto }: {
assetDeltaSyncDto: AssetDeltaSyncDto;
}, opts?: Oazapfts.RequestOpts) {
@ -2876,6 +2917,18 @@ export function getFullSyncForUser({ assetFullSyncDto }: {
body: assetFullSyncDto
})));
}
export function getSyncStream({ syncStreamDto }: {
syncStreamDto: SyncStreamDto;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: SyncStreamResponseDto[];
}>("/sync/stream", oazapfts.json({
...opts,
method: "POST",
body: syncStreamDto
})));
}
export function getConfig(opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
@ -3491,6 +3544,26 @@ export enum Error2 {
NoPermission = "no_permission",
NotFound = "not_found"
}
export enum SyncAction {
Upsert = "upsert",
Delete = "delete"
}
export enum SyncType {
Asset = "asset",
AssetPartner = "asset.partner",
AssetAlbum = "assetAlbum",
Album = "album",
AlbumAsset = "albumAsset",
AlbumUser = "albumUser",
Activity = "activity",
Memory = "memory",
Partner = "partner",
Person = "person",
SharedLink = "sharedLink",
Stack = "stack",
Tag = "tag",
User = "user"
}
export enum TranscodeHWAccel {
Nvenc = "nvenc",
Qsv = "qsv",

View file

@ -26,7 +26,7 @@ import { teardownTelemetry } from 'src/repositories/telemetry.repository';
import { services } from 'src/services';
import { DatabaseService } from 'src/services/database.service';
const common = [...services, ...repositories];
const common = [...services, ...repositories, GlobalExceptionFilter];
const middleware = [
FileUploadInterceptor,

View file

@ -1,27 +1,60 @@
import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { Body, Controller, Header, HttpCode, HttpStatus, Post, Res } from '@nestjs/common';
import { ApiResponse, ApiTags } from '@nestjs/swagger';
import { Response } from 'express';
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import { AssetDeltaSyncDto, AssetDeltaSyncResponseDto, AssetFullSyncDto } from 'src/dtos/sync.dto';
import {
AssetDeltaSyncDto,
AssetDeltaSyncResponseDto,
AssetFullSyncDto,
SyncAcknowledgeDto,
SyncStreamDto,
SyncStreamResponseDto,
} from 'src/dtos/sync.dto';
import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { GlobalExceptionFilter } from 'src/middleware/global-exception.filter';
import { SyncService } from 'src/services/sync.service';
@ApiTags('Sync')
@Controller('sync')
export class SyncController {
constructor(private service: SyncService) {}
constructor(
private syncService: SyncService,
private errorService: GlobalExceptionFilter,
) {}
@Post('acknowledge')
@HttpCode(HttpStatus.NO_CONTENT)
@Authenticated()
ackSync(@Auth() auth: AuthDto, @Body() dto: SyncAcknowledgeDto) {
return this.syncService.acknowledge(auth, dto);
}
@Post('stream')
@Header('Content-Type', 'application/jsonlines+json')
@HttpCode(HttpStatus.OK)
@ApiResponse({ status: 200, type: SyncStreamResponseDto, isArray: true })
@Authenticated()
getSyncStream(@Auth() auth: AuthDto, @Res() res: Response, @Body() dto: SyncStreamDto) {
try {
void this.syncService.stream(auth, res, dto);
} catch (error: Error | any) {
res.setHeader('Content-Type', 'application/json');
this.errorService.handleError(res, error);
}
}
@Post('full-sync')
@HttpCode(HttpStatus.OK)
@Authenticated()
getFullSyncForUser(@Auth() auth: AuthDto, @Body() dto: AssetFullSyncDto): Promise<AssetResponseDto[]> {
return this.service.getFullSync(auth, dto);
return this.syncService.getFullSync(auth, dto);
}
@Post('delta-sync')
@HttpCode(HttpStatus.OK)
@Authenticated()
getDeltaSync(@Auth() auth: AuthDto, @Body() dto: AssetDeltaSyncDto): Promise<AssetDeltaSyncResponseDto> {
return this.service.getDeltaSync(auth, dto);
return this.syncService.getDeltaSync(auth, dto);
}
}

View file

@ -0,0 +1,9 @@
import { ApiProperty } from '@nestjs/swagger';
export class AlbumAssetResponseDto {
@ApiProperty({ format: 'uuid' })
albumId!: string;
@ApiProperty({ format: 'uuid' })
assetId!: string;
}

View file

@ -1,7 +1,140 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsInt, IsPositive } from 'class-validator';
import { ApiProperty, getSchemaPath } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { ArrayNotEmpty, IsDateString, IsEnum, IsInt, IsPositive, ValidateNested } from 'class-validator';
import { ActivityResponseDto } from 'src/dtos/activity.dto';
import { AlbumAssetResponseDto } from 'src/dtos/album-asset.dto';
import { AlbumResponseDto } from 'src/dtos/album.dto';
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
import { ValidateDate, ValidateUUID } from 'src/validation';
import { MemoryResponseDto } from 'src/dtos/memory.dto';
import { PartnerResponseDto } from 'src/dtos/partner.dto';
import { PersonResponseDto } from 'src/dtos/person.dto';
import { SharedLinkResponseDto } from 'src/dtos/shared-link.dto';
import { StackResponseDto } from 'src/dtos/stack.dto';
import { UserResponseDto } from 'src/dtos/user.dto';
import { SyncState } from 'src/entities/session-sync-state.entity';
import { SyncAction, SyncEntity } from 'src/enum';
import { Optional, ValidateDate, ValidateUUID } from 'src/validation';
class SyncCheckpointDto {
@ValidateUUID()
id!: string;
@IsDateString()
timestamp!: string;
}
export class SyncAcknowledgeDto implements SyncState {
@Optional()
@ValidateNested()
@Type(() => SyncCheckpointDto)
activity?: SyncCheckpointDto;
@Optional()
@ValidateNested()
@Type(() => SyncCheckpointDto)
album?: SyncCheckpointDto;
@Optional()
@ValidateNested()
@Type(() => SyncCheckpointDto)
albumUser?: SyncCheckpointDto;
@Optional()
@ValidateNested()
@Type(() => SyncCheckpointDto)
albumAsset?: SyncCheckpointDto;
@Optional()
@ValidateNested()
@Type(() => SyncCheckpointDto)
asset?: SyncCheckpointDto;
@Optional()
@ValidateNested()
@Type(() => SyncCheckpointDto)
assetAlbum?: SyncCheckpointDto;
@Optional()
@ValidateNested()
@Type(() => SyncCheckpointDto)
assetPartner?: SyncCheckpointDto;
@Optional()
@ValidateNested()
@Type(() => SyncCheckpointDto)
memory?: SyncCheckpointDto;
@Optional()
@ValidateNested()
@Type(() => SyncCheckpointDto)
partner?: SyncCheckpointDto;
@Optional()
@ValidateNested()
@Type(() => SyncCheckpointDto)
person?: SyncCheckpointDto;
@Optional()
@ValidateNested()
@Type(() => SyncCheckpointDto)
sharedLink?: SyncCheckpointDto;
@Optional()
@ValidateNested()
@Type(() => SyncCheckpointDto)
stack?: SyncCheckpointDto;
@Optional()
@ValidateNested()
@Type(() => SyncCheckpointDto)
tag?: SyncCheckpointDto;
@Optional()
@ValidateNested()
@Type(() => SyncCheckpointDto)
user?: SyncCheckpointDto;
}
export class SyncStreamResponseDto {
@ApiProperty({ enum: SyncEntity, enumName: 'SyncType' })
type!: SyncEntity;
@ApiProperty({ enum: SyncAction, enumName: 'SyncAction' })
action!: SyncAction;
@ApiProperty({
anyOf: [
{ $ref: getSchemaPath(AssetResponseDto) },
{ $ref: getSchemaPath(AlbumResponseDto) },
{ $ref: getSchemaPath(AlbumAssetResponseDto) },
{ $ref: getSchemaPath(ActivityResponseDto) },
{ $ref: getSchemaPath(MemoryResponseDto) },
{ $ref: getSchemaPath(PartnerResponseDto) },
{ $ref: getSchemaPath(PersonResponseDto) },
{ $ref: getSchemaPath(SharedLinkResponseDto) },
{ $ref: getSchemaPath(StackResponseDto) },
{ $ref: getSchemaPath(UserResponseDto) },
],
})
data!:
| ActivityResponseDto
| AssetResponseDto
| AlbumResponseDto
| AlbumAssetResponseDto
| MemoryResponseDto
| PartnerResponseDto
| PersonResponseDto
| SharedLinkResponseDto
| StackResponseDto
| UserResponseDto;
}
export class SyncStreamDto {
@IsEnum(SyncEntity, { each: true })
@ApiProperty({ enum: SyncEntity, isArray: true })
@ArrayNotEmpty()
types!: SyncEntity[];
}
export class AssetFullSyncDto {
@ValidateUUID({ optional: true })

View file

@ -16,6 +16,7 @@ import { MoveEntity } from 'src/entities/move.entity';
import { NaturalEarthCountriesEntity } from 'src/entities/natural-earth-countries.entity';
import { PartnerEntity } from 'src/entities/partner.entity';
import { PersonEntity } from 'src/entities/person.entity';
import { SessionSyncStateEntity } from 'src/entities/session-sync-state.entity';
import { SessionEntity } from 'src/entities/session.entity';
import { SharedLinkEntity } from 'src/entities/shared-link.entity';
import { SmartInfoEntity } from 'src/entities/smart-info.entity';
@ -54,6 +55,7 @@ export const entities = [
UserEntity,
UserMetadataEntity,
SessionEntity,
SessionSyncStateEntity,
LibraryEntity,
VersionHistoryEntity,
];

View file

@ -0,0 +1,52 @@
import { SessionEntity } from 'src/entities/session.entity';
import { Column, CreateDateColumn, Entity, JoinColumn, OneToOne, PrimaryColumn, UpdateDateColumn } from 'typeorm';
export type SyncCheckpoint = {
id: string;
timestamp: string;
};
export type SyncState = {
activity?: SyncCheckpoint;
album?: SyncCheckpoint;
albumUser?: SyncCheckpoint;
albumAsset?: SyncCheckpoint;
asset?: SyncCheckpoint;
assetAlbum?: SyncCheckpoint;
assetPartner?: SyncCheckpoint;
memory?: SyncCheckpoint;
partner?: SyncCheckpoint;
person?: SyncCheckpoint;
sharedLink?: SyncCheckpoint;
stack?: SyncCheckpoint;
tag?: SyncCheckpoint;
user?: SyncCheckpoint;
};
@Entity('session_sync_states')
export class SessionSyncStateEntity {
@OneToOne(() => SessionEntity, { onDelete: 'CASCADE', nullable: true })
@JoinColumn()
session?: SessionEntity;
@PrimaryColumn()
sessionId!: string;
@CreateDateColumn({ type: 'timestamptz' })
createdAt!: Date;
@UpdateDateColumn({ type: 'timestamptz' })
updatedAt!: Date;
@Column({ type: 'jsonb', nullable: true })
state?: SyncState;
}

View file

@ -53,6 +53,28 @@ export enum DatabaseAction {
DELETE = 'DELETE',
}
export enum SyncEntity {
ASSET = 'asset',
ASSET_PARTNER = 'asset.partner',
ASSET_ALBUM = 'assetAlbum',
ALBUM = 'album',
ALBUM_ASSET = 'albumAsset',
ALBUM_USER = 'albumUser',
ACTIVITY = 'activity',
MEMORY = 'memory',
PARTNER = 'partner',
PERSON = 'person',
SHARED_LINK = 'sharedLink',
STACK = 'stack',
TAG = 'tag',
USER = 'user',
}
export enum SyncAction {
UPSERT = 'upsert',
DELETE = 'delete',
}
export enum EntityType {
ASSET = 'ASSET',
ALBUM = 'ALBUM',

View file

@ -0,0 +1,43 @@
import { AlbumEntity } from 'src/entities/album.entity';
import { AssetEntity } from 'src/entities/asset.entity';
import { SessionSyncStateEntity, SyncCheckpoint } from 'src/entities/session-sync-state.entity';
import { Paginated, PaginationOptions } from 'src/utils/pagination';
export const ISyncRepository = 'ISyncRepository';
export type SyncOptions = PaginationOptions & {
userId: string;
checkpoint?: SyncCheckpoint;
};
export type AssetPartnerSyncOptions = SyncOptions & { partnerIds: string[] };
export type EntityPK = { id: string };
export type DeletedEntity<T = EntityPK> = T & {
deletedAt: Date;
};
export type AlbumAssetPK = {
albumId: string;
assetId: string;
};
export type AlbumAssetEntity = AlbumAssetPK & {
createdAt: Date;
};
export interface ISyncRepository {
get(sessionId: string): Promise<SessionSyncStateEntity | null>;
upsert(state: Partial<SessionSyncStateEntity>): Promise<void>;
getAssets(options: SyncOptions): Paginated<AssetEntity>;
getDeletedAssets(options: SyncOptions): Paginated<DeletedEntity>;
getAssetsPartner(options: AssetPartnerSyncOptions): Paginated<AssetEntity>;
getDeletedAssetsPartner(options: AssetPartnerSyncOptions): Paginated<DeletedEntity>;
getAlbums(options: SyncOptions): Paginated<AlbumEntity>;
getDeletedAlbums(options: SyncOptions): Paginated<DeletedEntity>;
getAlbumAssets(options: SyncOptions): Paginated<AlbumAssetEntity>;
getDeletedAlbumAssets(options: SyncOptions): Paginated<DeletedEntity<AlbumAssetPK>>;
}

View file

@ -1,9 +1,10 @@
import { ArgumentsHost, Catch, ExceptionFilter, HttpException, Inject } from '@nestjs/common';
import { ArgumentsHost, Catch, ExceptionFilter, HttpException, Inject, Injectable } from '@nestjs/common';
import { Response } from 'express';
import { ClsService } from 'nestjs-cls';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { logGlobalError } from 'src/utils/logger';
@Injectable()
@Catch()
export class GlobalExceptionFilter implements ExceptionFilter<Error> {
constructor(
@ -15,10 +16,13 @@ export class GlobalExceptionFilter implements ExceptionFilter<Error> {
catch(error: Error, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
this.handleError(ctx.getResponse(), error);
}
handleError(res: Response, error: Error) {
const { status, body } = this.fromError(error);
if (!response.headersSent) {
response.status(status).json({ ...body, statusCode: status, correlationId: this.cls.getId() });
if (!res.headersSent) {
res.status(status).json({ ...body, statusCode: status, correlationId: this.cls.getId() });
}
}

View file

@ -0,0 +1,16 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class AddSessionStateTable1729792220961 implements MigrationInterface {
name = 'AddSessionStateTable1729792220961'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "session_sync_states" ("sessionId" uuid NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "state" jsonb, CONSTRAINT "PK_4821e7414daba4413b8b33546d1" PRIMARY KEY ("sessionId"))`);
await queryRunner.query(`ALTER TABLE "session_sync_states" ADD CONSTRAINT "FK_4821e7414daba4413b8b33546d1" FOREIGN KEY ("sessionId") REFERENCES "sessions"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "session_sync_states" DROP CONSTRAINT "FK_4821e7414daba4413b8b33546d1"`);
await queryRunner.query(`DROP TABLE "session_sync_states"`);
}
}

View file

@ -28,6 +28,7 @@ import { ISessionRepository } from 'src/interfaces/session.interface';
import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
import { IStackRepository } from 'src/interfaces/stack.interface';
import { IStorageRepository } from 'src/interfaces/storage.interface';
import { ISyncRepository } from 'src/interfaces/sync.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { ITagRepository } from 'src/interfaces/tag.interface';
import { ITelemetryRepository } from 'src/interfaces/telemetry.interface';
@ -65,6 +66,7 @@ import { SessionRepository } from 'src/repositories/session.repository';
import { SharedLinkRepository } from 'src/repositories/shared-link.repository';
import { StackRepository } from 'src/repositories/stack.repository';
import { StorageRepository } from 'src/repositories/storage.repository';
import { SyncRepository } from 'src/repositories/sync.repository';
import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
import { TagRepository } from 'src/repositories/tag.repository';
import { TelemetryRepository } from 'src/repositories/telemetry.repository';
@ -104,6 +106,7 @@ export const repositories = [
{ provide: ISharedLinkRepository, useClass: SharedLinkRepository },
{ provide: IStackRepository, useClass: StackRepository },
{ provide: IStorageRepository, useClass: StorageRepository },
{ provide: ISyncRepository, useClass: SyncRepository },
{ provide: ISystemMetadataRepository, useClass: SystemMetadataRepository },
{ provide: ITagRepository, useClass: TagRepository },
{ provide: ITelemetryRepository, useClass: TelemetryRepository },

View file

@ -0,0 +1,114 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { AlbumEntity } from 'src/entities/album.entity';
import { AssetEntity } from 'src/entities/asset.entity';
import { SessionSyncStateEntity, SyncCheckpoint } from 'src/entities/session-sync-state.entity';
import {
AlbumAssetEntity,
AlbumAssetPK,
AssetPartnerSyncOptions,
DeletedEntity,
EntityPK,
ISyncRepository,
SyncOptions,
} from 'src/interfaces/sync.interface';
import { paginate, Paginated } from 'src/utils/pagination';
import { DataSource, FindOptionsWhere, In, MoreThan, MoreThanOrEqual, Repository } from 'typeorm';
const withCheckpoint = <T>(where: FindOptionsWhere<T>, key: keyof T, checkpoint?: SyncCheckpoint) => {
if (!checkpoint) {
return [where];
}
const { id: checkpointId, timestamp } = checkpoint;
const checkpointDate = new Date(timestamp);
return [
{
...where,
[key]: MoreThanOrEqual(new Date(checkpointDate)),
id: MoreThan(checkpointId),
},
{
...where,
[key]: MoreThan(checkpointDate),
},
];
};
@Injectable()
export class SyncRepository implements ISyncRepository {
constructor(
private dataSource: DataSource,
@InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>,
@InjectRepository(AlbumEntity) private albumRepository: Repository<AlbumEntity>,
@InjectRepository(SessionSyncStateEntity) private repository: Repository<SessionSyncStateEntity>,
) {}
get(sessionId: string): Promise<SessionSyncStateEntity | null> {
return this.repository.findOneBy({ sessionId });
}
async upsert(state: Partial<SessionSyncStateEntity>): Promise<void> {
await this.repository.upsert(state, { conflictPaths: ['sessionId'] });
}
getAssets({ checkpoint, userId, ...options }: AssetPartnerSyncOptions): Paginated<AssetEntity> {
return paginate(this.assetRepository, options, {
where: withCheckpoint<AssetEntity>(
{
ownerId: userId,
isVisible: true,
},
'updatedAt',
checkpoint,
),
relations: {
exifInfo: true,
},
order: {
updatedAt: 'ASC',
id: 'ASC',
},
});
}
getDeletedAssets(): Paginated<DeletedEntity<EntityPK>> {
return Promise.resolve({ items: [], hasNextPage: false });
}
getAssetsPartner({ checkpoint, partnerIds, ...options }: AssetPartnerSyncOptions): Paginated<AssetEntity> {
return paginate(this.assetRepository, options, {
where: withCheckpoint<AssetEntity>({ ownerId: In(partnerIds) }, 'updatedAt', checkpoint),
order: {
updatedAt: 'ASC',
id: 'ASC',
},
});
}
getDeletedAssetsPartner(): Paginated<DeletedEntity<EntityPK>> {
return Promise.resolve({ items: [], hasNextPage: false });
}
getAlbums({ checkpoint, userId, ...options }: SyncOptions): Paginated<AlbumEntity> {
return paginate(this.albumRepository, options, {
where: withCheckpoint<AlbumEntity>({ ownerId: userId }, 'updatedAt', checkpoint),
order: {
updatedAt: 'ASC',
id: 'ASC',
},
});
}
getDeletedAlbums(): Paginated<DeletedEntity<EntityPK>> {
return Promise.resolve({ items: [], hasNextPage: false });
}
getAlbumAssets(): Paginated<AlbumAssetEntity> {
return Promise.resolve({ items: [], hasNextPage: false });
}
getDeletedAlbumAssets(): Paginated<DeletedEntity<AlbumAssetPK>> {
return Promise.resolve({ items: [], hasNextPage: false });
}
}

View file

@ -31,6 +31,7 @@ import { ISessionRepository } from 'src/interfaces/session.interface';
import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
import { IStackRepository } from 'src/interfaces/stack.interface';
import { IStorageRepository } from 'src/interfaces/storage.interface';
import { ISyncRepository } from 'src/interfaces/sync.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { ITagRepository } from 'src/interfaces/tag.interface';
import { ITelemetryRepository } from 'src/interfaces/telemetry.interface';
@ -75,6 +76,7 @@ export class BaseService {
@Inject(ISharedLinkRepository) protected sharedLinkRepository: ISharedLinkRepository,
@Inject(IStackRepository) protected stackRepository: IStackRepository,
@Inject(IStorageRepository) protected storageRepository: IStorageRepository,
@Inject(ISyncRepository) protected syncRepository: ISyncRepository,
@Inject(ISystemMetadataRepository) protected systemMetadataRepository: ISystemMetadataRepository,
@Inject(ITagRepository) protected tagRepository: ITagRepository,
@Inject(ITelemetryRepository) protected telemetryRepository: ITelemetryRepository,

View file

@ -1,16 +1,184 @@
import { ForbiddenException } from '@nestjs/common';
import { DateTime } from 'luxon';
import { Writable } from 'node:stream';
import { AUDIT_LOG_MAX_DURATION } from 'src/constants';
import { AlbumAssetResponseDto } from 'src/dtos/album-asset.dto';
import { AlbumResponseDto, mapAlbumWithoutAssets } from 'src/dtos/album.dto';
import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import { AssetDeltaSyncDto, AssetDeltaSyncResponseDto, AssetFullSyncDto } from 'src/dtos/sync.dto';
import { DatabaseAction, EntityType, Permission } from 'src/enum';
import {
AssetDeltaSyncDto,
AssetDeltaSyncResponseDto,
AssetFullSyncDto,
SyncAcknowledgeDto,
SyncStreamDto,
} from 'src/dtos/sync.dto';
import { AlbumEntity } from 'src/entities/album.entity';
import { AssetEntity } from 'src/entities/asset.entity';
import { SyncCheckpoint } from 'src/entities/session-sync-state.entity';
import { DatabaseAction, EntityType, Permission, SyncAction, SyncEntity as SyncEntityType } from 'src/enum';
import { AlbumAssetEntity, DeletedEntity, SyncOptions } from 'src/interfaces/sync.interface';
import { BaseService } from 'src/services/base.service';
import { getMyPartnerIds } from 'src/utils/asset.util';
import { Paginated, usePagination } from 'src/utils/pagination';
import { setIsEqual } from 'src/utils/set';
const FULL_SYNC = { needsFullSync: true, deleted: [], upserted: [] };
const SYNC_PAGE_SIZE = 5000;
const asJsonLine = (item: unknown) => JSON.stringify(item) + '\n';
type Loader<T> = (options: SyncOptions) => Paginated<T>;
type Mapper<T, R> = (item: T) => R;
type StreamerArgs<T, R> = {
type: SyncEntityType;
action: SyncAction;
lastAck?: string;
load: Loader<T>;
map?: Mapper<T, R>;
ack: Mapper<T, SyncCheckpoint>;
};
class Streamer<T = any, R = any> {
constructor(private args: StreamerArgs<T, R>) {}
getEntityType() {
return this.args.type;
}
async write({ stream, userId, checkpoint }: { stream: Writable; userId: string; checkpoint?: SyncCheckpoint }) {
const { type, action, load, map, ack } = this.args;
const pagination = usePagination(SYNC_PAGE_SIZE, (options) => load({ ...options, userId, checkpoint }));
for await (const items of pagination) {
for (const item of items) {
stream.write(asJsonLine({ type, action, data: map?.(item) || (item as unknown as R), ack: ack(item) }));
}
}
}
}
export class SyncService extends BaseService {
async acknowledge(auth: AuthDto, dto: SyncAcknowledgeDto) {
const { id: sessionId } = this.assertSession(auth);
await this.syncRepository.upsert({
...dto,
sessionId,
});
}
async stream(auth: AuthDto, stream: Writable, dto: SyncStreamDto) {
const { id: sessionId, userId } = this.assertSession(auth);
const syncState = await this.syncRepository.get(sessionId);
const state = syncState?.state;
const checkpoints: Record<SyncEntityType, SyncCheckpoint | undefined> = {
[SyncEntityType.ACTIVITY]: state?.activity,
[SyncEntityType.ASSET]: state?.asset,
[SyncEntityType.ASSET_ALBUM]: state?.assetAlbum,
[SyncEntityType.ASSET_PARTNER]: state?.assetPartner,
[SyncEntityType.ALBUM]: state?.album,
[SyncEntityType.ALBUM_ASSET]: state?.albumAsset,
[SyncEntityType.ALBUM_USER]: state?.albumUser,
[SyncEntityType.MEMORY]: state?.memory,
[SyncEntityType.PARTNER]: state?.partner,
[SyncEntityType.PERSON]: state?.partner,
[SyncEntityType.SHARED_LINK]: state?.sharedLink,
[SyncEntityType.STACK]: state?.stack,
[SyncEntityType.TAG]: state?.tag,
[SyncEntityType.USER]: state?.user,
};
const streamers: Streamer[] = [];
for (const type of dto.types) {
switch (type) {
case SyncEntityType.ASSET: {
streamers.push(
new Streamer<AssetEntity, AssetResponseDto>({
type: SyncEntityType.ASSET,
action: SyncAction.UPSERT,
load: (options) => this.syncRepository.getAssets(options),
map: (item) => mapAsset(item, { auth, stripMetadata: false }),
ack: (item) => ({ id: item.id, timestamp: item.updatedAt.toISOString() }),
}),
new Streamer<DeletedEntity, DeletedEntity>({
type: SyncEntityType.ASSET,
action: SyncAction.DELETE,
load: (options) => this.syncRepository.getDeletedAssets(options),
map: (entity) => entity,
ack: (item) => ({ id: item.id, timestamp: item.deletedAt.toISOString() }),
}),
);
break;
}
case SyncEntityType.ASSET_PARTNER: {
const partnerIds = await getMyPartnerIds({ userId, repository: this.partnerRepository });
streamers.push(
new Streamer<AssetEntity, AssetResponseDto>({
type: SyncEntityType.ASSET_PARTNER,
action: SyncAction.UPSERT,
load: (options) => this.syncRepository.getAssetsPartner({ ...options, partnerIds }),
map: (item) => mapAsset(item, { auth, stripMetadata: false }),
ack: (item) => ({ id: item.id, timestamp: item.updatedAt.toISOString() }),
}),
new Streamer<DeletedEntity, DeletedEntity>({
type: SyncEntityType.ASSET_PARTNER,
action: SyncAction.DELETE,
load: (options) => this.syncRepository.getDeletedAssetsPartner({ ...options, partnerIds }),
ack: (item) => ({ id: item.id, timestamp: item.deletedAt.toISOString() }),
}),
);
break;
}
case SyncEntityType.ALBUM: {
streamers.push(
new Streamer<AlbumEntity, AlbumResponseDto>({
type: SyncEntityType.ALBUM,
action: SyncAction.UPSERT,
load: (options) => this.syncRepository.getAlbums(options),
map: (item) => mapAlbumWithoutAssets(item),
ack: (item) => ({ id: item.id, timestamp: item.updatedAt.toISOString() }),
}),
new Streamer<DeletedEntity, DeletedEntity>({
type: SyncEntityType.ALBUM,
action: SyncAction.DELETE,
load: (options) => this.syncRepository.getDeletedAlbums(options),
ack: (item) => ({ id: item.id, timestamp: item.deletedAt.toISOString() }),
}),
);
}
case SyncEntityType.ALBUM_ASSET: {
streamers.push(
new Streamer<AlbumAssetEntity, AlbumAssetResponseDto>({
type: SyncEntityType.ALBUM_ASSET,
action: SyncAction.UPSERT,
load: (options) => this.syncRepository.getAlbumAssets(options),
ack: (item) => ({ id: item.assetId, timestamp: item.createdAt.toISOString() }),
}),
new Streamer<DeletedEntity, DeletedEntity>({
type: SyncEntityType.ALBUM_ASSET,
action: SyncAction.DELETE,
load: (options) => this.syncRepository.getDeletedAlbums(options),
ack: (item) => ({ id: item.id, timestamp: item.deletedAt.toISOString() }),
}),
);
}
default: {
this.logger.warn(`Unsupported sync type: ${type}`);
break;
}
}
}
for (const streamer of streamers) {
await streamer.write({ stream, userId, checkpoint: checkpoints[streamer.getEntityType()] });
}
stream.end();
}
async getFullSync(auth: AuthDto, dto: AssetFullSyncDto): Promise<AssetResponseDto[]> {
// mobile implementation is faster if this is a single id
const userId = dto.userId || auth.user.id;
@ -71,4 +239,12 @@ export class SyncService extends BaseService {
};
return result;
}
private assertSession(auth: AuthDto) {
if (!auth.session?.id) {
throw new ForbiddenException('This endpoint requires session-based authentication');
}
return auth.session;
}
}

View file

@ -12,6 +12,7 @@ import { writeFileSync } from 'node:fs';
import path from 'node:path';
import { SystemConfig } from 'src/config';
import { CLIP_MODEL_INFO, serverVersion } from 'src/constants';
import { AlbumAssetResponseDto } from 'src/dtos/album-asset.dto';
import { ImmichCookie, ImmichHeader, MetadataKey } from 'src/enum';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
@ -219,6 +220,7 @@ export const useSwagger = (app: INestApplication, { write }: { write: boolean })
const options: SwaggerDocumentOptions = {
operationIdFactory: (controllerKey: string, methodKey: string) => methodKey,
extraModels: [AlbumAssetResponseDto],
};
const specification = SwaggerModule.createDocument(app, config, options);

View file

@ -0,0 +1,13 @@
import { ISyncRepository } from 'src/interfaces/sync.interface';
import { Mocked, vitest } from 'vitest';
export const newSyncRepositoryMock = (): Mocked<ISyncRepository> => {
return {
get: vitest.fn(),
upsert: vitest.fn(),
getAssets: vitest.fn(),
getAlbums: vitest.fn(),
getAlbumAssets: vitest.fn(),
};
};

View file

@ -31,6 +31,7 @@ import { newSessionRepositoryMock } from 'test/repositories/session.repository.m
import { newSharedLinkRepositoryMock } from 'test/repositories/shared-link.repository.mock';
import { newStackRepositoryMock } from 'test/repositories/stack.repository.mock';
import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock';
import { newSyncRepositoryMock } from 'test/repositories/sync.repository.mock';
import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
import { newTagRepositoryMock } from 'test/repositories/tag.repository.mock';
import { newTelemetryRepositoryMock } from 'test/repositories/telemetry.repository.mock';
@ -84,6 +85,7 @@ export const newTestService = <T extends BaseService>(
const sharedLinkMock = newSharedLinkRepositoryMock();
const stackMock = newStackRepositoryMock();
const storageMock = newStorageRepositoryMock();
const syncMock = newSyncRepositoryMock();
const systemMock = newSystemMetadataRepositoryMock();
const tagMock = newTagRepositoryMock();
const telemetryMock = newTelemetryRepositoryMock();
@ -123,6 +125,7 @@ export const newTestService = <T extends BaseService>(
sharedLinkMock,
stackMock,
storageMock,
syncMock,
systemMock,
tagMock,
telemetryMock,
@ -164,6 +167,7 @@ export const newTestService = <T extends BaseService>(
sharedLinkMock,
stackMock,
storageMock,
syncMock,
systemMock,
tagMock,
telemetryMock,

View file

@ -29,6 +29,34 @@
$: if ($user) {
openWebsocketConnection();
void fetch('/api/sync/stream', {
method: 'POST',
body: JSON.stringify({ types: ['asset'] }),
headers: { 'Content-Type': 'application/json' },
}).then(async (response) => {
if (response.body) {
const reader = response.body.getReader();
const decoder = new TextDecoder();
let done = false;
while (!done) {
const chunk = await reader.read();
done = chunk.done;
const data = chunk.value;
if (data) {
const parts = decoder.decode(data).split('\n');
for (const part of parts) {
if (!part.trim()) {
continue;
}
console.log(JSON.parse(part));
}
}
}
}
});
} else {
closeWebsocketConnection();
}