diff --git a/mobile/openapi/doc/CreateUserDto.md b/mobile/openapi/doc/CreateUserDto.md index 08fa8665a1c53..716571752c3d4 100644 --- a/mobile/openapi/doc/CreateUserDto.md +++ b/mobile/openapi/doc/CreateUserDto.md @@ -13,6 +13,7 @@ Name | Type | Description | Notes **memoriesEnabled** | **bool** | | [optional] **name** | **String** | | **password** | **String** | | +**quotaSizeInBytes** | **int** | | [optional] **storageLabel** | **String** | | [optional] [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/mobile/openapi/doc/PartnerResponseDto.md b/mobile/openapi/doc/PartnerResponseDto.md index 574b96f8df9b4..46d16e6b2815d 100644 --- a/mobile/openapi/doc/PartnerResponseDto.md +++ b/mobile/openapi/doc/PartnerResponseDto.md @@ -20,6 +20,8 @@ Name | Type | Description | Notes **name** | **String** | | **oauthId** | **String** | | **profileImagePath** | **String** | | +**quotaSizeInBytes** | **int** | | +**quotaUsageInBytes** | **int** | | **shouldChangePassword** | **bool** | | **storageLabel** | **String** | | **updatedAt** | [**DateTime**](DateTime.md) | | diff --git a/mobile/openapi/doc/UpdateUserDto.md b/mobile/openapi/doc/UpdateUserDto.md index 567bc43ebbae9..8c0572d1d2e0c 100644 --- a/mobile/openapi/doc/UpdateUserDto.md +++ b/mobile/openapi/doc/UpdateUserDto.md @@ -16,6 +16,7 @@ Name | Type | Description | Notes **memoriesEnabled** | **bool** | | [optional] **name** | **String** | | [optional] **password** | **String** | | [optional] +**quotaSizeInBytes** | **int** | | [optional] **shouldChangePassword** | **bool** | | [optional] **storageLabel** | **String** | | [optional] diff --git a/mobile/openapi/doc/UsageByUserDto.md b/mobile/openapi/doc/UsageByUserDto.md index f7b1b35931836..0eb2382b65974 100644 --- a/mobile/openapi/doc/UsageByUserDto.md +++ b/mobile/openapi/doc/UsageByUserDto.md @@ -9,6 +9,7 @@ import 'package:openapi/api.dart'; Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- **photos** | **int** | | +**quotaSizeInBytes** | **int** | | **usage** | **int** | | **userId** | **String** | | **userName** | **String** | | diff --git a/mobile/openapi/doc/UserResponseDto.md b/mobile/openapi/doc/UserResponseDto.md index 93f9aa62a3792..4ea44bb0cb527 100644 --- a/mobile/openapi/doc/UserResponseDto.md +++ b/mobile/openapi/doc/UserResponseDto.md @@ -19,6 +19,8 @@ Name | Type | Description | Notes **name** | **String** | | **oauthId** | **String** | | **profileImagePath** | **String** | | +**quotaSizeInBytes** | **int** | | +**quotaUsageInBytes** | **int** | | **shouldChangePassword** | **bool** | | **storageLabel** | **String** | | **updatedAt** | [**DateTime**](DateTime.md) | | diff --git a/mobile/openapi/lib/model/create_user_dto.dart b/mobile/openapi/lib/model/create_user_dto.dart index b33a3043c938a..887e44f109607 100644 --- a/mobile/openapi/lib/model/create_user_dto.dart +++ b/mobile/openapi/lib/model/create_user_dto.dart @@ -18,6 +18,7 @@ class CreateUserDto { this.memoriesEnabled, required this.name, required this.password, + this.quotaSizeInBytes, this.storageLabel, }); @@ -37,6 +38,8 @@ class CreateUserDto { String password; + int? quotaSizeInBytes; + String? storageLabel; @override @@ -46,6 +49,7 @@ class CreateUserDto { other.memoriesEnabled == memoriesEnabled && other.name == name && other.password == password && + other.quotaSizeInBytes == quotaSizeInBytes && other.storageLabel == storageLabel; @override @@ -56,10 +60,11 @@ class CreateUserDto { (memoriesEnabled == null ? 0 : memoriesEnabled!.hashCode) + (name.hashCode) + (password.hashCode) + + (quotaSizeInBytes == null ? 0 : quotaSizeInBytes!.hashCode) + (storageLabel == null ? 0 : storageLabel!.hashCode); @override - String toString() => 'CreateUserDto[email=$email, externalPath=$externalPath, memoriesEnabled=$memoriesEnabled, name=$name, password=$password, storageLabel=$storageLabel]'; + String toString() => 'CreateUserDto[email=$email, externalPath=$externalPath, memoriesEnabled=$memoriesEnabled, name=$name, password=$password, quotaSizeInBytes=$quotaSizeInBytes, storageLabel=$storageLabel]'; Map toJson() { final json = {}; @@ -76,6 +81,11 @@ class CreateUserDto { } json[r'name'] = this.name; json[r'password'] = this.password; + if (this.quotaSizeInBytes != null) { + json[r'quotaSizeInBytes'] = this.quotaSizeInBytes; + } else { + // json[r'quotaSizeInBytes'] = null; + } if (this.storageLabel != null) { json[r'storageLabel'] = this.storageLabel; } else { @@ -97,6 +107,7 @@ class CreateUserDto { memoriesEnabled: mapValueOfType(json, r'memoriesEnabled'), name: mapValueOfType(json, r'name')!, password: mapValueOfType(json, r'password')!, + quotaSizeInBytes: mapValueOfType(json, r'quotaSizeInBytes'), storageLabel: mapValueOfType(json, r'storageLabel'), ); } diff --git a/mobile/openapi/lib/model/partner_response_dto.dart b/mobile/openapi/lib/model/partner_response_dto.dart index 6e1776b26633a..7a3a7bf56696e 100644 --- a/mobile/openapi/lib/model/partner_response_dto.dart +++ b/mobile/openapi/lib/model/partner_response_dto.dart @@ -25,6 +25,8 @@ class PartnerResponseDto { required this.name, required this.oauthId, required this.profileImagePath, + required this.quotaSizeInBytes, + required this.quotaUsageInBytes, required this.shouldChangePassword, required this.storageLabel, required this.updatedAt, @@ -66,6 +68,10 @@ class PartnerResponseDto { String profileImagePath; + int? quotaSizeInBytes; + + int quotaUsageInBytes; + bool shouldChangePassword; String? storageLabel; @@ -86,6 +92,8 @@ class PartnerResponseDto { other.name == name && other.oauthId == oauthId && other.profileImagePath == profileImagePath && + other.quotaSizeInBytes == quotaSizeInBytes && + other.quotaUsageInBytes == quotaUsageInBytes && other.shouldChangePassword == shouldChangePassword && other.storageLabel == storageLabel && other.updatedAt == updatedAt; @@ -105,12 +113,14 @@ class PartnerResponseDto { (name.hashCode) + (oauthId.hashCode) + (profileImagePath.hashCode) + + (quotaSizeInBytes == null ? 0 : quotaSizeInBytes!.hashCode) + + (quotaUsageInBytes.hashCode) + (shouldChangePassword.hashCode) + (storageLabel == null ? 0 : storageLabel!.hashCode) + (updatedAt.hashCode); @override - String toString() => 'PartnerResponseDto[avatarColor=$avatarColor, createdAt=$createdAt, deletedAt=$deletedAt, email=$email, externalPath=$externalPath, id=$id, inTimeline=$inTimeline, isAdmin=$isAdmin, memoriesEnabled=$memoriesEnabled, name=$name, oauthId=$oauthId, profileImagePath=$profileImagePath, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel, updatedAt=$updatedAt]'; + String toString() => 'PartnerResponseDto[avatarColor=$avatarColor, createdAt=$createdAt, deletedAt=$deletedAt, email=$email, externalPath=$externalPath, id=$id, inTimeline=$inTimeline, isAdmin=$isAdmin, memoriesEnabled=$memoriesEnabled, name=$name, oauthId=$oauthId, profileImagePath=$profileImagePath, quotaSizeInBytes=$quotaSizeInBytes, quotaUsageInBytes=$quotaUsageInBytes, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel, updatedAt=$updatedAt]'; Map toJson() { final json = {}; @@ -142,6 +152,12 @@ class PartnerResponseDto { json[r'name'] = this.name; json[r'oauthId'] = this.oauthId; json[r'profileImagePath'] = this.profileImagePath; + if (this.quotaSizeInBytes != null) { + json[r'quotaSizeInBytes'] = this.quotaSizeInBytes; + } else { + // json[r'quotaSizeInBytes'] = null; + } + json[r'quotaUsageInBytes'] = this.quotaUsageInBytes; json[r'shouldChangePassword'] = this.shouldChangePassword; if (this.storageLabel != null) { json[r'storageLabel'] = this.storageLabel; @@ -172,6 +188,8 @@ class PartnerResponseDto { name: mapValueOfType(json, r'name')!, oauthId: mapValueOfType(json, r'oauthId')!, profileImagePath: mapValueOfType(json, r'profileImagePath')!, + quotaSizeInBytes: mapValueOfType(json, r'quotaSizeInBytes'), + quotaUsageInBytes: mapValueOfType(json, r'quotaUsageInBytes')!, shouldChangePassword: mapValueOfType(json, r'shouldChangePassword')!, storageLabel: mapValueOfType(json, r'storageLabel'), updatedAt: mapDateTime(json, r'updatedAt', '')!, @@ -232,6 +250,8 @@ class PartnerResponseDto { 'name', 'oauthId', 'profileImagePath', + 'quotaSizeInBytes', + 'quotaUsageInBytes', 'shouldChangePassword', 'storageLabel', 'updatedAt', diff --git a/mobile/openapi/lib/model/update_user_dto.dart b/mobile/openapi/lib/model/update_user_dto.dart index d0e46e7f5dc53..f64fde7f77770 100644 --- a/mobile/openapi/lib/model/update_user_dto.dart +++ b/mobile/openapi/lib/model/update_user_dto.dart @@ -21,6 +21,7 @@ class UpdateUserDto { this.memoriesEnabled, this.name, this.password, + this.quotaSizeInBytes, this.shouldChangePassword, this.storageLabel, }); @@ -83,6 +84,8 @@ class UpdateUserDto { /// String? password; + int? quotaSizeInBytes; + /// /// 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 @@ -109,6 +112,7 @@ class UpdateUserDto { other.memoriesEnabled == memoriesEnabled && other.name == name && other.password == password && + other.quotaSizeInBytes == quotaSizeInBytes && other.shouldChangePassword == shouldChangePassword && other.storageLabel == storageLabel; @@ -123,11 +127,12 @@ class UpdateUserDto { (memoriesEnabled == null ? 0 : memoriesEnabled!.hashCode) + (name == null ? 0 : name!.hashCode) + (password == null ? 0 : password!.hashCode) + + (quotaSizeInBytes == null ? 0 : quotaSizeInBytes!.hashCode) + (shouldChangePassword == null ? 0 : shouldChangePassword!.hashCode) + (storageLabel == null ? 0 : storageLabel!.hashCode); @override - String toString() => 'UpdateUserDto[avatarColor=$avatarColor, email=$email, externalPath=$externalPath, id=$id, isAdmin=$isAdmin, memoriesEnabled=$memoriesEnabled, name=$name, password=$password, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel]'; + String toString() => 'UpdateUserDto[avatarColor=$avatarColor, email=$email, externalPath=$externalPath, id=$id, isAdmin=$isAdmin, memoriesEnabled=$memoriesEnabled, name=$name, password=$password, quotaSizeInBytes=$quotaSizeInBytes, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel]'; Map toJson() { final json = {}; @@ -167,6 +172,11 @@ class UpdateUserDto { } else { // json[r'password'] = null; } + if (this.quotaSizeInBytes != null) { + json[r'quotaSizeInBytes'] = this.quotaSizeInBytes; + } else { + // json[r'quotaSizeInBytes'] = null; + } if (this.shouldChangePassword != null) { json[r'shouldChangePassword'] = this.shouldChangePassword; } else { @@ -196,6 +206,7 @@ class UpdateUserDto { memoriesEnabled: mapValueOfType(json, r'memoriesEnabled'), name: mapValueOfType(json, r'name'), password: mapValueOfType(json, r'password'), + quotaSizeInBytes: mapValueOfType(json, r'quotaSizeInBytes'), shouldChangePassword: mapValueOfType(json, r'shouldChangePassword'), storageLabel: mapValueOfType(json, r'storageLabel'), ); diff --git a/mobile/openapi/lib/model/usage_by_user_dto.dart b/mobile/openapi/lib/model/usage_by_user_dto.dart index 27d8a26730fa9..bda87eb963ae9 100644 --- a/mobile/openapi/lib/model/usage_by_user_dto.dart +++ b/mobile/openapi/lib/model/usage_by_user_dto.dart @@ -14,6 +14,7 @@ class UsageByUserDto { /// Returns a new [UsageByUserDto] instance. UsageByUserDto({ required this.photos, + required this.quotaSizeInBytes, required this.usage, required this.userId, required this.userName, @@ -22,6 +23,8 @@ class UsageByUserDto { int photos; + int? quotaSizeInBytes; + int usage; String userId; @@ -33,6 +36,7 @@ class UsageByUserDto { @override bool operator ==(Object other) => identical(this, other) || other is UsageByUserDto && other.photos == photos && + other.quotaSizeInBytes == quotaSizeInBytes && other.usage == usage && other.userId == userId && other.userName == userName && @@ -42,17 +46,23 @@ class UsageByUserDto { int get hashCode => // ignore: unnecessary_parenthesis (photos.hashCode) + + (quotaSizeInBytes == null ? 0 : quotaSizeInBytes!.hashCode) + (usage.hashCode) + (userId.hashCode) + (userName.hashCode) + (videos.hashCode); @override - String toString() => 'UsageByUserDto[photos=$photos, usage=$usage, userId=$userId, userName=$userName, videos=$videos]'; + String toString() => 'UsageByUserDto[photos=$photos, quotaSizeInBytes=$quotaSizeInBytes, usage=$usage, userId=$userId, userName=$userName, videos=$videos]'; Map toJson() { final json = {}; json[r'photos'] = this.photos; + if (this.quotaSizeInBytes != null) { + json[r'quotaSizeInBytes'] = this.quotaSizeInBytes; + } else { + // json[r'quotaSizeInBytes'] = null; + } json[r'usage'] = this.usage; json[r'userId'] = this.userId; json[r'userName'] = this.userName; @@ -69,6 +79,7 @@ class UsageByUserDto { return UsageByUserDto( photos: mapValueOfType(json, r'photos')!, + quotaSizeInBytes: mapValueOfType(json, r'quotaSizeInBytes'), usage: mapValueOfType(json, r'usage')!, userId: mapValueOfType(json, r'userId')!, userName: mapValueOfType(json, r'userName')!, @@ -121,6 +132,7 @@ class UsageByUserDto { /// The list of required keys that must be present in a JSON. static const requiredKeys = { 'photos', + 'quotaSizeInBytes', 'usage', 'userId', 'userName', diff --git a/mobile/openapi/lib/model/user_response_dto.dart b/mobile/openapi/lib/model/user_response_dto.dart index 11a182b6b7475..0f2e2eaf2df88 100644 --- a/mobile/openapi/lib/model/user_response_dto.dart +++ b/mobile/openapi/lib/model/user_response_dto.dart @@ -24,6 +24,8 @@ class UserResponseDto { required this.name, required this.oauthId, required this.profileImagePath, + required this.quotaSizeInBytes, + required this.quotaUsageInBytes, required this.shouldChangePassword, required this.storageLabel, required this.updatedAt, @@ -57,6 +59,10 @@ class UserResponseDto { String profileImagePath; + int? quotaSizeInBytes; + + int quotaUsageInBytes; + bool shouldChangePassword; String? storageLabel; @@ -76,6 +82,8 @@ class UserResponseDto { other.name == name && other.oauthId == oauthId && other.profileImagePath == profileImagePath && + other.quotaSizeInBytes == quotaSizeInBytes && + other.quotaUsageInBytes == quotaUsageInBytes && other.shouldChangePassword == shouldChangePassword && other.storageLabel == storageLabel && other.updatedAt == updatedAt; @@ -94,12 +102,14 @@ class UserResponseDto { (name.hashCode) + (oauthId.hashCode) + (profileImagePath.hashCode) + + (quotaSizeInBytes == null ? 0 : quotaSizeInBytes!.hashCode) + + (quotaUsageInBytes.hashCode) + (shouldChangePassword.hashCode) + (storageLabel == null ? 0 : storageLabel!.hashCode) + (updatedAt.hashCode); @override - String toString() => 'UserResponseDto[avatarColor=$avatarColor, createdAt=$createdAt, deletedAt=$deletedAt, email=$email, externalPath=$externalPath, id=$id, isAdmin=$isAdmin, memoriesEnabled=$memoriesEnabled, name=$name, oauthId=$oauthId, profileImagePath=$profileImagePath, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel, updatedAt=$updatedAt]'; + String toString() => 'UserResponseDto[avatarColor=$avatarColor, createdAt=$createdAt, deletedAt=$deletedAt, email=$email, externalPath=$externalPath, id=$id, isAdmin=$isAdmin, memoriesEnabled=$memoriesEnabled, name=$name, oauthId=$oauthId, profileImagePath=$profileImagePath, quotaSizeInBytes=$quotaSizeInBytes, quotaUsageInBytes=$quotaUsageInBytes, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel, updatedAt=$updatedAt]'; Map toJson() { final json = {}; @@ -126,6 +136,12 @@ class UserResponseDto { json[r'name'] = this.name; json[r'oauthId'] = this.oauthId; json[r'profileImagePath'] = this.profileImagePath; + if (this.quotaSizeInBytes != null) { + json[r'quotaSizeInBytes'] = this.quotaSizeInBytes; + } else { + // json[r'quotaSizeInBytes'] = null; + } + json[r'quotaUsageInBytes'] = this.quotaUsageInBytes; json[r'shouldChangePassword'] = this.shouldChangePassword; if (this.storageLabel != null) { json[r'storageLabel'] = this.storageLabel; @@ -155,6 +171,8 @@ class UserResponseDto { name: mapValueOfType(json, r'name')!, oauthId: mapValueOfType(json, r'oauthId')!, profileImagePath: mapValueOfType(json, r'profileImagePath')!, + quotaSizeInBytes: mapValueOfType(json, r'quotaSizeInBytes'), + quotaUsageInBytes: mapValueOfType(json, r'quotaUsageInBytes')!, shouldChangePassword: mapValueOfType(json, r'shouldChangePassword')!, storageLabel: mapValueOfType(json, r'storageLabel'), updatedAt: mapDateTime(json, r'updatedAt', '')!, @@ -215,6 +233,8 @@ class UserResponseDto { 'name', 'oauthId', 'profileImagePath', + 'quotaSizeInBytes', + 'quotaUsageInBytes', 'shouldChangePassword', 'storageLabel', 'updatedAt', diff --git a/mobile/openapi/test/create_user_dto_test.dart b/mobile/openapi/test/create_user_dto_test.dart index 0820ef0ed459f..6614b54a625fa 100644 --- a/mobile/openapi/test/create_user_dto_test.dart +++ b/mobile/openapi/test/create_user_dto_test.dart @@ -41,6 +41,11 @@ void main() { // TODO }); + // int quotaSizeInBytes + test('to test the property `quotaSizeInBytes`', () async { + // TODO + }); + // String storageLabel test('to test the property `storageLabel`', () async { // TODO diff --git a/mobile/openapi/test/partner_response_dto_test.dart b/mobile/openapi/test/partner_response_dto_test.dart index 50ac1d8050a79..d6b7769ab401b 100644 --- a/mobile/openapi/test/partner_response_dto_test.dart +++ b/mobile/openapi/test/partner_response_dto_test.dart @@ -76,6 +76,16 @@ void main() { // TODO }); + // int quotaSizeInBytes + test('to test the property `quotaSizeInBytes`', () async { + // TODO + }); + + // int quotaUsageInBytes + test('to test the property `quotaUsageInBytes`', () async { + // TODO + }); + // bool shouldChangePassword test('to test the property `shouldChangePassword`', () async { // TODO diff --git a/mobile/openapi/test/update_user_dto_test.dart b/mobile/openapi/test/update_user_dto_test.dart index 0b4cc0b65dfe1..aaacfa7cbd66e 100644 --- a/mobile/openapi/test/update_user_dto_test.dart +++ b/mobile/openapi/test/update_user_dto_test.dart @@ -56,6 +56,11 @@ void main() { // TODO }); + // int quotaSizeInBytes + test('to test the property `quotaSizeInBytes`', () async { + // TODO + }); + // bool shouldChangePassword test('to test the property `shouldChangePassword`', () async { // TODO diff --git a/mobile/openapi/test/usage_by_user_dto_test.dart b/mobile/openapi/test/usage_by_user_dto_test.dart index 51becb06f0535..68a9f20846bab 100644 --- a/mobile/openapi/test/usage_by_user_dto_test.dart +++ b/mobile/openapi/test/usage_by_user_dto_test.dart @@ -21,6 +21,11 @@ void main() { // TODO }); + // int quotaSizeInBytes + test('to test the property `quotaSizeInBytes`', () async { + // TODO + }); + // int usage test('to test the property `usage`', () async { // TODO diff --git a/mobile/openapi/test/user_response_dto_test.dart b/mobile/openapi/test/user_response_dto_test.dart index aa0717e74d29a..bef6c812e9c39 100644 --- a/mobile/openapi/test/user_response_dto_test.dart +++ b/mobile/openapi/test/user_response_dto_test.dart @@ -71,6 +71,16 @@ void main() { // TODO }); + // int quotaSizeInBytes + test('to test the property `quotaSizeInBytes`', () async { + // TODO + }); + + // int quotaUsageInBytes + test('to test the property `quotaUsageInBytes`', () async { + // TODO + }); + // bool shouldChangePassword test('to test the property `shouldChangePassword`', () async { // TODO diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 240b341d2bced..112174b49223c 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -7409,6 +7409,11 @@ "password": { "type": "string" }, + "quotaSizeInBytes": { + "format": "int64", + "nullable": true, + "type": "integer" + }, "storageLabel": { "nullable": true, "type": "string" @@ -8192,6 +8197,15 @@ "profileImagePath": { "type": "string" }, + "quotaSizeInBytes": { + "format": "int64", + "nullable": true, + "type": "integer" + }, + "quotaUsageInBytes": { + "format": "int64", + "type": "integer" + }, "shouldChangePassword": { "type": "boolean" }, @@ -8206,10 +8220,8 @@ }, "required": [ "avatarColor", - "id", - "name", - "email", - "profileImagePath", + "quotaSizeInBytes", + "quotaUsageInBytes", "storageLabel", "externalPath", "shouldChangePassword", @@ -8217,7 +8229,11 @@ "createdAt", "deletedAt", "updatedAt", - "oauthId" + "oauthId", + "id", + "name", + "email", + "profileImagePath" ], "type": "object" }, @@ -9757,6 +9773,11 @@ "password": { "type": "string" }, + "quotaSizeInBytes": { + "format": "int64", + "nullable": true, + "type": "integer" + }, "shouldChangePassword": { "type": "boolean" }, @@ -9774,6 +9795,11 @@ "photos": { "type": "integer" }, + "quotaSizeInBytes": { + "format": "int64", + "nullable": true, + "type": "integer" + }, "usage": { "format": "int64", "type": "integer" @@ -9793,7 +9819,8 @@ "userName", "photos", "videos", - "usage" + "usage", + "quotaSizeInBytes" ], "type": "object" }, @@ -9878,6 +9905,15 @@ "profileImagePath": { "type": "string" }, + "quotaSizeInBytes": { + "format": "int64", + "nullable": true, + "type": "integer" + }, + "quotaUsageInBytes": { + "format": "int64", + "type": "integer" + }, "shouldChangePassword": { "type": "boolean" }, @@ -9892,10 +9928,8 @@ }, "required": [ "avatarColor", - "id", - "name", - "email", - "profileImagePath", + "quotaSizeInBytes", + "quotaUsageInBytes", "storageLabel", "externalPath", "shouldChangePassword", @@ -9903,7 +9937,11 @@ "createdAt", "deletedAt", "updatedAt", - "oauthId" + "oauthId", + "id", + "name", + "email", + "profileImagePath" ], "type": "object" }, diff --git a/open-api/typescript-sdk/client/api.ts b/open-api/typescript-sdk/client/api.ts index eebe1bdd55245..7f0587319f4ab 100644 --- a/open-api/typescript-sdk/client/api.ts +++ b/open-api/typescript-sdk/client/api.ts @@ -1472,6 +1472,12 @@ export interface CreateUserDto { * @memberof CreateUserDto */ 'password': string; + /** + * + * @type {number} + * @memberof CreateUserDto + */ + 'quotaSizeInBytes'?: number | null; /** * * @type {string} @@ -2479,6 +2485,18 @@ export interface PartnerResponseDto { * @memberof PartnerResponseDto */ 'profileImagePath': string; + /** + * + * @type {number} + * @memberof PartnerResponseDto + */ + 'quotaSizeInBytes': number | null; + /** + * + * @type {number} + * @memberof PartnerResponseDto + */ + 'quotaUsageInBytes': number; /** * * @type {boolean} @@ -4544,6 +4562,12 @@ export interface UpdateUserDto { * @memberof UpdateUserDto */ 'password'?: string; + /** + * + * @type {number} + * @memberof UpdateUserDto + */ + 'quotaSizeInBytes'?: number | null; /** * * @type {boolean} @@ -4571,6 +4595,12 @@ export interface UsageByUserDto { * @memberof UsageByUserDto */ 'photos': number; + /** + * + * @type {number} + * @memberof UsageByUserDto + */ + 'quotaSizeInBytes': number | null; /** * * @type {number} @@ -4729,6 +4759,18 @@ export interface UserResponseDto { * @memberof UserResponseDto */ 'profileImagePath': string; + /** + * + * @type {number} + * @memberof UserResponseDto + */ + 'quotaSizeInBytes': number | null; + /** + * + * @type {number} + * @memberof UserResponseDto + */ + 'quotaUsageInBytes': number; /** * * @type {boolean} diff --git a/server/e2e/api/specs/album.e2e-spec.ts b/server/e2e/api/specs/album.e2e-spec.ts index d7dea11c788b7..0df753a846f03 100644 --- a/server/e2e/api/specs/album.e2e-spec.ts +++ b/server/e2e/api/specs/album.e2e-spec.ts @@ -250,7 +250,7 @@ describe(`${AlbumController.name} (e2e)`, () => { .set('Authorization', `Bearer ${user1.accessToken}`); expect(status).toBe(200); - expect(body).toEqual(user1Albums[0]); + expect(body).toEqual({ ...user1Albums[0], assets: [expect.objectContaining(user1Albums[0].assets[0])] }); }); it('should return album info for shared album', async () => { @@ -259,7 +259,7 @@ describe(`${AlbumController.name} (e2e)`, () => { .set('Authorization', `Bearer ${user1.accessToken}`); expect(status).toBe(200); - expect(body).toEqual(user2Albums[0]); + expect(body).toEqual({ ...user2Albums[0], assets: [expect.objectContaining(user2Albums[0].assets[0])] }); }); it('should return album info with assets when withoutAssets is undefined', async () => { @@ -268,7 +268,7 @@ describe(`${AlbumController.name} (e2e)`, () => { .set('Authorization', `Bearer ${user1.accessToken}`); expect(status).toBe(200); - expect(body).toEqual(user1Albums[0]); + expect(body).toEqual({ ...user1Albums[0], assets: [expect.objectContaining(user1Albums[0].assets[0])] }); }); it('should return album info without assets when withoutAssets is true', async () => { diff --git a/server/e2e/api/specs/asset.e2e-spec.ts b/server/e2e/api/specs/asset.e2e-spec.ts index 783f3539b45ec..1aafea39dcf36 100644 --- a/server/e2e/api/specs/asset.e2e-spec.ts +++ b/server/e2e/api/specs/asset.e2e-spec.ts @@ -43,6 +43,7 @@ describe(`${AssetController.name} (e2e)`, () => { let assetRepository: IAssetRepository; let user1: LoginResponseDto; let user2: LoginResponseDto; + let userWithQuota: LoginResponseDto; let libraries: LibraryResponseDto[]; let asset1: AssetResponseDto; let asset2: AssetResponseDto; @@ -75,11 +76,13 @@ describe(`${AssetController.name} (e2e)`, () => { await Promise.all([ api.userApi.create(server, admin.accessToken, userDto.user1), api.userApi.create(server, admin.accessToken, userDto.user2), + api.userApi.create(server, admin.accessToken, userDto.userWithQuota), ]); - [user1, user2] = await Promise.all([ + [user1, user2, userWithQuota] = await Promise.all([ api.authApi.login(server, userDto.user1), api.authApi.login(server, userDto.user2), + api.authApi.login(server, userDto.userWithQuota), ]); const [user1Libraries, user2Libraries] = await Promise.all([ @@ -634,6 +637,46 @@ describe(`${AssetController.name} (e2e)`, () => { expect(status).toBe(400); expect(body).toEqual(errorStub.badRequest('Not found or no asset.upload access')); }); + + it('should update the used quota', async () => { + const content = randomBytes(32); + const { body, status } = await request(server) + .post('/asset/upload') + .set('Authorization', `Bearer ${userWithQuota.accessToken}`) + .field('deviceAssetId', 'example-image') + .field('deviceId', 'TEST') + .field('fileCreatedAt', new Date().toISOString()) + .field('fileModifiedAt', new Date().toISOString()) + .field('isFavorite', 'true') + .field('duration', '0:00:00.000000') + .attach('assetData', content, 'example.jpg'); + + expect(status).toBe(201); + expect(body).toEqual({ id: expect.any(String), duplicate: false }); + + const { body: user } = await request(server) + .get('/user/me') + .set('Authorization', `Bearer ${userWithQuota.accessToken}`); + + expect(user).toEqual(expect.objectContaining({ quotaUsageInBytes: 32 })); + }); + + it('should not upload an asset if it would exceed the quota', async () => { + const content = randomBytes(420); + const { body, status } = await request(server) + .post('/asset/upload') + .set('Authorization', `Bearer ${userWithQuota.accessToken}`) + .field('deviceAssetId', 'example-image') + .field('deviceId', 'TEST') + .field('fileCreatedAt', new Date().toISOString()) + .field('fileModifiedAt', new Date().toISOString()) + .field('isFavorite', 'true') + .field('duration', '0:00:00.000000') + .attach('assetData', content, 'example.jpg'); + + expect(status).toBe(400); + expect(body).toEqual(errorStub.badRequest('Quota has been exceeded!')); + }); }); describe('PUT /asset/:id', () => { diff --git a/server/e2e/api/specs/auth.e2e-spec.ts b/server/e2e/api/specs/auth.e2e-spec.ts index bccece1bae1ca..179fef79513da 100644 --- a/server/e2e/api/specs/auth.e2e-spec.ts +++ b/server/e2e/api/specs/auth.e2e-spec.ts @@ -32,6 +32,8 @@ const adminSignupResponse = { deletedAt: null, oauthId: '', memoriesEnabled: true, + quotaUsageInBytes: 0, + quotaSizeInBytes: null, }; describe(`${AuthController.name} (e2e)`, () => { diff --git a/server/e2e/api/specs/server-info.e2e-spec.ts b/server/e2e/api/specs/server-info.e2e-spec.ts index 587f0a2cc49eb..015fd53dcd932 100644 --- a/server/e2e/api/specs/server-info.e2e-spec.ts +++ b/server/e2e/api/specs/server-info.e2e-spec.ts @@ -128,6 +128,7 @@ describe(`${ServerInfoController.name} (e2e)`, () => { usage: 0, usageByUser: [ { + quotaSizeInBytes: null, photos: 0, usage: 0, userName: 'Immich Admin', @@ -135,6 +136,7 @@ describe(`${ServerInfoController.name} (e2e)`, () => { videos: 0, }, { + quotaSizeInBytes: null, photos: 0, usage: 0, userName: 'User 1', diff --git a/server/src/domain/asset/asset.service.spec.ts b/server/src/domain/asset/asset.service.spec.ts index e49f771110ce3..cfc39480c9d83 100644 --- a/server/src/domain/asset/asset.service.spec.ts +++ b/server/src/domain/asset/asset.service.spec.ts @@ -13,6 +13,7 @@ import { newPartnerRepositoryMock, newStorageRepositoryMock, newSystemConfigRepositoryMock, + newUserRepositoryMock, } from '@test'; import { when } from 'jest-when'; import { Readable } from 'stream'; @@ -28,6 +29,7 @@ import { IPartnerRepository, IStorageRepository, ISystemConfigRepository, + IUserRepository, JobItem, TimeBucketSize, } from '../repositories'; @@ -67,6 +69,7 @@ const uploadFile = { checksum: Buffer.from('checksum', 'utf8'), originalPath: 'upload/admin/image.jpeg', originalName: 'image.jpeg', + size: 1000, }, }, filename: (fieldName: UploadFieldName, filename: string) => { @@ -79,6 +82,7 @@ const uploadFile = { checksum: Buffer.from('checksum', 'utf8'), originalPath: `upload/admin/${filename}`, originalName: filename, + size: 1000, }, }; }, @@ -167,6 +171,7 @@ describe(AssetService.name, () => { let cryptoMock: jest.Mocked; let jobMock: jest.Mocked; let storageMock: jest.Mocked; + let userMock: jest.Mocked; let communicationMock: jest.Mocked; let configMock: jest.Mocked; let partnerMock: jest.Mocked; @@ -182,6 +187,7 @@ describe(AssetService.name, () => { cryptoMock = newCryptoRepositoryMock(); jobMock = newJobRepositoryMock(); storageMock = newStorageRepositoryMock(); + userMock = newUserRepositoryMock(); configMock = newSystemConfigRepositoryMock(); partnerMock = newPartnerRepositoryMock(); @@ -192,6 +198,7 @@ describe(AssetService.name, () => { jobMock, configMock, storageMock, + userMock, communicationMock, partnerMock, ); @@ -836,7 +843,7 @@ describe(AssetService.name, () => { }); it('should remove faces', async () => { - const assetWithFace = { ...(assetStub.image as AssetEntity), faces: [faceStub.face1, faceStub.mergeFace1] }; + const assetWithFace = { ...assetStub.image, faces: [faceStub.face1, faceStub.mergeFace1] }; when(assetMock.getById).calledWith(assetWithFace.id).mockResolvedValue(assetWithFace); @@ -863,9 +870,7 @@ describe(AssetService.name, () => { }); it('should update stack parent if asset has stack children', async () => { - when(assetMock.getById) - .calledWith(assetStub.primaryImage.id) - .mockResolvedValue(assetStub.primaryImage as AssetEntity); + when(assetMock.getById).calledWith(assetStub.primaryImage.id).mockResolvedValue(assetStub.primaryImage); await sut.handleAssetDeletion({ id: assetStub.primaryImage.id }); @@ -878,9 +883,7 @@ describe(AssetService.name, () => { }); it('should not schedule delete-files job for readonly assets', async () => { - when(assetMock.getById) - .calledWith(assetStub.readOnly.id) - .mockResolvedValue(assetStub.readOnly as AssetEntity); + when(assetMock.getById).calledWith(assetStub.readOnly.id).mockResolvedValue(assetStub.readOnly); await sut.handleAssetDeletion({ id: assetStub.readOnly.id }); @@ -890,21 +893,17 @@ describe(AssetService.name, () => { }); it('should not process assets from external library without fromExternal flag', async () => { - when(assetMock.getById) - .calledWith(assetStub.external.id) - .mockResolvedValue(assetStub.external as AssetEntity); + when(assetMock.getById).calledWith(assetStub.external.id).mockResolvedValue(assetStub.external); await sut.handleAssetDeletion({ id: assetStub.external.id }); - expect(jobMock.queue).not.toBeCalled(); - expect(jobMock.queueAll).not.toBeCalled(); - expect(assetMock.remove).not.toBeCalled(); + expect(jobMock.queue).not.toHaveBeenCalled(); + expect(jobMock.queueAll).not.toHaveBeenCalled(); + expect(assetMock.remove).not.toHaveBeenCalled(); }); it('should process assets from external library with fromExternal flag', async () => { - when(assetMock.getById) - .calledWith(assetStub.external.id) - .mockResolvedValue(assetStub.external as AssetEntity); + when(assetMock.getById).calledWith(assetStub.external.id).mockResolvedValue(assetStub.external); await sut.handleAssetDeletion({ id: assetStub.external.id, fromExternal: true }); @@ -949,6 +948,13 @@ describe(AssetService.name, () => { ], ]); }); + + it('should update usage', async () => { + when(assetMock.getById).calledWith(assetStub.image.id).mockResolvedValue(assetStub.image); + await sut.handleAssetDeletion({ id: assetStub.image.id }); + + expect(userMock.updateUsage).toHaveBeenCalledWith(assetStub.image.ownerId, -5000); + }); }); describe('run', () => { diff --git a/server/src/domain/asset/asset.service.ts b/server/src/domain/asset/asset.service.ts index d82fd4027007e..9eaca4cbe35f0 100644 --- a/server/src/domain/asset/asset.service.ts +++ b/server/src/domain/asset/asset.service.ts @@ -20,6 +20,7 @@ import { IPartnerRepository, IStorageRepository, ISystemConfigRepository, + IUserRepository, ImmichReadStream, JobItem, TimeBucketOptions, @@ -75,6 +76,7 @@ export interface UploadFile { checksum: Buffer; originalPath: string; originalName: string; + size: number; } export class AssetService { @@ -89,6 +91,7 @@ export class AssetService { @Inject(IJobRepository) private jobRepository: IJobRepository, @Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository, @Inject(IStorageRepository) private storageRepository: IStorageRepository, + @Inject(IUserRepository) private userRepository: IUserRepository, @Inject(ICommunicationRepository) private communicationRepository: ICommunicationRepository, @Inject(IPartnerRepository) private partnerRepository: IPartnerRepository, ) { @@ -481,6 +484,7 @@ export class AssetService { } await this.assetRepository.remove(asset); + await this.userRepository.updateUsage(asset.ownerId, -(asset.exifInfo?.fileSizeInByte || 0)); this.communicationRepository.send(ClientEvent.ASSET_DELETE, asset.ownerId, id); // TODO refactor this to use cascades diff --git a/server/src/domain/job/job.constants.ts b/server/src/domain/job/job.constants.ts index d239d28c2b655..84dc89c0f85e4 100644 --- a/server/src/domain/job/job.constants.ts +++ b/server/src/domain/job/job.constants.ts @@ -37,9 +37,10 @@ export enum JobName { METADATA_EXTRACTION = 'metadata-extraction', LINK_LIVE_PHOTOS = 'link-live-photos', - // user deletion + // user USER_DELETION = 'user-deletion', USER_DELETE_CHECK = 'user-delete-check', + USER_SYNC_USAGE = 'user-sync-usage', // asset ASSET_DELETION = 'asset-deletion', @@ -95,6 +96,7 @@ export const JOBS_TO_QUEUE: Record = { [JobName.CLEAN_OLD_AUDIT_LOGS]: QueueName.BACKGROUND_TASK, [JobName.PERSON_CLEANUP]: QueueName.BACKGROUND_TASK, [JobName.PERSON_DELETE]: QueueName.BACKGROUND_TASK, + [JobName.USER_SYNC_USAGE]: QueueName.BACKGROUND_TASK, // conversion [JobName.QUEUE_VIDEO_CONVERSION]: QueueName.VIDEO_CONVERSION, diff --git a/server/src/domain/job/job.service.spec.ts b/server/src/domain/job/job.service.spec.ts index 7b912a111b539..747f29dd77f9d 100644 --- a/server/src/domain/job/job.service.spec.ts +++ b/server/src/domain/job/job.service.spec.ts @@ -60,6 +60,7 @@ describe(JobService.name, () => { { name: JobName.PERSON_CLEANUP }, { name: JobName.QUEUE_GENERATE_THUMBNAILS, data: { force: false } }, { name: JobName.CLEAN_OLD_AUDIT_LOGS }, + { name: JobName.USER_SYNC_USAGE }, ]); }); }); diff --git a/server/src/domain/job/job.service.ts b/server/src/domain/job/job.service.ts index 3f418cf335f09..e1cd48156fa38 100644 --- a/server/src/domain/job/job.service.ts +++ b/server/src/domain/job/job.service.ts @@ -164,6 +164,7 @@ export class JobService { { name: JobName.PERSON_CLEANUP }, { name: JobName.QUEUE_GENERATE_THUMBNAILS, data: { force: false } }, { name: JobName.CLEAN_OLD_AUDIT_LOGS }, + { name: JobName.USER_SYNC_USAGE }, ]); } diff --git a/server/src/domain/partner/partner.service.spec.ts b/server/src/domain/partner/partner.service.spec.ts index 1d28ee89213a0..0f9e173b70b66 100644 --- a/server/src/domain/partner/partner.service.spec.ts +++ b/server/src/domain/partner/partner.service.spec.ts @@ -21,7 +21,9 @@ const responseDto = { externalPath: null, memoriesEnabled: true, avatarColor: UserAvatarColor.PRIMARY, + quotaSizeInBytes: null, inTimeline: true, + quotaUsageInBytes: 0, }, user1: { email: 'immich@test.com', @@ -39,6 +41,8 @@ const responseDto = { memoriesEnabled: true, avatarColor: UserAvatarColor.PRIMARY, inTimeline: true, + quotaSizeInBytes: null, + quotaUsageInBytes: 0, }, }; diff --git a/server/src/domain/repositories/job.repository.ts b/server/src/domain/repositories/job.repository.ts index faa78adb06cb3..164f3830e8d42 100644 --- a/server/src/domain/repositories/job.repository.ts +++ b/server/src/domain/repositories/job.repository.ts @@ -39,9 +39,10 @@ export type JobItem = | { name: JobName.GENERATE_WEBP_THUMBNAIL; data: IEntityJob } | { name: JobName.GENERATE_THUMBHASH_THUMBNAIL; data: IEntityJob } - // User Deletion + // User | { name: JobName.USER_DELETE_CHECK; data?: IBaseJob } | { name: JobName.USER_DELETION; data: IEntityJob } + | { name: JobName.USER_SYNC_USAGE; data?: IBaseJob } // Storage Template | { name: JobName.STORAGE_TEMPLATE_MIGRATION; data?: IBaseJob } diff --git a/server/src/domain/repositories/user.repository.ts b/server/src/domain/repositories/user.repository.ts index 76060c3270b60..26e251148135a 100644 --- a/server/src/domain/repositories/user.repository.ts +++ b/server/src/domain/repositories/user.repository.ts @@ -10,6 +10,7 @@ export interface UserStatsQueryResponse { photos: number; videos: number; usage: number; + quotaSizeInBytes: number | null; } export interface UserFindOptions { @@ -32,4 +33,6 @@ export interface IUserRepository { update(id: string, user: Partial): Promise; delete(user: UserEntity, hard?: boolean): Promise; restore(user: UserEntity): Promise; + updateUsage(id: string, delta: number): Promise; + syncUsage(): Promise; } diff --git a/server/src/domain/server-info/server-info.dto.ts b/server/src/domain/server-info/server-info.dto.ts index ea0d03b881fc0..8470da8bcd1bf 100644 --- a/server/src/domain/server-info/server-info.dto.ts +++ b/server/src/domain/server-info/server-info.dto.ts @@ -45,6 +45,8 @@ export class UsageByUserDto { videos!: number; @ApiProperty({ type: 'integer', format: 'int64' }) usage!: number; + @ApiProperty({ type: 'integer', format: 'int64' }) + quotaSizeInBytes!: number | null; } export class ServerStatsResponseDto { diff --git a/server/src/domain/server-info/server-info.service.spec.ts b/server/src/domain/server-info/server-info.service.spec.ts index a8cd82443a3aa..4bc1b10443af6 100644 --- a/server/src/domain/server-info/server-info.service.spec.ts +++ b/server/src/domain/server-info/server-info.service.spec.ts @@ -220,6 +220,7 @@ describe(ServerInfoService.name, () => { photos: 10, videos: 11, usage: 12345, + quotaSizeInBytes: 0, }, { userId: 'user2', @@ -227,6 +228,7 @@ describe(ServerInfoService.name, () => { photos: 10, videos: 20, usage: 123456, + quotaSizeInBytes: 0, }, { userId: 'user3', @@ -234,6 +236,7 @@ describe(ServerInfoService.name, () => { photos: 100, videos: 0, usage: 987654, + quotaSizeInBytes: 0, }, ]); @@ -244,6 +247,7 @@ describe(ServerInfoService.name, () => { usageByUser: [ { photos: 10, + quotaSizeInBytes: 0, usage: 12345, userName: '1 User', userId: 'user1', @@ -251,6 +255,7 @@ describe(ServerInfoService.name, () => { }, { photos: 10, + quotaSizeInBytes: 0, usage: 123456, userName: '2 User', userId: 'user2', @@ -258,6 +263,7 @@ describe(ServerInfoService.name, () => { }, { photos: 100, + quotaSizeInBytes: 0, usage: 987654, userName: '3 User', userId: 'user3', diff --git a/server/src/domain/server-info/server-info.service.ts b/server/src/domain/server-info/server-info.service.ts index c16c62fb23801..7da045a18a289 100644 --- a/server/src/domain/server-info/server-info.service.ts +++ b/server/src/domain/server-info/server-info.service.ts @@ -118,6 +118,7 @@ export class ServerInfoService { usage.photos = user.photos; usage.videos = user.videos; usage.usage = user.usage; + usage.quotaSizeInBytes = user.quotaSizeInBytes; serverStats.photos += usage.photos; serverStats.videos += usage.videos; diff --git a/server/src/domain/user/dto/create-user.dto.ts b/server/src/domain/user/dto/create-user.dto.ts index b1090d9c0e85b..179974eae8b85 100644 --- a/server/src/domain/user/dto/create-user.dto.ts +++ b/server/src/domain/user/dto/create-user.dto.ts @@ -1,5 +1,6 @@ +import { ApiProperty } from '@nestjs/swagger'; import { Transform } from 'class-transformer'; -import { IsBoolean, IsEmail, IsNotEmpty, IsString } from 'class-validator'; +import { IsBoolean, IsEmail, IsNotEmpty, IsNumber, IsPositive, IsString } from 'class-validator'; import { Optional, toEmail, toSanitized } from '../../domain.util'; export class CreateUserDto { @@ -27,6 +28,12 @@ export class CreateUserDto { @Optional() @IsBoolean() memoriesEnabled?: boolean; + + @Optional({ nullable: true }) + @IsNumber() + @IsPositive() + @ApiProperty({ type: 'integer', format: 'int64' }) + quotaSizeInBytes?: number | null; } export class CreateAdminDto { diff --git a/server/src/domain/user/dto/update-user.dto.ts b/server/src/domain/user/dto/update-user.dto.ts index a71c0e21a5c12..3c6977ff053e5 100644 --- a/server/src/domain/user/dto/update-user.dto.ts +++ b/server/src/domain/user/dto/update-user.dto.ts @@ -1,7 +1,7 @@ import { UserAvatarColor } from '@app/infra/entities'; import { ApiProperty } from '@nestjs/swagger'; import { Transform } from 'class-transformer'; -import { IsBoolean, IsEmail, IsEnum, IsNotEmpty, IsString, IsUUID } from 'class-validator'; +import { IsBoolean, IsEmail, IsEnum, IsNotEmpty, IsNumber, IsPositive, IsString, IsUUID } from 'class-validator'; import { Optional, toEmail, toSanitized } from '../../domain.util'; export class UpdateUserDto { @@ -50,4 +50,10 @@ export class UpdateUserDto { @IsEnum(UserAvatarColor) @ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor }) avatarColor?: UserAvatarColor; + + @Optional({ nullable: true }) + @IsNumber() + @IsPositive() + @ApiProperty({ type: 'integer', format: 'int64' }) + quotaSizeInBytes?: number | null; } diff --git a/server/src/domain/user/response-dto/user-response.dto.ts b/server/src/domain/user/response-dto/user-response.dto.ts index 7b6aef1910269..7ef0b98b3c1fd 100644 --- a/server/src/domain/user/response-dto/user-response.dto.ts +++ b/server/src/domain/user/response-dto/user-response.dto.ts @@ -33,6 +33,10 @@ export class UserResponseDto extends UserDto { updatedAt!: Date; oauthId!: string; memoriesEnabled?: boolean; + @ApiProperty({ type: 'integer', format: 'int64' }) + quotaSizeInBytes!: number | null; + @ApiProperty({ type: 'integer', format: 'int64' }) + quotaUsageInBytes!: number; } export const mapSimpleUser = (entity: UserEntity): UserDto => { @@ -57,5 +61,7 @@ export function mapUser(entity: UserEntity): UserResponseDto { updatedAt: entity.updatedAt, oauthId: entity.oauthId, memoriesEnabled: entity.memoriesEnabled, + quotaSizeInBytes: entity.quotaSizeInBytes, + quotaUsageInBytes: entity.quotaUsageInBytes, }; } diff --git a/server/src/domain/user/user.service.spec.ts b/server/src/domain/user/user.service.spec.ts index 45fe4a9025050..78743a0439697 100644 --- a/server/src/domain/user/user.service.spec.ts +++ b/server/src/domain/user/user.service.spec.ts @@ -512,4 +512,11 @@ describe(UserService.name, () => { expect(storageMock.unlinkDir).toHaveBeenCalledWith('upload/library/admin', options); }); }); + + describe('handleUserSyncUsage', () => { + it('should sync usage', async () => { + await sut.handleUserSyncUsage(); + expect(userMock.syncUsage).toHaveBeenCalledTimes(1); + }); + }); }); diff --git a/server/src/domain/user/user.service.ts b/server/src/domain/user/user.service.ts index 8185a83c6446e..136b9c7cfdbbc 100644 --- a/server/src/domain/user/user.service.ts +++ b/server/src/domain/user/user.service.ts @@ -127,6 +127,11 @@ export class UserService { return { admin, password, provided: !!providedPassword }; } + async handleUserSyncUsage() { + await this.userRepository.syncUsage(); + return true; + } + async handleUserDeleteCheck() { const users = await this.userRepository.getDeletedUsers(); await this.jobRepository.queueAll( diff --git a/server/src/immich/api-v1/asset/asset-repository.ts b/server/src/immich/api-v1/asset/asset-repository.ts index 88e558680f6da..5adf663cb47b6 100644 --- a/server/src/immich/api-v1/asset/asset-repository.ts +++ b/server/src/immich/api-v1/asset/asset-repository.ts @@ -1,5 +1,5 @@ import { AssetCreate } from '@app/domain'; -import { AssetEntity } from '@app/infra/entities'; +import { AssetEntity, ExifEntity } from '@app/infra/entities'; import { OptionalBetween } from '@app/infra/infra.utils'; import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; @@ -23,6 +23,7 @@ export interface AssetOwnerCheck extends AssetCheck { export interface IAssetRepository { get(id: string): Promise; create(asset: AssetCreate): Promise; + upsertExif(exif: Partial): Promise; getAllByUserId(userId: string, dto: AssetSearchDto): Promise; getAllByDeviceId(userId: string, deviceId: string): Promise; getById(assetId: string): Promise; @@ -38,7 +39,10 @@ export const IAssetRepository = 'IAssetRepository'; @Injectable() export class AssetRepository implements IAssetRepository { - constructor(@InjectRepository(AssetEntity) private assetRepository: Repository) {} + constructor( + @InjectRepository(AssetEntity) private assetRepository: Repository, + @InjectRepository(ExifEntity) private exifRepository: Repository, + ) {} getSearchPropertiesByUserId(userId: string): Promise { return this.assetRepository @@ -162,6 +166,10 @@ export class AssetRepository implements IAssetRepository { return this.assetRepository.save(asset); } + async upsertExif(exif: Partial): Promise { + await this.exifRepository.upsert(exif, { conflictPaths: ['assetId'] }); + } + /** * Get assets by device's Id on the database * @param ownerId diff --git a/server/src/immich/api-v1/asset/asset.core.ts b/server/src/immich/api-v1/asset/asset.core.ts index 9e85dfc58d169..e3fd6365e29bf 100644 --- a/server/src/immich/api-v1/asset/asset.core.ts +++ b/server/src/immich/api-v1/asset/asset.core.ts @@ -1,5 +1,6 @@ import { AuthDto, IJobRepository, JobName, mimeTypes, UploadFile } from '@app/domain'; import { AssetEntity } from '@app/infra/entities'; +import { BadRequestException } from '@nestjs/common'; import { parse } from 'node:path'; import { IAssetRepository } from './asset-repository'; import { CreateAssetDto } from './dto/create-asset.dto'; @@ -52,8 +53,15 @@ export class AssetCore { isOffline: dto.isOffline ?? false, }); + await this.repository.upsertExif({ assetId: asset.id, fileSizeInByte: file.size }); await this.jobRepository.queue({ name: JobName.METADATA_EXTRACTION, data: { id: asset.id, source: 'upload' } }); return asset; } + + static requireQuota(auth: AuthDto, size: number) { + if (auth.user.quotaSizeInBytes && auth.user.quotaSizeInBytes < auth.user.quotaUsageInBytes + size) { + throw new BadRequestException('Quota has been exceeded!'); + } + } } diff --git a/server/src/immich/api-v1/asset/asset.service.spec.ts b/server/src/immich/api-v1/asset/asset.service.spec.ts index d603e92268977..3c1e5914b3124 100644 --- a/server/src/immich/api-v1/asset/asset.service.spec.ts +++ b/server/src/immich/api-v1/asset/asset.service.spec.ts @@ -1,4 +1,4 @@ -import { IJobRepository, ILibraryRepository, JobName } from '@app/domain'; +import { IJobRepository, ILibraryRepository, IUserRepository, JobName } from '@app/domain'; import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity, AssetType, ExifEntity } from '@app/infra/entities'; import { BadRequestException } from '@nestjs/common'; import { @@ -9,6 +9,7 @@ import { newAccessRepositoryMock, newJobRepositoryMock, newLibraryRepositoryMock, + newUserRepositoryMock, } from '@test'; import { when } from 'jest-when'; import { QueryFailedError } from 'typeorm'; @@ -87,11 +88,13 @@ describe('AssetService', () => { let assetRepositoryMock: jest.Mocked; let jobMock: jest.Mocked; let libraryMock: jest.Mocked; + let userMock: jest.Mocked; beforeEach(() => { assetRepositoryMock = { get: jest.fn(), create: jest.fn(), + upsertExif: jest.fn(), getAllByUserId: jest.fn(), getAllByDeviceId: jest.fn(), @@ -107,8 +110,9 @@ describe('AssetService', () => { accessMock = newAccessRepositoryMock(); jobMock = newJobRepositoryMock(); libraryMock = newLibraryRepositoryMock(); + userMock = newUserRepositoryMock(); - sut = new AssetService(accessMock, assetRepositoryMock, jobMock, libraryMock); + sut = new AssetService(accessMock, assetRepositoryMock, jobMock, libraryMock, userMock); when(assetRepositoryMock.get) .calledWith(assetStub.livePhotoStillAsset.id) @@ -127,6 +131,7 @@ describe('AssetService', () => { mimeType: 'image/jpeg', checksum: Buffer.from('file hash', 'utf8'), originalName: 'asset_1.jpeg', + size: 42, }; const dto = _getCreateAssetDto(); @@ -136,6 +141,7 @@ describe('AssetService', () => { await expect(sut.uploadFile(authStub.user1, dto, file)).resolves.toEqual({ duplicate: false, id: 'id_1' }); expect(assetRepositoryMock.create).toHaveBeenCalled(); + expect(userMock.updateUsage).toHaveBeenCalledWith(authStub.user1.user.id, file.size); }); it('should handle a duplicate', async () => { @@ -145,6 +151,7 @@ describe('AssetService', () => { mimeType: 'image/jpeg', checksum: Buffer.from('file hash', 'utf8'), originalName: 'asset_1.jpeg', + size: 0, }; const dto = _getCreateAssetDto(); const error = new QueryFailedError('', [], ''); @@ -160,6 +167,7 @@ describe('AssetService', () => { name: JobName.DELETE_FILES, data: { files: ['fake_path/asset_1.jpeg', undefined, undefined] }, }); + expect(userMock.updateUsage).not.toHaveBeenCalled(); }); it('should handle a live photo', async () => { @@ -187,6 +195,7 @@ describe('AssetService', () => { ], [{ name: JobName.METADATA_EXTRACTION, data: { id: assetStub.livePhotoStillAsset.id, source: 'upload' } }], ]); + expect(userMock.updateUsage).toHaveBeenCalledWith(authStub.user1.user.id, 111); }); }); diff --git a/server/src/immich/api-v1/asset/asset.service.ts b/server/src/immich/api-v1/asset/asset.service.ts index 4a4492f94bbc7..6cfdfff541365 100644 --- a/server/src/immich/api-v1/asset/asset.service.ts +++ b/server/src/immich/api-v1/asset/asset.service.ts @@ -8,6 +8,7 @@ import { IJobRepository, ILibraryRepository, ImmichFileResponse, + IUserRepository, JobName, mapAsset, mimeTypes, @@ -49,6 +50,7 @@ export class AssetService { @Inject(IAssetRepository) private _assetRepository: IAssetRepository, @Inject(IJobRepository) private jobRepository: IJobRepository, @Inject(ILibraryRepository) private libraryRepository: ILibraryRepository, + @Inject(IUserRepository) private userRepository: IUserRepository, ) { this.assetCore = new AssetCore(_assetRepository, jobRepository); this.access = AccessCore.create(accessRepository); @@ -73,6 +75,7 @@ export class AssetService { try { const libraryId = await this.getLibraryId(auth, dto.libraryId); await this.access.requirePermission(auth, Permission.ASSET_UPLOAD, libraryId); + AssetCore.requireQuota(auth, file.size); if (livePhotoFile) { const livePhotoDto = { ...dto, assetType: AssetType.VIDEO, isVisible: false, libraryId }; livePhotoAsset = await this.assetCore.create(auth, livePhotoDto, livePhotoFile); @@ -86,6 +89,8 @@ export class AssetService { sidecarFile?.originalPath, ); + await this.userRepository.updateUsage(auth.user.id, (livePhotoFile?.size || 0) + file.size); + return { id: asset.id, duplicate: false }; } catch (error: any) { // clean up files diff --git a/server/src/immich/app.module.ts b/server/src/immich/app.module.ts index ca346697a2df8..ff53401df0e3a 100644 --- a/server/src/immich/app.module.ts +++ b/server/src/immich/app.module.ts @@ -1,6 +1,6 @@ import { DomainModule } from '@app/domain'; import { InfraModule } from '@app/infra'; -import { AssetEntity } from '@app/infra/entities'; +import { AssetEntity, ExifEntity } from '@app/infra/entities'; import { Module, OnModuleInit } from '@nestjs/common'; import { APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core'; import { ScheduleModule } from '@nestjs/schedule'; @@ -40,7 +40,7 @@ import { ErrorInterceptor, FileUploadInterceptor } from './interceptors'; InfraModule, DomainModule, ScheduleModule.forRoot(), - TypeOrmModule.forFeature([AssetEntity]), + TypeOrmModule.forFeature([AssetEntity, ExifEntity]), ], controllers: [ ActivityController, diff --git a/server/src/immich/interceptors/file-upload.interceptor.ts b/server/src/immich/interceptors/file-upload.interceptor.ts index 425229f245261..d94761d44a8be 100644 --- a/server/src/immich/interceptors/file-upload.interceptor.ts +++ b/server/src/immich/interceptors/file-upload.interceptor.ts @@ -27,6 +27,7 @@ export function mapToUploadFile(file: ImmichFile): UploadFile { checksum: file.checksum, originalPath: file.path, originalName: Buffer.from(file.originalname, 'latin1').toString('utf8'), + size: file.size, }; } diff --git a/server/src/infra/entities/user.entity.ts b/server/src/infra/entities/user.entity.ts index 5a0a6afd6c89c..dbc563ee0ac9e 100644 --- a/server/src/infra/entities/user.entity.ts +++ b/server/src/infra/entities/user.entity.ts @@ -75,4 +75,10 @@ export class UserEntity { @OneToMany(() => AssetEntity, (asset) => asset.owner) assets!: AssetEntity[]; + + @Column({ type: 'bigint', nullable: true }) + quotaSizeInBytes!: number | null; + + @Column({ type: 'bigint', default: 0 }) + quotaUsageInBytes!: number; } diff --git a/server/src/infra/migrations/1704382918223-AddQuotaColumnsToUser.ts b/server/src/infra/migrations/1704382918223-AddQuotaColumnsToUser.ts new file mode 100644 index 0000000000000..5d6477b8fbaa0 --- /dev/null +++ b/server/src/infra/migrations/1704382918223-AddQuotaColumnsToUser.ts @@ -0,0 +1,16 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddQuotaColumnsToUser1704382918223 implements MigrationInterface { + name = 'AddQuotaColumnsToUser1704382918223' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "users" ADD "quotaSizeInBytes" bigint`); + await queryRunner.query(`ALTER TABLE "users" ADD "quotaUsageInBytes" bigint NOT NULL DEFAULT '0'`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "quotaUsageInBytes"`); + await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "quotaSizeInBytes"`); + } + +} diff --git a/server/src/infra/repositories/user.repository.ts b/server/src/infra/repositories/user.repository.ts index d1b1c0d5eab11..639d335987bfa 100644 --- a/server/src/infra/repositories/user.repository.ts +++ b/server/src/infra/repositories/user.repository.ts @@ -2,12 +2,15 @@ import { IUserRepository, UserFindOptions, UserListFilter, UserStatsQueryRespons import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { IsNull, Not, Repository } from 'typeorm'; -import { UserEntity } from '../entities'; +import { AssetEntity, UserEntity } from '../entities'; import { DummyValue, GenerateSql } from '../infra.util'; @Injectable() export class UserRepository implements IUserRepository { - constructor(@InjectRepository(UserEntity) private userRepository: Repository) {} + constructor( + @InjectRepository(AssetEntity) private assetRepository: Repository, + @InjectRepository(UserEntity) private userRepository: Repository, + ) {} async get(userId: string, options: UserFindOptions): Promise { options = options || {}; @@ -91,6 +94,7 @@ export class UserRepository implements IUserRepository { .addSelect(`COUNT(assets.id) FILTER (WHERE assets.type = 'IMAGE' AND assets.isVisible)`, 'photos') .addSelect(`COUNT(assets.id) FILTER (WHERE assets.type = 'VIDEO' AND assets.isVisible)`, 'videos') .addSelect('COALESCE(SUM(exif.fileSizeInByte), 0)', 'usage') + .addSelect('users.quotaSizeInBytes', 'quotaSizeInBytes') .leftJoin('users.assets', 'assets') .leftJoin('assets.exifInfo', 'exif') .groupBy('users.id') @@ -101,11 +105,32 @@ export class UserRepository implements IUserRepository { stat.photos = Number(stat.photos); stat.videos = Number(stat.videos); stat.usage = Number(stat.usage); + stat.quotaSizeInBytes = stat.quotaSizeInBytes; } return stats; } + async updateUsage(id: string, delta: number): Promise { + await this.userRepository.increment({ id }, 'quotaUsageInBytes', delta); + } + + async syncUsage() { + const subQuery = this.assetRepository + .createQueryBuilder('assets') + .select('COALESCE(SUM(exif."fileSizeInByte"), 0)') + .leftJoin('assets.exifInfo', 'exif') + .where('assets.ownerId = users.id') + .withDeleted(); + + await this.userRepository + .createQueryBuilder('users') + .leftJoin('users.assets', 'assets') + .update() + .set({ quotaUsageInBytes: () => `(${subQuery.getQuery()})` }) + .execute(); + } + private async save(user: Partial) { const { id } = await this.userRepository.save(user); return this.userRepository.findOneByOrFail({ id }); diff --git a/server/src/infra/sql/album.repository.sql b/server/src/infra/sql/album.repository.sql index 8b6d1708411b3..a5f32f57a7296 100644 --- a/server/src/infra/sql/album.repository.sql +++ b/server/src/infra/sql/album.repository.sql @@ -29,6 +29,8 @@ FROM "AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt", "AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt", "AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled", + "AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes", + "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes", "AlbumEntity__AlbumEntity_sharedUsers"."id" AS "AlbumEntity__AlbumEntity_sharedUsers_id", "AlbumEntity__AlbumEntity_sharedUsers"."name" AS "AlbumEntity__AlbumEntity_sharedUsers_name", "AlbumEntity__AlbumEntity_sharedUsers"."avatarColor" AS "AlbumEntity__AlbumEntity_sharedUsers_avatarColor", @@ -43,6 +45,8 @@ FROM "AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_deletedAt", "AlbumEntity__AlbumEntity_sharedUsers"."updatedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_updatedAt", "AlbumEntity__AlbumEntity_sharedUsers"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_sharedUsers_memoriesEnabled", + "AlbumEntity__AlbumEntity_sharedUsers"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_sharedUsers_quotaSizeInBytes", + "AlbumEntity__AlbumEntity_sharedUsers"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_sharedUsers_quotaUsageInBytes", "AlbumEntity__AlbumEntity_sharedLinks"."id" AS "AlbumEntity__AlbumEntity_sharedLinks_id", "AlbumEntity__AlbumEntity_sharedLinks"."description" AS "AlbumEntity__AlbumEntity_sharedLinks_description", "AlbumEntity__AlbumEntity_sharedLinks"."password" AS "AlbumEntity__AlbumEntity_sharedLinks_password", @@ -101,6 +105,8 @@ SELECT "AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt", "AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt", "AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled", + "AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes", + "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes", "AlbumEntity__AlbumEntity_sharedUsers"."id" AS "AlbumEntity__AlbumEntity_sharedUsers_id", "AlbumEntity__AlbumEntity_sharedUsers"."name" AS "AlbumEntity__AlbumEntity_sharedUsers_name", "AlbumEntity__AlbumEntity_sharedUsers"."avatarColor" AS "AlbumEntity__AlbumEntity_sharedUsers_avatarColor", @@ -114,7 +120,9 @@ SELECT "AlbumEntity__AlbumEntity_sharedUsers"."createdAt" AS "AlbumEntity__AlbumEntity_sharedUsers_createdAt", "AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_deletedAt", "AlbumEntity__AlbumEntity_sharedUsers"."updatedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_updatedAt", - "AlbumEntity__AlbumEntity_sharedUsers"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_sharedUsers_memoriesEnabled" + "AlbumEntity__AlbumEntity_sharedUsers"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_sharedUsers_memoriesEnabled", + "AlbumEntity__AlbumEntity_sharedUsers"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_sharedUsers_quotaSizeInBytes", + "AlbumEntity__AlbumEntity_sharedUsers"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_sharedUsers_quotaUsageInBytes" FROM "albums" "AlbumEntity" LEFT JOIN "users" "AlbumEntity__AlbumEntity_owner" ON "AlbumEntity__AlbumEntity_owner"."id" = "AlbumEntity"."ownerId" @@ -155,6 +163,8 @@ SELECT "AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt", "AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt", "AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled", + "AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes", + "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes", "AlbumEntity__AlbumEntity_sharedUsers"."id" AS "AlbumEntity__AlbumEntity_sharedUsers_id", "AlbumEntity__AlbumEntity_sharedUsers"."name" AS "AlbumEntity__AlbumEntity_sharedUsers_name", "AlbumEntity__AlbumEntity_sharedUsers"."avatarColor" AS "AlbumEntity__AlbumEntity_sharedUsers_avatarColor", @@ -168,7 +178,9 @@ SELECT "AlbumEntity__AlbumEntity_sharedUsers"."createdAt" AS "AlbumEntity__AlbumEntity_sharedUsers_createdAt", "AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_deletedAt", "AlbumEntity__AlbumEntity_sharedUsers"."updatedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_updatedAt", - "AlbumEntity__AlbumEntity_sharedUsers"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_sharedUsers_memoriesEnabled" + "AlbumEntity__AlbumEntity_sharedUsers"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_sharedUsers_memoriesEnabled", + "AlbumEntity__AlbumEntity_sharedUsers"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_sharedUsers_quotaSizeInBytes", + "AlbumEntity__AlbumEntity_sharedUsers"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_sharedUsers_quotaUsageInBytes" FROM "albums" "AlbumEntity" LEFT JOIN "users" "AlbumEntity__AlbumEntity_owner" ON "AlbumEntity__AlbumEntity_owner"."id" = "AlbumEntity"."ownerId" @@ -273,6 +285,8 @@ SELECT "AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_deletedAt", "AlbumEntity__AlbumEntity_sharedUsers"."updatedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_updatedAt", "AlbumEntity__AlbumEntity_sharedUsers"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_sharedUsers_memoriesEnabled", + "AlbumEntity__AlbumEntity_sharedUsers"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_sharedUsers_quotaSizeInBytes", + "AlbumEntity__AlbumEntity_sharedUsers"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_sharedUsers_quotaUsageInBytes", "AlbumEntity__AlbumEntity_sharedLinks"."id" AS "AlbumEntity__AlbumEntity_sharedLinks_id", "AlbumEntity__AlbumEntity_sharedLinks"."description" AS "AlbumEntity__AlbumEntity_sharedLinks_description", "AlbumEntity__AlbumEntity_sharedLinks"."password" AS "AlbumEntity__AlbumEntity_sharedLinks_password", @@ -298,7 +312,9 @@ SELECT "AlbumEntity__AlbumEntity_owner"."createdAt" AS "AlbumEntity__AlbumEntity_owner_createdAt", "AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt", "AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt", - "AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled" + "AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled", + "AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes", + "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes" FROM "albums" "AlbumEntity" LEFT JOIN "albums_shared_users_users" "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers" ON "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers"."albumsId" = "AlbumEntity"."id" @@ -342,6 +358,8 @@ SELECT "AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_deletedAt", "AlbumEntity__AlbumEntity_sharedUsers"."updatedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_updatedAt", "AlbumEntity__AlbumEntity_sharedUsers"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_sharedUsers_memoriesEnabled", + "AlbumEntity__AlbumEntity_sharedUsers"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_sharedUsers_quotaSizeInBytes", + "AlbumEntity__AlbumEntity_sharedUsers"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_sharedUsers_quotaUsageInBytes", "AlbumEntity__AlbumEntity_sharedLinks"."id" AS "AlbumEntity__AlbumEntity_sharedLinks_id", "AlbumEntity__AlbumEntity_sharedLinks"."description" AS "AlbumEntity__AlbumEntity_sharedLinks_description", "AlbumEntity__AlbumEntity_sharedLinks"."password" AS "AlbumEntity__AlbumEntity_sharedLinks_password", @@ -367,7 +385,9 @@ SELECT "AlbumEntity__AlbumEntity_owner"."createdAt" AS "AlbumEntity__AlbumEntity_owner_createdAt", "AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt", "AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt", - "AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled" + "AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled", + "AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes", + "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes" FROM "albums" "AlbumEntity" LEFT JOIN "albums_shared_users_users" "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers" ON "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers"."albumsId" = "AlbumEntity"."id" @@ -424,6 +444,8 @@ SELECT "AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_deletedAt", "AlbumEntity__AlbumEntity_sharedUsers"."updatedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_updatedAt", "AlbumEntity__AlbumEntity_sharedUsers"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_sharedUsers_memoriesEnabled", + "AlbumEntity__AlbumEntity_sharedUsers"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_sharedUsers_quotaSizeInBytes", + "AlbumEntity__AlbumEntity_sharedUsers"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_sharedUsers_quotaUsageInBytes", "AlbumEntity__AlbumEntity_sharedLinks"."id" AS "AlbumEntity__AlbumEntity_sharedLinks_id", "AlbumEntity__AlbumEntity_sharedLinks"."description" AS "AlbumEntity__AlbumEntity_sharedLinks_description", "AlbumEntity__AlbumEntity_sharedLinks"."password" AS "AlbumEntity__AlbumEntity_sharedLinks_password", @@ -449,7 +471,9 @@ SELECT "AlbumEntity__AlbumEntity_owner"."createdAt" AS "AlbumEntity__AlbumEntity_owner_createdAt", "AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt", "AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt", - "AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled" + "AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled", + "AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes", + "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes" FROM "albums" "AlbumEntity" LEFT JOIN "albums_shared_users_users" "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers" ON "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers"."albumsId" = "AlbumEntity"."id" @@ -498,7 +522,9 @@ SELECT "AlbumEntity__AlbumEntity_owner"."createdAt" AS "AlbumEntity__AlbumEntity_owner_createdAt", "AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt", "AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt", - "AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled" + "AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled", + "AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes", + "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes" FROM "albums" "AlbumEntity" LEFT JOIN "users" "AlbumEntity__AlbumEntity_owner" ON "AlbumEntity__AlbumEntity_owner"."id" = "AlbumEntity"."ownerId" diff --git a/server/src/infra/sql/api.key.repository.sql b/server/src/infra/sql/api.key.repository.sql index 7f26d8575742a..71d8022ca5b98 100644 --- a/server/src/infra/sql/api.key.repository.sql +++ b/server/src/infra/sql/api.key.repository.sql @@ -22,7 +22,9 @@ FROM "APIKeyEntity__APIKeyEntity_user"."createdAt" AS "APIKeyEntity__APIKeyEntity_user_createdAt", "APIKeyEntity__APIKeyEntity_user"."deletedAt" AS "APIKeyEntity__APIKeyEntity_user_deletedAt", "APIKeyEntity__APIKeyEntity_user"."updatedAt" AS "APIKeyEntity__APIKeyEntity_user_updatedAt", - "APIKeyEntity__APIKeyEntity_user"."memoriesEnabled" AS "APIKeyEntity__APIKeyEntity_user_memoriesEnabled" + "APIKeyEntity__APIKeyEntity_user"."memoriesEnabled" AS "APIKeyEntity__APIKeyEntity_user_memoriesEnabled", + "APIKeyEntity__APIKeyEntity_user"."quotaSizeInBytes" AS "APIKeyEntity__APIKeyEntity_user_quotaSizeInBytes", + "APIKeyEntity__APIKeyEntity_user"."quotaUsageInBytes" AS "APIKeyEntity__APIKeyEntity_user_quotaUsageInBytes" FROM "api_keys" "APIKeyEntity" LEFT JOIN "users" "APIKeyEntity__APIKeyEntity_user" ON "APIKeyEntity__APIKeyEntity_user"."id" = "APIKeyEntity"."userId" diff --git a/server/src/infra/sql/library.repository.sql b/server/src/infra/sql/library.repository.sql index 44e6ddcc94168..8dd7828f0b590 100644 --- a/server/src/infra/sql/library.repository.sql +++ b/server/src/infra/sql/library.repository.sql @@ -30,7 +30,9 @@ FROM "LibraryEntity__LibraryEntity_owner"."createdAt" AS "LibraryEntity__LibraryEntity_owner_createdAt", "LibraryEntity__LibraryEntity_owner"."deletedAt" AS "LibraryEntity__LibraryEntity_owner_deletedAt", "LibraryEntity__LibraryEntity_owner"."updatedAt" AS "LibraryEntity__LibraryEntity_owner_updatedAt", - "LibraryEntity__LibraryEntity_owner"."memoriesEnabled" AS "LibraryEntity__LibraryEntity_owner_memoriesEnabled" + "LibraryEntity__LibraryEntity_owner"."memoriesEnabled" AS "LibraryEntity__LibraryEntity_owner_memoriesEnabled", + "LibraryEntity__LibraryEntity_owner"."quotaSizeInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaSizeInBytes", + "LibraryEntity__LibraryEntity_owner"."quotaUsageInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaUsageInBytes" FROM "libraries" "LibraryEntity" LEFT JOIN "users" "LibraryEntity__LibraryEntity_owner" ON "LibraryEntity__LibraryEntity_owner"."id" = "LibraryEntity"."ownerId" @@ -144,7 +146,9 @@ SELECT "LibraryEntity__LibraryEntity_owner"."createdAt" AS "LibraryEntity__LibraryEntity_owner_createdAt", "LibraryEntity__LibraryEntity_owner"."deletedAt" AS "LibraryEntity__LibraryEntity_owner_deletedAt", "LibraryEntity__LibraryEntity_owner"."updatedAt" AS "LibraryEntity__LibraryEntity_owner_updatedAt", - "LibraryEntity__LibraryEntity_owner"."memoriesEnabled" AS "LibraryEntity__LibraryEntity_owner_memoriesEnabled" + "LibraryEntity__LibraryEntity_owner"."memoriesEnabled" AS "LibraryEntity__LibraryEntity_owner_memoriesEnabled", + "LibraryEntity__LibraryEntity_owner"."quotaSizeInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaSizeInBytes", + "LibraryEntity__LibraryEntity_owner"."quotaUsageInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaUsageInBytes" FROM "libraries" "LibraryEntity" LEFT JOIN "users" "LibraryEntity__LibraryEntity_owner" ON "LibraryEntity__LibraryEntity_owner"."id" = "LibraryEntity"."ownerId" @@ -188,7 +192,9 @@ SELECT "LibraryEntity__LibraryEntity_owner"."createdAt" AS "LibraryEntity__LibraryEntity_owner_createdAt", "LibraryEntity__LibraryEntity_owner"."deletedAt" AS "LibraryEntity__LibraryEntity_owner_deletedAt", "LibraryEntity__LibraryEntity_owner"."updatedAt" AS "LibraryEntity__LibraryEntity_owner_updatedAt", - "LibraryEntity__LibraryEntity_owner"."memoriesEnabled" AS "LibraryEntity__LibraryEntity_owner_memoriesEnabled" + "LibraryEntity__LibraryEntity_owner"."memoriesEnabled" AS "LibraryEntity__LibraryEntity_owner_memoriesEnabled", + "LibraryEntity__LibraryEntity_owner"."quotaSizeInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaSizeInBytes", + "LibraryEntity__LibraryEntity_owner"."quotaUsageInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaUsageInBytes" FROM "libraries" "LibraryEntity" LEFT JOIN "users" "LibraryEntity__LibraryEntity_owner" ON "LibraryEntity__LibraryEntity_owner"."id" = "LibraryEntity"."ownerId" @@ -226,7 +232,9 @@ SELECT "LibraryEntity__LibraryEntity_owner"."createdAt" AS "LibraryEntity__LibraryEntity_owner_createdAt", "LibraryEntity__LibraryEntity_owner"."deletedAt" AS "LibraryEntity__LibraryEntity_owner_deletedAt", "LibraryEntity__LibraryEntity_owner"."updatedAt" AS "LibraryEntity__LibraryEntity_owner_updatedAt", - "LibraryEntity__LibraryEntity_owner"."memoriesEnabled" AS "LibraryEntity__LibraryEntity_owner_memoriesEnabled" + "LibraryEntity__LibraryEntity_owner"."memoriesEnabled" AS "LibraryEntity__LibraryEntity_owner_memoriesEnabled", + "LibraryEntity__LibraryEntity_owner"."quotaSizeInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaSizeInBytes", + "LibraryEntity__LibraryEntity_owner"."quotaUsageInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaUsageInBytes" FROM "libraries" "LibraryEntity" LEFT JOIN "users" "LibraryEntity__LibraryEntity_owner" ON "LibraryEntity__LibraryEntity_owner"."id" = "LibraryEntity"."ownerId" diff --git a/server/src/infra/sql/shared.link.repository.sql b/server/src/infra/sql/shared.link.repository.sql index dc19b23cbd122..a1e827ddc11ee 100644 --- a/server/src/infra/sql/shared.link.repository.sql +++ b/server/src/infra/sql/shared.link.repository.sql @@ -155,7 +155,9 @@ FROM "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."createdAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_createdAt", "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."deletedAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_deletedAt", "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."updatedAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_updatedAt", - "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."memoriesEnabled" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_memoriesEnabled" + "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."memoriesEnabled" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_memoriesEnabled", + "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."quotaSizeInBytes" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_quotaSizeInBytes", + "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."quotaUsageInBytes" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_quotaUsageInBytes" FROM "shared_links" "SharedLinkEntity" LEFT JOIN "shared_link__asset" "SharedLinkEntity__SharedLinkEntity_assets_SharedLinkEntity" ON "SharedLinkEntity__SharedLinkEntity_assets_SharedLinkEntity"."sharedLinksId" = "SharedLinkEntity"."id" @@ -257,7 +259,9 @@ SELECT "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."createdAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_createdAt", "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."deletedAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_deletedAt", "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."updatedAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_updatedAt", - "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."memoriesEnabled" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_memoriesEnabled" + "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."memoriesEnabled" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_memoriesEnabled", + "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."quotaSizeInBytes" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_quotaSizeInBytes", + "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."quotaUsageInBytes" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_quotaUsageInBytes" FROM "shared_links" "SharedLinkEntity" LEFT JOIN "shared_link__asset" "SharedLinkEntity__SharedLinkEntity_assets_SharedLinkEntity" ON "SharedLinkEntity__SharedLinkEntity_assets_SharedLinkEntity"."sharedLinksId" = "SharedLinkEntity"."id" @@ -309,7 +313,9 @@ FROM "SharedLinkEntity__SharedLinkEntity_user"."createdAt" AS "SharedLinkEntity__SharedLinkEntity_user_createdAt", "SharedLinkEntity__SharedLinkEntity_user"."deletedAt" AS "SharedLinkEntity__SharedLinkEntity_user_deletedAt", "SharedLinkEntity__SharedLinkEntity_user"."updatedAt" AS "SharedLinkEntity__SharedLinkEntity_user_updatedAt", - "SharedLinkEntity__SharedLinkEntity_user"."memoriesEnabled" AS "SharedLinkEntity__SharedLinkEntity_user_memoriesEnabled" + "SharedLinkEntity__SharedLinkEntity_user"."memoriesEnabled" AS "SharedLinkEntity__SharedLinkEntity_user_memoriesEnabled", + "SharedLinkEntity__SharedLinkEntity_user"."quotaSizeInBytes" AS "SharedLinkEntity__SharedLinkEntity_user_quotaSizeInBytes", + "SharedLinkEntity__SharedLinkEntity_user"."quotaUsageInBytes" AS "SharedLinkEntity__SharedLinkEntity_user_quotaUsageInBytes" FROM "shared_links" "SharedLinkEntity" LEFT JOIN "users" "SharedLinkEntity__SharedLinkEntity_user" ON "SharedLinkEntity__SharedLinkEntity_user"."id" = "SharedLinkEntity"."userId" diff --git a/server/src/infra/sql/user.repository.sql b/server/src/infra/sql/user.repository.sql index d21f1e7b07a18..7440eef9177dc 100644 --- a/server/src/infra/sql/user.repository.sql +++ b/server/src/infra/sql/user.repository.sql @@ -15,7 +15,9 @@ SELECT "UserEntity"."createdAt" AS "UserEntity_createdAt", "UserEntity"."deletedAt" AS "UserEntity_deletedAt", "UserEntity"."updatedAt" AS "UserEntity_updatedAt", - "UserEntity"."memoriesEnabled" AS "UserEntity_memoriesEnabled" + "UserEntity"."memoriesEnabled" AS "UserEntity_memoriesEnabled", + "UserEntity"."quotaSizeInBytes" AS "UserEntity_quotaSizeInBytes", + "UserEntity"."quotaUsageInBytes" AS "UserEntity_quotaUsageInBytes" FROM "users" "UserEntity" WHERE @@ -60,7 +62,9 @@ SELECT "user"."createdAt" AS "user_createdAt", "user"."deletedAt" AS "user_deletedAt", "user"."updatedAt" AS "user_updatedAt", - "user"."memoriesEnabled" AS "user_memoriesEnabled" + "user"."memoriesEnabled" AS "user_memoriesEnabled", + "user"."quotaSizeInBytes" AS "user_quotaSizeInBytes", + "user"."quotaUsageInBytes" AS "user_quotaUsageInBytes" FROM "users" "user" WHERE @@ -82,7 +86,9 @@ SELECT "UserEntity"."createdAt" AS "UserEntity_createdAt", "UserEntity"."deletedAt" AS "UserEntity_deletedAt", "UserEntity"."updatedAt" AS "UserEntity_updatedAt", - "UserEntity"."memoriesEnabled" AS "UserEntity_memoriesEnabled" + "UserEntity"."memoriesEnabled" AS "UserEntity_memoriesEnabled", + "UserEntity"."quotaSizeInBytes" AS "UserEntity_quotaSizeInBytes", + "UserEntity"."quotaUsageInBytes" AS "UserEntity_quotaUsageInBytes" FROM "users" "UserEntity" WHERE @@ -106,7 +112,9 @@ SELECT "UserEntity"."createdAt" AS "UserEntity_createdAt", "UserEntity"."deletedAt" AS "UserEntity_deletedAt", "UserEntity"."updatedAt" AS "UserEntity_updatedAt", - "UserEntity"."memoriesEnabled" AS "UserEntity_memoriesEnabled" + "UserEntity"."memoriesEnabled" AS "UserEntity_memoriesEnabled", + "UserEntity"."quotaSizeInBytes" AS "UserEntity_quotaSizeInBytes", + "UserEntity"."quotaUsageInBytes" AS "UserEntity_quotaUsageInBytes" FROM "users" "UserEntity" WHERE @@ -119,6 +127,7 @@ LIMIT SELECT "users"."id" AS "userId", "users"."name" AS "userName", + "users"."quotaSizeInBytes" AS "quotaSizeInBytes", COUNT("assets"."id") FILTER ( WHERE "assets"."type" = 'IMAGE' diff --git a/server/src/infra/sql/user.token.repository.sql b/server/src/infra/sql/user.token.repository.sql index e1d1906222722..576dea5c4fd40 100644 --- a/server/src/infra/sql/user.token.repository.sql +++ b/server/src/infra/sql/user.token.repository.sql @@ -25,7 +25,9 @@ FROM "UserTokenEntity__UserTokenEntity_user"."createdAt" AS "UserTokenEntity__UserTokenEntity_user_createdAt", "UserTokenEntity__UserTokenEntity_user"."deletedAt" AS "UserTokenEntity__UserTokenEntity_user_deletedAt", "UserTokenEntity__UserTokenEntity_user"."updatedAt" AS "UserTokenEntity__UserTokenEntity_user_updatedAt", - "UserTokenEntity__UserTokenEntity_user"."memoriesEnabled" AS "UserTokenEntity__UserTokenEntity_user_memoriesEnabled" + "UserTokenEntity__UserTokenEntity_user"."memoriesEnabled" AS "UserTokenEntity__UserTokenEntity_user_memoriesEnabled", + "UserTokenEntity__UserTokenEntity_user"."quotaSizeInBytes" AS "UserTokenEntity__UserTokenEntity_user_quotaSizeInBytes", + "UserTokenEntity__UserTokenEntity_user"."quotaUsageInBytes" AS "UserTokenEntity__UserTokenEntity_user_quotaUsageInBytes" FROM "user_token" "UserTokenEntity" LEFT JOIN "users" "UserTokenEntity__UserTokenEntity_user" ON "UserTokenEntity__UserTokenEntity_user"."id" = "UserTokenEntity"."userId" diff --git a/server/src/microservices/app.service.ts b/server/src/microservices/app.service.ts index fe734bbff78ce..14c660488871c 100644 --- a/server/src/microservices/app.service.ts +++ b/server/src/microservices/app.service.ts @@ -45,6 +45,7 @@ export class AppService { [JobName.CLEAN_OLD_AUDIT_LOGS]: () => this.auditService.handleCleanup(), [JobName.USER_DELETE_CHECK]: () => this.userService.handleUserDeleteCheck(), [JobName.USER_DELETION]: (data) => this.userService.handleUserDelete(data), + [JobName.USER_SYNC_USAGE]: () => this.userService.handleUserSyncUsage(), [JobName.QUEUE_ENCODE_CLIP]: (data) => this.smartInfoService.handleQueueEncodeClip(data), [JobName.ENCODE_CLIP]: (data) => this.smartInfoService.handleEncodeClip(data), [JobName.STORAGE_TEMPLATE_MIGRATION]: () => this.storageTemplateService.handleMigration(), diff --git a/server/test/fixtures/file.stub.ts b/server/test/fixtures/file.stub.ts index a313204b83eb5..fff737893992c 100644 --- a/server/test/fixtures/file.stub.ts +++ b/server/test/fixtures/file.stub.ts @@ -4,11 +4,13 @@ export const fileStub = { originalPath: 'fake_path/asset_1.jpeg', checksum: Buffer.from('file hash', 'utf8'), originalName: 'asset_1.jpeg', + size: 42, }), livePhotoMotion: Object.freeze({ uuid: 'random-uuid', originalPath: 'fake_path/asset_1.mp4', checksum: Buffer.from('live photo file hash', 'utf8'), originalName: 'asset_1.mp4', + size: 69, }), }; diff --git a/server/test/fixtures/user.stub.ts b/server/test/fixtures/user.stub.ts index 3837e67204a66..770e615306dc0 100644 --- a/server/test/fixtures/user.stub.ts +++ b/server/test/fixtures/user.stub.ts @@ -17,6 +17,12 @@ export const userDto = { password: 'Password123', name: 'User 3', }, + userWithQuota: { + email: 'quota-user@immich.app', + password: 'Password123', + name: 'User with quota', + quotaSizeInBytes: 42, + }, }; export const userStub = { @@ -36,6 +42,8 @@ export const userStub = { assets: [], memoriesEnabled: true, avatarColor: UserAvatarColor.PRIMARY, + quotaSizeInBytes: null, + quotaUsageInBytes: 0, }), user1: Object.freeze({ ...authStub.user1.user, @@ -53,6 +61,8 @@ export const userStub = { assets: [], memoriesEnabled: true, avatarColor: UserAvatarColor.PRIMARY, + quotaSizeInBytes: null, + quotaUsageInBytes: 0, }), user2: Object.freeze({ ...authStub.user2.user, @@ -70,6 +80,8 @@ export const userStub = { assets: [], memoriesEnabled: true, avatarColor: UserAvatarColor.PRIMARY, + quotaSizeInBytes: null, + quotaUsageInBytes: 0, }), storageLabel: Object.freeze({ ...authStub.user1.user, @@ -87,6 +99,8 @@ export const userStub = { assets: [], memoriesEnabled: true, avatarColor: UserAvatarColor.PRIMARY, + quotaSizeInBytes: null, + quotaUsageInBytes: 0, }), externalPath1: Object.freeze({ ...authStub.user1.user, @@ -104,6 +118,8 @@ export const userStub = { assets: [], memoriesEnabled: true, avatarColor: UserAvatarColor.PRIMARY, + quotaSizeInBytes: null, + quotaUsageInBytes: 0, }), externalPath2: Object.freeze({ ...authStub.user1.user, @@ -121,6 +137,8 @@ export const userStub = { assets: [], memoriesEnabled: true, avatarColor: UserAvatarColor.PRIMARY, + quotaSizeInBytes: null, + quotaUsageInBytes: 0, }), profilePath: Object.freeze({ ...authStub.user1.user, @@ -138,5 +156,7 @@ export const userStub = { assets: [], memoriesEnabled: true, avatarColor: UserAvatarColor.PRIMARY, + quotaSizeInBytes: null, + quotaUsageInBytes: 0, }), }; diff --git a/server/test/repositories/user.repository.mock.ts b/server/test/repositories/user.repository.mock.ts index d164bbe10f997..e365a20bd5b26 100644 --- a/server/test/repositories/user.repository.mock.ts +++ b/server/test/repositories/user.repository.mock.ts @@ -19,5 +19,7 @@ export const newUserRepositoryMock = (reset = true): jest.Mocked