1
0
mirror of https://github.com/immich-app/immich.git synced 2025-01-11 06:10:28 +02:00

infra(server)!: fix typeorm asset entity relations (#1782)

* fix: add correct relations to asset typeorm entity

* fix: add missing createdAt column to asset entity

* ci: run check to make sure generated API is up-to-date

* ci: cancel workflows that aren't for the latest commit in a branch

* chore: add fvm config for flutter
This commit is contained in:
Zack Pollard 2023-02-19 16:44:53 +00:00 committed by GitHub
parent 000d0a08f4
commit 5ad4e5b614
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
65 changed files with 364 additions and 238 deletions

View File

@ -11,6 +11,10 @@ on:
push:
branches: [main]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build-sign-android:
name: Build and sign Android
@ -24,7 +28,7 @@ jobs:
github_ref="${{ github.sha }}"
ref="${input_ref:-$github_ref}"
echo "ref=$ref" >> $GITHUB_OUTPUT
- uses: actions/checkout@v3
with:
ref: ${{ steps.get-ref.outputs.ref }}

View File

@ -4,24 +4,28 @@ on:
types:
- closed
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
cleanup:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v3
- name: Cleanup
run: |
gh extension install actions/gh-actions-cache
REPO=${{ github.repository }}
BRANCH=${{ github.ref }}
echo "Fetching list of cache keys"
cacheKeysForPR=$(gh actions-cache list -R $REPO -B $BRANCH -L 100 | cut -f 1 )
## Setting this to not fail the workflow while deleting cache keys.
## Setting this to not fail the workflow while deleting cache keys.
set +e
echo "Deleting caches..."
for cacheKey in $cacheKeysForPR

View File

@ -20,6 +20,10 @@ on:
schedule:
- cron: '20 13 * * 1'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
analyze:
name: Analyze
@ -48,11 +52,11 @@ jobs:
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
@ -61,7 +65,7 @@ jobs:
# ℹ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# If the Autobuild fails above, remove it and uncomment the following three lines.
# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# - run: |

View File

@ -5,6 +5,10 @@ on:
push:
branches: ["main"]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
update-sdk-repos:
runs-on: ubuntu-latest

View File

@ -9,6 +9,10 @@ on:
release:
types: [published]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build_and_push:
runs-on: ubuntu-latest

View File

@ -7,6 +7,10 @@ on:
- cron: "0 23 * * *"
workflow_dispatch: # Allow for running this manually.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
j1:
name: github-repo-stats

View File

@ -17,13 +17,17 @@ on:
required: false
type: boolean
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
bump_version:
runs-on: ubuntu-latest
outputs:
ref: ${{ steps.push-tag.outputs.commit_long_sha }}
steps:
- name: Checkout
uses: actions/checkout@v3
@ -42,7 +46,7 @@ jobs:
message: "Version ${{ env.IMMICH_VERSION }}"
tag: ${{ env.IMMICH_VERSION }}
push: true
build_mobile:
uses: ./.github/workflows/build-mobile.yml
needs: bump_version
@ -59,7 +63,7 @@ jobs:
uses: actions/checkout@v3
with:
token: ${{ secrets.ORG_RELEASE_TOKEN }}
- name: Download APK
uses: actions/download-artifact@v3
with:

View File

@ -5,6 +5,10 @@ on:
push:
branches: [main]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
mobile-dart-analyze:
name: Run Dart Code Analysis

View File

@ -5,6 +5,10 @@ on:
push:
branches: [main]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
e2e-tests:
name: Run end-to-end test suites
@ -54,6 +58,27 @@ jobs:
working-directory: ./mobile
run: flutter test
generated-api-up-to-date:
name: Check generated files are up-to-date
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run API generation
run: cd server && npm ci && npm run api:generate
- name: Find file changes
uses: tj-actions/verify-changed-files@v13.1
id: verify-changed-files
with:
files: |
mobile/openapi
web/src/api/open-api
- name: Verify files have not changed
if: steps.verify-changed-files.outputs.files_changed == 'true'
run: |
echo "ERROR: Generated files not up to date!"
echo "Changed files: ${{ steps.verify-changed-files.outputs.changed_files }}"
exit 1
mobile-integration-tests:
name: Run mobile end-to-end integration tests
runs-on: macos-latest

View File

@ -0,0 +1,4 @@
{
"flutterSdkVersion": "3.7.0",
"flavors": {}
}

3
mobile/.gitignore vendored
View File

@ -32,6 +32,7 @@
.pub-cache/
.pub/
/build/
.fvm/flutter_sdk
# Web related
lib/generated_plugin_registrant.dart
@ -48,4 +49,4 @@ app.*.map.json
/android/app/release
# Fastlane
ios/fastlane/report.xml
ios/fastlane/report.xml

View File

@ -120,8 +120,8 @@ class AlbumViewerPage extends HookConsumerWidget {
}
Widget buildAlbumDateRange(Album album) {
final DateTime startDate = album.assets.first.createdAt;
final DateTime endDate = album.assets.last.createdAt; //Need default.
final DateTime startDate = album.assets.first.fileCreatedAt;
final DateTime endDate = album.assets.last.fileCreatedAt; //Need default.
final String startDateText =
DateFormat(startDate.year == endDate.year ? 'LLL d' : 'LLL d, y')
.format(startDate);

View File

@ -146,7 +146,7 @@ class ExifBottomSheet extends HookConsumerWidget {
buildDate() {
return Text(
DateFormat('date_format'.tr()).format(
assetDetail.createdAt.toLocal(),
assetDetail.fileCreatedAt.toLocal(),
),
style: const TextStyle(
fontWeight: FontWeight.bold,

View File

@ -2,26 +2,26 @@ import 'dart:convert';
class CurrentUploadAsset {
final String id;
final DateTime createdAt;
final DateTime fileCreatedAt;
final String fileName;
final String fileType;
CurrentUploadAsset({
required this.id,
required this.createdAt,
required this.fileCreatedAt,
required this.fileName,
required this.fileType,
});
CurrentUploadAsset copyWith({
String? id,
DateTime? createdAt,
DateTime? fileCreatedAt,
String? fileName,
String? fileType,
}) {
return CurrentUploadAsset(
id: id ?? this.id,
createdAt: createdAt ?? this.createdAt,
fileCreatedAt: fileCreatedAt ?? this.fileCreatedAt,
fileName: fileName ?? this.fileName,
fileType: fileType ?? this.fileType,
);
@ -31,7 +31,7 @@ class CurrentUploadAsset {
final result = <String, dynamic>{};
result.addAll({'id': id});
result.addAll({'createdAt': createdAt.millisecondsSinceEpoch});
result.addAll({'fileCreatedAt': fileCreatedAt.millisecondsSinceEpoch});
result.addAll({'fileName': fileName});
result.addAll({'fileType': fileType});
@ -41,7 +41,7 @@ class CurrentUploadAsset {
factory CurrentUploadAsset.fromMap(Map<String, dynamic> map) {
return CurrentUploadAsset(
id: map['id'] ?? '',
createdAt: DateTime.fromMillisecondsSinceEpoch(map['createdAt']),
fileCreatedAt: DateTime.fromMillisecondsSinceEpoch(map['fileCreatedAt']),
fileName: map['fileName'] ?? '',
fileType: map['fileType'] ?? '',
);
@ -54,7 +54,7 @@ class CurrentUploadAsset {
@override
String toString() {
return 'CurrentUploadAsset(id: $id, createdAt: $createdAt, fileName: $fileName, fileType: $fileType)';
return 'CurrentUploadAsset(id: $id, fileCreatedAt: $fileCreatedAt, fileName: $fileName, fileType: $fileType)';
}
@override
@ -63,7 +63,7 @@ class CurrentUploadAsset {
return other is CurrentUploadAsset &&
other.id == id &&
other.createdAt == createdAt &&
other.fileCreatedAt == fileCreatedAt &&
other.fileName == fileName &&
other.fileType == fileType;
}
@ -71,7 +71,7 @@ class CurrentUploadAsset {
@override
int get hashCode {
return id.hashCode ^
createdAt.hashCode ^
fileCreatedAt.hashCode ^
fileName.hashCode ^
fileType.hashCode;
}

View File

@ -2,7 +2,7 @@ import 'package:photo_manager/photo_manager.dart';
class ErrorUploadAsset {
final String id;
final DateTime createdAt;
final DateTime fileCreatedAt;
final String fileName;
final String fileType;
final AssetEntity asset;
@ -10,7 +10,7 @@ class ErrorUploadAsset {
const ErrorUploadAsset({
required this.id,
required this.createdAt,
required this.fileCreatedAt,
required this.fileName,
required this.fileType,
required this.asset,
@ -19,7 +19,7 @@ class ErrorUploadAsset {
ErrorUploadAsset copyWith({
String? id,
DateTime? createdAt,
DateTime? fileCreatedAt,
String? fileName,
String? fileType,
AssetEntity? asset,
@ -27,7 +27,7 @@ class ErrorUploadAsset {
}) {
return ErrorUploadAsset(
id: id ?? this.id,
createdAt: createdAt ?? this.createdAt,
fileCreatedAt: fileCreatedAt ?? this.fileCreatedAt,
fileName: fileName ?? this.fileName,
fileType: fileType ?? this.fileType,
asset: asset ?? this.asset,
@ -37,7 +37,7 @@ class ErrorUploadAsset {
@override
String toString() {
return 'ErrorUploadAsset(id: $id, createdAt: $createdAt, fileName: $fileName, fileType: $fileType, asset: $asset, errorMessage: $errorMessage)';
return 'ErrorUploadAsset(id: $id, fileCreatedAt: $fileCreatedAt, fileName: $fileName, fileType: $fileType, asset: $asset, errorMessage: $errorMessage)';
}
@override
@ -46,7 +46,7 @@ class ErrorUploadAsset {
return other is ErrorUploadAsset &&
other.id == id &&
other.createdAt == createdAt &&
other.fileCreatedAt == fileCreatedAt &&
other.fileName == fileName &&
other.fileType == fileType &&
other.asset == asset &&
@ -56,7 +56,7 @@ class ErrorUploadAsset {
@override
int get hashCode {
return id.hashCode ^
createdAt.hashCode ^
fileCreatedAt.hashCode ^
fileName.hashCode ^
fileType.hashCode ^
asset.hashCode ^

View File

@ -55,7 +55,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
selectedAlbumsBackupAssetsIds: const {},
currentUploadAsset: CurrentUploadAsset(
id: '...',
createdAt: DateTime.parse('2020-10-04'),
fileCreatedAt: DateTime.parse('2020-10-04'),
fileName: '...',
fileType: '...',
),

View File

@ -260,8 +260,8 @@ class BackupService {
req.fields['deviceAssetId'] = entity.id;
req.fields['deviceId'] = deviceId;
req.fields['assetType'] = _getAssetType(entity.type);
req.fields['createdAt'] = entity.createDateTime.toIso8601String();
req.fields['modifiedAt'] = entity.modifiedDateTime.toIso8601String();
req.fields['fileCreatedAt'] = entity.createDateTime.toIso8601String();
req.fields['fileModifiedAt'] = entity.modifiedDateTime.toIso8601String();
req.fields['isFavorite'] = entity.isFavorite.toString();
req.fields['fileExtension'] = fileExtension;
req.fields['duration'] = entity.videoDuration.toString();
@ -278,7 +278,7 @@ class BackupService {
setCurrentUploadAssetCb(
CurrentUploadAsset(
id: entity.id,
createdAt: entity.createDateTime.year == 1970
fileCreatedAt: entity.createDateTime.year == 1970
? entity.modifiedDateTime
: entity.createDateTime,
fileName: originalFileName,
@ -308,7 +308,7 @@ class BackupService {
ErrorUploadAsset(
asset: entity,
id: entity.id,
createdAt: entity.createDateTime,
fileCreatedAt: entity.createDateTime,
fileName: originalFileName,
fileType: _getAssetType(entity.type),
errorMessage: error['error'],

View File

@ -20,7 +20,7 @@ class CurrentUploadingAssetInfoBox extends HookConsumerWidget {
String getAssetCreationDate() {
return DateFormat.yMMMMd('en_US').format(
DateTime.parse(
asset.createdAt.toString(),
asset.fileCreatedAt.toString(),
).toLocal(),
);
}

View File

@ -89,7 +89,7 @@ class FailedBackupStatusPage extends HookConsumerWidget {
Text(
DateFormat.yMMMMd('en_US').format(
DateTime.parse(
errorAsset.createdAt.toString(),
errorAsset.fileCreatedAt.toString(),
).toLocal(),
),
style: TextStyle(

View File

@ -82,14 +82,14 @@ class RenderList {
if (groupBy == GroupAssetsBy.day) {
return assets.groupListsBy(
(element) {
final date = element.createdAt.toLocal();
final date = element.fileCreatedAt.toLocal();
return DateTime(date.year, date.month, date.day);
},
);
} else if (groupBy == GroupAssetsBy.month) {
return assets.groupListsBy(
(element) {
final date = element.createdAt.toLocal();
final date = element.fileCreatedAt.toLocal();
return DateTime(date.year, date.month);
},
);

View File

@ -10,8 +10,8 @@ import 'package:path/path.dart' as p;
class Asset {
Asset.remote(AssetResponseDto remote)
: remoteId = remote.id,
createdAt = DateTime.parse(remote.createdAt),
modifiedAt = DateTime.parse(remote.modifiedAt),
fileCreatedAt = DateTime.parse(remote.fileCreatedAt),
fileModifiedAt = DateTime.parse(remote.fileModifiedAt),
durationInSeconds = remote.duration.toDuration().inSeconds,
fileName = p.basename(remote.originalPath),
height = remote.exifInfo?.exifImageHeight?.toInt(),
@ -37,11 +37,11 @@ class Asset {
deviceAssetId = local.id,
deviceId = Hive.box(userInfoBox).get(deviceIdKey),
ownerId = owner,
modifiedAt = local.modifiedDateTime.toUtc(),
fileModifiedAt = local.modifiedDateTime.toUtc(),
isFavorite = local.isFavorite,
createdAt = local.createDateTime.toUtc() {
if (createdAt.year == 1970) {
createdAt = modifiedAt;
fileCreatedAt = local.createDateTime.toUtc() {
if (fileCreatedAt.year == 1970) {
fileCreatedAt = fileModifiedAt;
}
}
@ -51,8 +51,8 @@ class Asset {
required this.deviceAssetId,
required this.deviceId,
required this.ownerId,
required this.createdAt,
required this.modifiedAt,
required this.fileCreatedAt,
required this.fileModifiedAt,
this.latitude,
this.longitude,
required this.durationInSeconds,
@ -74,10 +74,10 @@ class Asset {
width: width!,
height: height!,
duration: durationInSeconds,
createDateSecond: createdAt.millisecondsSinceEpoch ~/ 1000,
createDateSecond: fileCreatedAt.millisecondsSinceEpoch ~/ 1000,
latitude: latitude,
longitude: longitude,
modifiedDateSecond: modifiedAt.millisecondsSinceEpoch ~/ 1000,
modifiedDateSecond: fileModifiedAt.millisecondsSinceEpoch ~/ 1000,
title: fileName,
);
}
@ -94,9 +94,9 @@ class Asset {
String ownerId;
DateTime createdAt;
DateTime fileCreatedAt;
DateTime modifiedAt;
DateTime fileModifiedAt;
double? latitude;
@ -146,8 +146,8 @@ class Asset {
json["deviceAssetId"] = deviceAssetId;
json["deviceId"] = deviceId;
json["ownerId"] = ownerId;
json["createdAt"] = createdAt.millisecondsSinceEpoch;
json["modifiedAt"] = modifiedAt.millisecondsSinceEpoch;
json["fileCreatedAt"] = fileCreatedAt.millisecondsSinceEpoch;
json["fileModifiedAt"] = fileModifiedAt.millisecondsSinceEpoch;
json["latitude"] = latitude;
json["longitude"] = longitude;
json["durationInSeconds"] = durationInSeconds;
@ -171,10 +171,10 @@ class Asset {
deviceAssetId: json["deviceAssetId"],
deviceId: json["deviceId"],
ownerId: json["ownerId"],
createdAt:
DateTime.fromMillisecondsSinceEpoch(json["createdAt"], isUtc: true),
modifiedAt: DateTime.fromMillisecondsSinceEpoch(
json["modifiedAt"],
fileCreatedAt:
DateTime.fromMillisecondsSinceEpoch(json["fileCreatedAt"], isUtc: true),
fileModifiedAt: DateTime.fromMillisecondsSinceEpoch(
json["fileModifiedAt"],
isUtc: true,
),
latitude: json["latitude"],

View File

@ -302,11 +302,11 @@ final assetGroupByMonthYearProvider = StateProvider((ref) {
ref.watch(assetProvider).allAssets.where((e) => e.isRemote).toList();
assets.sortByCompare<DateTime>(
(e) => e.createdAt,
(e) => e.fileCreatedAt,
(a, b) => b.compareTo(a),
);
return assets.groupListsBy(
(element) => DateFormat('MMMM, y').format(element.createdAt.toLocal()),
(element) => DateFormat('MMMM, y').format(element.fileCreatedAt.toLocal()),
);
});

BIN
mobile/openapi/README.md generated

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1513,5 +1513,5 @@ packages:
source: hosted
version: "3.1.1"
sdks:
dart: ">=2.19.0 <3.0.0"
dart: ">=2.19.0 <4.0.0"
flutter: ">=3.3.0"

View File

@ -16,8 +16,8 @@ void main() {
deviceAssetId: '$i',
deviceId: '',
ownerId: '',
createdAt: date,
modifiedAt: date,
fileCreatedAt: date,
fileModifiedAt: date,
durationInSeconds: 0,
fileName: '',
isFavorite: false,
@ -29,25 +29,25 @@ void main() {
assets.addAll(
testAssets.sublist(0, 5).map((e) {
e.createdAt = DateTime(2022, 1, 5);
e.fileCreatedAt = DateTime(2022, 1, 5);
return e;
}).toList(),
);
assets.addAll(
testAssets.sublist(5, 10).map((e) {
e.createdAt = DateTime(2022, 1, 10);
e.fileCreatedAt = DateTime(2022, 1, 10);
return e;
}).toList(),
);
assets.addAll(
testAssets.sublist(10, 15).map((e) {
e.createdAt = DateTime(2022, 2, 17);
e.fileCreatedAt = DateTime(2022, 2, 17);
return e;
}).toList(),
);
assets.addAll(
testAssets.sublist(15, 30).map((e) {
e.createdAt = DateTime(2022, 10, 15);
e.fileCreatedAt = DateTime(2022, 10, 15);
return e;
}).toList(),
);

View File

@ -79,7 +79,7 @@ export class AlbumRepository implements IAlbumRepository {
const queryProperties: FindManyOptions<AlbumEntity> = {
relations: { sharedUsers: true, assets: true, sharedLinks: true, owner: true },
order: { assets: { createdAt: 'ASC' }, createdAt: 'ASC' },
order: { assets: { fileCreatedAt: 'ASC' }, createdAt: 'ASC' },
};
let albumsQuery: Promise<AlbumEntity[]>;
@ -123,7 +123,7 @@ export class AlbumRepository implements IAlbumRepository {
const albums = await this.albumRepository.find({
where: { ownerId: userId, assets: { id: assetId } },
relations: { owner: true, assets: true, sharedUsers: true },
order: { assets: { createdAt: 'ASC' } },
order: { assets: { fileCreatedAt: 'ASC' } },
});
return albums;
@ -142,7 +142,7 @@ export class AlbumRepository implements IAlbumRepository {
},
order: {
assets: {
createdAt: 'ASC',
fileCreatedAt: 'ASC',
},
},
});

View File

@ -19,7 +19,9 @@ import { AssetSearchDto } from './dto/asset-search.dto';
export interface IAssetRepository {
get(id: string): Promise<AssetEntity | null>;
create(asset: Omit<AssetEntity, 'id'>): Promise<AssetEntity>;
create(
asset: Omit<AssetEntity, 'id' | 'createdAt' | 'updatedAt' | 'ownerId' | 'livePhotoVideoId'>,
): Promise<AssetEntity>;
remove(asset: AssetEntity): Promise<void>;
update(userId: string, asset: AssetEntity, dto: UpdateAssetDto): Promise<AssetEntity>;
@ -112,13 +114,13 @@ export class AssetRepository implements IAssetRepository {
.getMany();
}
async getAssetCountByUserId(userId: string): Promise<AssetCountByUserIdResponseDto> {
async getAssetCountByUserId(ownerId: string): Promise<AssetCountByUserIdResponseDto> {
// Get asset count by AssetType
const items = await this.assetRepository
.createQueryBuilder('asset')
.select(`COUNT(asset.id)`, 'count')
.addSelect(`asset.type`, 'type')
.where('"userId" = :userId', { userId: userId })
.where('"ownerId" = :ownerId', { ownerId: ownerId })
.andWhere('asset.isVisible = true')
.groupBy('asset.type')
.getRawMany();
@ -149,7 +151,7 @@ export class AssetRepository implements IAssetRepository {
// Get asset entity from a list of time buckets
return await this.assetRepository
.createQueryBuilder('asset')
.where('asset.userId = :userId', { userId: userId })
.where('asset.ownerId = :userId', { userId: userId })
.andWhere(`date_trunc('month', "createdAt") IN (:...buckets)`, {
buckets: [...getAssetByTimeBucketDto.timeBucket],
})
@ -167,7 +169,7 @@ export class AssetRepository implements IAssetRepository {
.createQueryBuilder('asset')
.select(`COUNT(asset.id)::int`, 'count')
.addSelect(`date_trunc('month', "createdAt")`, 'timeBucket')
.where('"userId" = :userId', { userId: userId })
.where('"ownerId" = :userId', { userId: userId })
.andWhere('asset.resizePath is not NULL')
.andWhere('asset.isVisible = true')
.groupBy(`date_trunc('month', "createdAt")`)
@ -178,7 +180,7 @@ export class AssetRepository implements IAssetRepository {
.createQueryBuilder('asset')
.select(`COUNT(asset.id)::int`, 'count')
.addSelect(`date_trunc('day', "createdAt")`, 'timeBucket')
.where('"userId" = :userId', { userId: userId })
.where('"ownerId" = :userId', { userId: userId })
.andWhere('asset.resizePath is not NULL')
.andWhere('asset.isVisible = true')
.groupBy(`date_trunc('day', "createdAt")`)
@ -192,7 +194,7 @@ export class AssetRepository implements IAssetRepository {
async getSearchPropertiesByUserId(userId: string): Promise<SearchPropertiesDto[]> {
return await this.assetRepository
.createQueryBuilder('asset')
.where('asset.userId = :userId', { userId: userId })
.where('asset.ownerId = :userId', { userId: userId })
.andWhere('asset.isVisible = true')
.leftJoin('asset.exifInfo', 'ei')
.leftJoin('asset.smartInfo', 'si')
@ -216,7 +218,7 @@ export class AssetRepository implements IAssetRepository {
SELECT DISTINCT ON (unnest(si.objects)) a.id, unnest(si.objects) as "object", a."resizePath", a."deviceAssetId", a."deviceId"
FROM assets a
LEFT JOIN smart_info si ON a.id = si."assetId"
WHERE a."userId" = $1
WHERE a."ownerId" = $1
AND a."isVisible" = true
AND si.objects IS NOT NULL
`,
@ -230,7 +232,7 @@ export class AssetRepository implements IAssetRepository {
SELECT DISTINCT ON (e.city) a.id, e.city, a."resizePath", a."deviceAssetId", a."deviceId"
FROM assets a
LEFT JOIN exif e ON a.id = e."assetId"
WHERE a."userId" = $1
WHERE a."ownerId" = $1
AND a."isVisible" = true
AND e.city IS NOT NULL
AND a.type = 'IMAGE';
@ -255,12 +257,12 @@ export class AssetRepository implements IAssetRepository {
/**
* Get all assets belong to the user on the database
* @param userId
* @param ownerId
*/
async getAllByUserId(userId: string, dto: AssetSearchDto): Promise<AssetEntity[]> {
async getAllByUserId(ownerId: string, dto: AssetSearchDto): Promise<AssetEntity[]> {
return this.assetRepository.find({
where: {
userId,
ownerId,
resizePath: Not(IsNull()),
isVisible: true,
isFavorite: dto.isFavorite,
@ -271,7 +273,7 @@ export class AssetRepository implements IAssetRepository {
},
skip: dto.skip || 0,
order: {
createdAt: 'DESC',
fileCreatedAt: 'DESC',
},
});
}
@ -280,7 +282,9 @@ export class AssetRepository implements IAssetRepository {
return this.assetRepository.findOne({ where: { id } });
}
async create(asset: Omit<AssetEntity, 'id'>): Promise<AssetEntity> {
async create(
asset: Omit<AssetEntity, 'id' | 'createdAt' | 'updatedAt' | 'ownerId' | 'livePhotoVideoId'>,
): Promise<AssetEntity> {
return this.assetRepository.save(asset);
}
@ -304,16 +308,16 @@ export class AssetRepository implements IAssetRepository {
/**
* Get assets by device's Id on the database
* @param userId
* @param ownerId
* @param deviceId
*
* @returns Promise<string[]> - Array of assetIds belong to the device
*/
async getAllByDeviceId(userId: string, deviceId: string): Promise<string[]> {
async getAllByDeviceId(ownerId: string, deviceId: string): Promise<string[]> {
const rows = await this.assetRepository.find({
where: {
userId: userId,
deviceId: deviceId,
ownerId,
deviceId,
isVisible: true,
},
select: ['deviceAssetId'],
@ -326,14 +330,14 @@ export class AssetRepository implements IAssetRepository {
/**
* Get asset by checksum on the database
* @param userId
* @param ownerId
* @param checksum
*
*/
getAssetByChecksum(userId: string, checksum: Buffer): Promise<AssetEntity> {
getAssetByChecksum(ownerId: string, checksum: Buffer): Promise<AssetEntity> {
return this.assetRepository.findOneOrFail({
where: {
userId,
ownerId,
checksum,
},
relations: ['exifInfo'],
@ -341,7 +345,7 @@ export class AssetRepository implements IAssetRepository {
}
async getExistingAssets(
userId: string,
ownerId: string,
checkDuplicateAssetDto: CheckExistingAssetsDto,
): Promise<CheckExistingAssetsResponseDto> {
const existingAssets = await this.assetRepository.find({
@ -349,17 +353,17 @@ export class AssetRepository implements IAssetRepository {
where: {
deviceAssetId: In(checkDuplicateAssetDto.deviceAssetIds),
deviceId: checkDuplicateAssetDto.deviceId,
userId,
ownerId,
},
});
return new CheckExistingAssetsResponseDto(existingAssets.map((a) => a.deviceAssetId));
}
async countByIdAndUser(assetId: string, userId: string): Promise<number> {
async countByIdAndUser(assetId: string, ownerId: string): Promise<number> {
return await this.assetRepository.count({
where: {
id: assetId,
userId,
ownerId,
},
});
}

View File

@ -1,6 +1,5 @@
import { timeUtils } from '@app/common';
import { AuthUserDto, IJobRepository, JobName } from '@app/domain';
import { AssetEntity } from '@app/infra/db/entities';
import { AssetEntity, UserEntity } from '@app/infra/db/entities';
import { StorageService } from '@app/storage';
import { IAssetRepository } from './asset-repository';
import { CreateAssetDto, UploadFile } from './dto/create-asset.dto';
@ -19,24 +18,23 @@ export class AssetCore {
livePhotoAssetId?: string,
): Promise<AssetEntity> {
let asset = await this.repository.create({
userId: authUser.id,
owner: { id: authUser.id } as UserEntity,
mimeType: file.mimeType,
checksum: file.checksum || null,
originalPath: file.originalPath,
createdAt: timeUtils.checkValidTimestamp(dto.createdAt) ? dto.createdAt : new Date().toISOString(),
modifiedAt: timeUtils.checkValidTimestamp(dto.modifiedAt) ? dto.modifiedAt : new Date().toISOString(),
updatedAt: new Date().toISOString(),
deviceAssetId: dto.deviceAssetId,
deviceId: dto.deviceId,
fileCreatedAt: dto.fileCreatedAt,
fileModifiedAt: dto.fileModifiedAt,
type: dto.assetType,
isFavorite: dto.isFavorite,
duration: dto.duration || null,
isVisible: dto.isVisible ?? true,
livePhotoVideoId: livePhotoAssetId || null,
livePhotoVideo: livePhotoAssetId != null ? ({ id: livePhotoAssetId } as AssetEntity) : null,
resizePath: null,
webpPath: null,
encodedVideoPath: null,

View File

@ -27,8 +27,8 @@ const _getCreateAssetDto = (): CreateAssetDto => {
createAssetDto.deviceAssetId = 'deviceAssetId';
createAssetDto.deviceId = 'deviceId';
createAssetDto.assetType = AssetType.OTHER;
createAssetDto.createdAt = '2022-06-19T23:41:36.910Z';
createAssetDto.modifiedAt = '2022-06-19T23:41:36.910Z';
createAssetDto.fileCreatedAt = '2022-06-19T23:41:36.910Z';
createAssetDto.fileModifiedAt = '2022-06-19T23:41:36.910Z';
createAssetDto.isFavorite = false;
createAssetDto.duration = '0:00:00.000000';
@ -39,14 +39,15 @@ const _getAsset_1 = () => {
const asset_1 = new AssetEntity();
asset_1.id = 'id_1';
asset_1.userId = 'user_id_1';
asset_1.ownerId = 'user_id_1';
asset_1.deviceAssetId = 'device_asset_id_1';
asset_1.deviceId = 'device_id_1';
asset_1.type = AssetType.VIDEO;
asset_1.originalPath = 'fake_path/asset_1.jpeg';
asset_1.resizePath = '';
asset_1.createdAt = '2022-06-19T23:41:36.910Z';
asset_1.modifiedAt = '2022-06-19T23:41:36.910Z';
asset_1.fileModifiedAt = '2022-06-19T23:41:36.910Z';
asset_1.fileCreatedAt = '2022-06-19T23:41:36.910Z';
asset_1.updatedAt = '2022-06-19T23:41:36.910Z';
asset_1.isFavorite = false;
asset_1.mimeType = 'image/jpeg';
asset_1.webpPath = '';
@ -59,14 +60,15 @@ const _getAsset_2 = () => {
const asset_2 = new AssetEntity();
asset_2.id = 'id_2';
asset_2.userId = 'user_id_1';
asset_2.ownerId = 'user_id_1';
asset_2.deviceAssetId = 'device_asset_id_2';
asset_2.deviceId = 'device_id_1';
asset_2.type = AssetType.VIDEO;
asset_2.originalPath = 'fake_path/asset_2.jpeg';
asset_2.resizePath = '';
asset_2.createdAt = '2022-06-19T23:41:36.910Z';
asset_2.modifiedAt = '2022-06-19T23:41:36.910Z';
asset_2.fileModifiedAt = '2022-06-19T23:41:36.910Z';
asset_2.fileCreatedAt = '2022-06-19T23:41:36.910Z';
asset_2.updatedAt = '2022-06-19T23:41:36.910Z';
asset_2.isFavorite = false;
asset_2.mimeType = 'image/jpeg';
asset_2.webpPath = '';
@ -292,7 +294,7 @@ describe('AssetService', () => {
const asset = {
id: 'live-photo-asset',
originalPath: file.originalPath,
userId: authStub.user1.id,
ownerId: authStub.user1.id,
type: AssetType.IMAGE,
isVisible: true,
} as AssetEntity;
@ -307,7 +309,7 @@ describe('AssetService', () => {
const livePhotoAsset = {
id: 'live-photo-motion',
originalPath: livePhotoFile.originalPath,
userId: authStub.user1.id,
ownerId: authStub.user1.id,
type: AssetType.VIDEO,
isVisible: false,
} as AssetEntity;

View File

@ -518,7 +518,7 @@ export class AssetService {
where: {
deviceAssetId: checkDuplicateAssetDto.deviceAssetId,
deviceId: checkDuplicateAssetDto.deviceId,
userId: authUser.id,
ownerId: authUser.id,
},
});

View File

@ -16,10 +16,10 @@ export class CreateAssetDto {
assetType!: AssetType;
@IsNotEmpty()
createdAt!: string;
fileCreatedAt!: string;
@IsNotEmpty()
modifiedAt!: string;
fileModifiedAt!: string;
@IsNotEmpty()
isFavorite!: boolean;

View File

@ -159,8 +159,8 @@ export class MetadataExtractionProcessor {
return exifDate.toDate();
};
const createdAt = exifToDate(exifData?.DateTimeOriginal ?? exifData?.CreateDate ?? asset.createdAt);
const modifyDate = exifToDate(exifData?.ModifyDate ?? asset.modifiedAt);
const fileCreatedAt = exifToDate(exifData?.DateTimeOriginal ?? exifData?.CreateDate ?? asset.fileCreatedAt);
const fileModifiedAt = exifToDate(exifData?.ModifyDate ?? asset.fileModifiedAt);
const fileStats = fs.statSync(asset.originalPath);
const fileSizeInBytes = fileStats.size;
@ -174,8 +174,8 @@ export class MetadataExtractionProcessor {
newExif.exifImageWidth = exifData?.ExifImageWidth || exifData?.ImageWidth || null;
newExif.exposureTime = exifData?.ExposureTime || null;
newExif.orientation = exifData?.Orientation?.toString() || null;
newExif.dateTimeOriginal = createdAt;
newExif.modifyDate = modifyDate;
newExif.dateTimeOriginal = fileCreatedAt;
newExif.modifyDate = fileModifiedAt;
newExif.lensModel = exifData?.LensModel || null;
newExif.fNumber = exifData?.FNumber || null;
newExif.focalLength = exifData?.FocalLength ? parseFloat(exifData.FocalLength) : null;
@ -186,7 +186,7 @@ export class MetadataExtractionProcessor {
await this.assetRepository.save({
id: asset.id,
createdAt: createdAt?.toISOString(),
fileCreatedAt: fileCreatedAt?.toISOString(),
});
if (newExif.livePhotoCID && !asset.livePhotoVideoId) {
@ -273,7 +273,7 @@ export class MetadataExtractionProcessor {
}),
);
let durationString = asset.duration;
let createdAt = asset.createdAt;
let fileCreatedAt = asset.fileCreatedAt;
if (data.format.duration) {
durationString = this.extractDuration(data.format.duration);
@ -282,14 +282,10 @@ export class MetadataExtractionProcessor {
const videoTags = data.format.tags;
if (videoTags) {
if (videoTags['com.apple.quicktime.creationdate']) {
createdAt = String(videoTags['com.apple.quicktime.creationdate']);
fileCreatedAt = String(videoTags['com.apple.quicktime.creationdate']);
} else if (videoTags['creation_time']) {
createdAt = String(videoTags['creation_time']);
} else {
createdAt = asset.createdAt;
fileCreatedAt = String(videoTags['creation_time']);
}
} else {
createdAt = asset.createdAt;
}
const exifData = await exiftool.read<ImmichTags>(asset.originalPath).catch((e) => {
@ -302,7 +298,7 @@ export class MetadataExtractionProcessor {
newExif.description = '';
newExif.imageName = path.parse(fileName).name || null;
newExif.fileSizeInByte = data.format.size || null;
newExif.dateTimeOriginal = createdAt ? new Date(createdAt) : null;
newExif.dateTimeOriginal = fileCreatedAt ? new Date(fileCreatedAt) : null;
newExif.modifyDate = null;
newExif.latitude = null;
newExif.longitude = null;
@ -382,8 +378,9 @@ export class MetadataExtractionProcessor {
}
await this.exifRepository.upsert(newExif, { conflictPaths: ['assetId'] });
await this.assetRepository.update({ id: asset.id }, { duration: durationString, createdAt: createdAt });
await this.assetRepository.update({ id: asset.id }, { duration: durationString, fileCreatedAt });
} catch (err) {
``;
// do nothing
console.log('Error in video metadata extraction', err);
}

View File

@ -40,7 +40,7 @@ export class ThumbnailGeneratorProcessor {
const { asset } = job.data;
const sanitizedDeviceId = sanitize(String(asset.deviceId));
const resizePath = join(basePath, asset.userId, 'thumb', sanitizedDeviceId);
const resizePath = join(basePath, asset.ownerId, 'thumb', sanitizedDeviceId);
if (!existsSync(resizePath)) {
mkdirSync(resizePath, { recursive: true });
@ -75,7 +75,7 @@ export class ThumbnailGeneratorProcessor {
await this.machineLearningQueue.add(JobName.IMAGE_TAGGING, { asset });
await this.machineLearningQueue.add(JobName.OBJECT_DETECTION, { asset });
this.wsCommunicationGateway.server.to(asset.userId).emit('on_upload_success', JSON.stringify(mapAsset(asset)));
this.wsCommunicationGateway.server.to(asset.ownerId).emit('on_upload_success', JSON.stringify(mapAsset(asset)));
}
if (asset.type == AssetType.VIDEO) {
@ -106,7 +106,7 @@ export class ThumbnailGeneratorProcessor {
await this.machineLearningQueue.add(JobName.IMAGE_TAGGING, { asset });
await this.machineLearningQueue.add(JobName.OBJECT_DETECTION, { asset });
this.wsCommunicationGateway.server.to(asset.userId).emit('on_upload_success', JSON.stringify(mapAsset(asset)));
this.wsCommunicationGateway.server.to(asset.ownerId).emit('on_upload_success', JSON.stringify(mapAsset(asset)));
}
}

View File

@ -61,7 +61,7 @@ export class UserDeletionProcessor {
await this.albumRepository.remove(albums);
await this.apiKeyRepository.delete({ userId: user.id });
await this.assetRepository.delete({ userId: user.id });
await this.assetRepository.delete({ ownerId: user.id });
await this.userRepository.remove(user);
} catch (error: any) {
this.logger.error(`Failed to remove user`);

View File

@ -22,7 +22,7 @@ export class VideoTranscodeProcessor {
async videoConversion(job: Job<IVideoConversionProcessor>) {
const { asset } = job.data;
const basePath = APP_UPLOAD_LOCATION;
const encodedVideoPath = `${basePath}/${asset.userId}/encoded-video`;
const encodedVideoPath = `${basePath}/${asset.ownerId}/encoded-video`;
if (!existsSync(encodedVideoPath)) {
mkdirSync(encodedVideoPath, { recursive: true });

View File

@ -3324,10 +3324,10 @@
"type": "string",
"nullable": true
},
"createdAt": {
"fileCreatedAt": {
"type": "string"
},
"modifiedAt": {
"fileModifiedAt": {
"type": "string"
},
"updatedAt": {
@ -3376,8 +3376,8 @@
"deviceId",
"originalPath",
"resizePath",
"createdAt",
"modifiedAt",
"fileCreatedAt",
"fileModifiedAt",
"updatedAt",
"isFavorite",
"mimeType",
@ -3817,10 +3817,10 @@
"deviceId": {
"type": "string"
},
"createdAt": {
"fileCreatedAt": {
"type": "string"
},
"modifiedAt": {
"fileModifiedAt": {
"type": "string"
},
"isFavorite": {
@ -3841,8 +3841,8 @@
"assetData",
"deviceAssetId",
"deviceId",
"createdAt",
"modifiedAt",
"fileCreatedAt",
"fileModifiedAt",
"isFavorite",
"fileExtension"
]

View File

@ -14,8 +14,8 @@ export class AssetResponseDto {
type!: AssetType;
originalPath!: string;
resizePath!: string | null;
createdAt!: string;
modifiedAt!: string;
fileCreatedAt!: string;
fileModifiedAt!: string;
updatedAt!: string;
isFavorite!: boolean;
mimeType!: string | null;
@ -32,13 +32,13 @@ export function mapAsset(entity: AssetEntity): AssetResponseDto {
return {
id: entity.id,
deviceAssetId: entity.deviceAssetId,
ownerId: entity.userId,
ownerId: entity.ownerId,
deviceId: entity.deviceId,
type: entity.type,
originalPath: entity.originalPath,
resizePath: entity.resizePath,
createdAt: entity.createdAt,
modifiedAt: entity.modifiedAt,
fileCreatedAt: entity.fileCreatedAt,
fileModifiedAt: entity.fileModifiedAt,
updatedAt: entity.updatedAt,
isFavorite: entity.isFavorite,
mimeType: entity.mimeType,
@ -56,13 +56,13 @@ export function mapAssetWithoutExif(entity: AssetEntity): AssetResponseDto {
return {
id: entity.id,
deviceAssetId: entity.deviceAssetId,
ownerId: entity.userId,
ownerId: entity.ownerId,
deviceId: entity.deviceId,
type: entity.type,
originalPath: entity.originalPath,
resizePath: entity.resizePath,
createdAt: entity.createdAt,
modifiedAt: entity.modifiedAt,
fileCreatedAt: entity.fileCreatedAt,
fileModifiedAt: entity.fileModifiedAt,
updatedAt: entity.updatedAt,
isFavorite: entity.isFavorite,
mimeType: entity.mimeType,

View File

@ -95,20 +95,23 @@ export const assetEntityStub = {
image: Object.freeze<AssetEntity>({
id: 'asset-id',
deviceAssetId: 'device-asset-id',
modifiedAt: today.toISOString(),
createdAt: today.toISOString(),
userId: 'user-id',
fileModifiedAt: today.toISOString(),
fileCreatedAt: today.toISOString(),
owner: userEntityStub.user1,
ownerId: 'user-id',
deviceId: 'device-id',
originalPath: '/original/path',
resizePath: null,
type: AssetType.IMAGE,
webpPath: null,
encodedVideoPath: null,
createdAt: today.toISOString(),
updatedAt: today.toISOString(),
mimeType: null,
isFavorite: true,
duration: null,
isVisible: true,
livePhotoVideo: null,
livePhotoVideoId: null,
tags: [],
sharedLinks: [],
@ -146,8 +149,8 @@ const assetResponse: AssetResponseDto = {
type: AssetType.VIDEO,
originalPath: 'fake_path/jpeg',
resizePath: '',
createdAt: today.toISOString(),
modifiedAt: today.toISOString(),
fileModifiedAt: today.toISOString(),
fileCreatedAt: today.toISOString(),
updatedAt: today.toISOString(),
isFavorite: false,
mimeType: 'image/jpeg',
@ -374,14 +377,16 @@ export const sharedLinkStub = {
assets: [
{
id: 'id_1',
userId: 'user_id_1',
owner: userEntityStub.user1,
ownerId: 'user_id_1',
deviceAssetId: 'device_asset_id_1',
deviceId: 'device_id_1',
type: AssetType.VIDEO,
originalPath: 'fake_path/jpeg',
resizePath: '',
fileModifiedAt: today.toISOString(),
fileCreatedAt: today.toISOString(),
createdAt: today.toISOString(),
modifiedAt: today.toISOString(),
updatedAt: today.toISOString(),
isFavorite: false,
mimeType: 'image/jpeg',
@ -396,6 +401,7 @@ export const sharedLinkStub = {
encodedVideoPath: '',
duration: null,
isVisible: true,
livePhotoVideo: null,
livePhotoVideoId: null,
exifInfo: {
livePhotoCID: null,

View File

@ -1,9 +1,12 @@
import {
Column,
CreateDateColumn,
Entity,
Index,
JoinColumn,
JoinTable,
ManyToMany,
ManyToOne,
OneToOne,
PrimaryGeneratedColumn,
Unique,
@ -13,9 +16,10 @@ import { ExifEntity } from './exif.entity';
import { SharedLinkEntity } from './shared-link.entity';
import { SmartInfoEntity } from './smart-info.entity';
import { TagEntity } from './tag.entity';
import { UserEntity } from './user.entity';
@Entity('assets')
@Unique('UQ_userid_checksum', ['userId', 'checksum'])
@Unique('UQ_userid_checksum', ['owner', 'checksum'])
export class AssetEntity {
@PrimaryGeneratedColumn('uuid')
id!: string;
@ -23,8 +27,11 @@ export class AssetEntity {
@Column()
deviceAssetId!: string;
@ManyToOne(() => UserEntity, { eager: true, onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false })
owner!: UserEntity;
@Column()
userId!: string;
ownerId!: string;
@Column()
deviceId!: string;
@ -44,15 +51,18 @@ export class AssetEntity {
@Column({ type: 'varchar', nullable: true, default: '' })
encodedVideoPath!: string | null;
@Column({ type: 'timestamptz' })
@CreateDateColumn({ type: 'timestamptz' })
createdAt!: string;
@Column({ type: 'timestamptz' })
modifiedAt!: string;
@UpdateDateColumn({ type: 'timestamptz' })
updatedAt!: string;
@Column({ type: 'timestamptz' })
fileCreatedAt!: string;
@Column({ type: 'timestamptz' })
fileModifiedAt!: string;
@Column({ type: 'boolean', default: false })
isFavorite!: boolean;
@ -69,7 +79,11 @@ export class AssetEntity {
@Column({ type: 'boolean', default: true })
isVisible!: boolean;
@Column({ type: 'uuid', nullable: true })
@OneToOne(() => AssetEntity, { nullable: true, onUpdate: 'CASCADE', onDelete: 'SET NULL' })
@JoinColumn()
livePhotoVideo!: AssetEntity | null;
@Column({ nullable: true })
livePhotoVideoId!: string | null;
@OneToOne(() => ExifEntity, (exifEntity) => exifEntity.asset)
@ -78,12 +92,11 @@ export class AssetEntity {
@OneToOne(() => SmartInfoEntity, (smartInfoEntity) => smartInfoEntity.asset)
smartInfo?: SmartInfoEntity;
// https://github.com/typeorm/typeorm/blob/master/docs/many-to-many-relations.md
@ManyToMany(() => TagEntity, (tag) => tag.assets, { cascade: true })
@ManyToMany(() => TagEntity, (tag) => tag.assets, { cascade: true, eager: true })
@JoinTable({ name: 'tag_asset' })
tags!: TagEntity[];
@ManyToMany(() => SharedLinkEntity, (link) => link.assets, { cascade: true })
@ManyToMany(() => SharedLinkEntity, (link) => link.assets, { cascade: true, eager: true })
@JoinTable({ name: 'shared_link__asset' })
sharedLinks!: SharedLinkEntity[];
}

View File

@ -0,0 +1,30 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class FixAssetRelations1676680127415 implements MigrationInterface {
name = 'FixAssetRelations1676680127415'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "assets" RENAME COLUMN "modifiedAt" TO "fileModifiedAt"`);
await queryRunner.query(`ALTER TABLE "assets" RENAME COLUMN "createdAt" TO "fileCreatedAt"`);
await queryRunner.query(`ALTER TABLE "assets" RENAME COLUMN "userId" TO "ownerId"`);
await queryRunner.query(`ALTER TABLE assets ALTER COLUMN "ownerId" TYPE uuid USING "ownerId"::uuid;`);
await queryRunner.query(`ALTER TABLE "assets" ADD CONSTRAINT "UQ_16294b83fa8c0149719a1f631ef" UNIQUE ("livePhotoVideoId")`);
await queryRunner.query(`ALTER TABLE "assets" ADD CONSTRAINT "FK_2c5ac0d6fb58b238fd2068de67d" FOREIGN KEY ("ownerId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE`);
await queryRunner.query(`ALTER TABLE "assets" ADD CONSTRAINT "FK_16294b83fa8c0149719a1f631ef" FOREIGN KEY ("livePhotoVideoId") REFERENCES "assets"("id") ON DELETE SET NULL ON UPDATE CASCADE`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "assets" DROP CONSTRAINT "FK_16294b83fa8c0149719a1f631ef"`);
await queryRunner.query(`ALTER TABLE "assets" DROP CONSTRAINT "FK_2c5ac0d6fb58b238fd2068de67d"`);
await queryRunner.query(`ALTER TABLE "assets" DROP CONSTRAINT "UQ_16294b83fa8c0149719a1f631ef"`);
await queryRunner.query(`ALTER TABLE "assets" RENAME COLUMN "fileCreatedAt" TO "createdAt"`);
await queryRunner.query(`ALTER TABLE "assets" RENAME COLUMN "fileModifiedAt" TO "modifiedAt"`);
await queryRunner.query(`ALTER TABLE "assets" RENAME COLUMN "ownerId" TO "userId"`);
await queryRunner.query(`ALTER TABLE assets ALTER COLUMN "userId" TYPE varchar`);
}
}

View File

@ -0,0 +1,14 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class AssetCreatedAtField1676721296440 implements MigrationInterface {
name = 'AssetCreatedAtField1676721296440'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "assets" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "assets" DROP COLUMN "createdAt"`);
}
}

View File

@ -31,11 +31,11 @@ export class SharedLinkRepository implements ISharedLinkRepository {
order: {
createdAt: 'DESC',
assets: {
createdAt: 'ASC',
fileCreatedAt: 'ASC',
},
album: {
assets: {
createdAt: 'ASC',
fileCreatedAt: 'ASC',
},
},
},

View File

@ -50,7 +50,7 @@ export class StorageService {
const source = asset.originalPath;
const ext = path.extname(source).split('.').pop() as string;
const sanitized = sanitize(path.basename(filename, `.${ext}`));
const rootPath = path.join(APP_UPLOAD_LOCATION, asset.userId);
const rootPath = path.join(APP_UPLOAD_LOCATION, asset.ownerId);
const storagePath = this.render(this.storageTemplate, asset, sanitized, ext);
const fullPath = path.normalize(path.join(rootPath, storagePath));
let destination = `${fullPath}.${ext}`;
@ -132,7 +132,7 @@ export class StorageService {
this.render(
template,
{
createdAt: new Date().toISOString(),
fileCreatedAt: new Date().toISOString(),
originalPath: '/upload/test/IMG_123.jpg',
type: AssetType.IMAGE,
} as AssetEntity,
@ -161,7 +161,7 @@ export class StorageService {
const fileType = asset.type == AssetType.IMAGE ? 'IMG' : 'VID';
const fileTypeFull = asset.type == AssetType.IMAGE ? 'IMAGE' : 'VIDEO';
const dt = luxon.DateTime.fromISO(new Date(asset.createdAt).toISOString());
const dt = luxon.DateTime.fromISO(new Date(asset.fileCreatedAt).toISOString());
const dateTokens = [
...supportedYearTokens,

View File

@ -6,7 +6,7 @@
"packages": {
"": {
"name": "immich",
"version": "1.46.1",
"version": "1.47.3",
"license": "UNLICENSED",
"dependencies": {
"@nestjs/bull": "^0.6.2",

View File

@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.47.2
* The version of the OpenAPI document: 1.47.3
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
@ -61,10 +61,10 @@ export interface APIKeyCreateResponseDto {
export interface APIKeyResponseDto {
/**
*
* @type {number}
* @type {string}
* @memberof APIKeyResponseDto
*/
'id': number;
'id': string;
/**
*
* @type {string}
@ -467,13 +467,13 @@ export interface AssetResponseDto {
* @type {string}
* @memberof AssetResponseDto
*/
'createdAt': string;
'fileCreatedAt': string;
/**
*
* @type {string}
* @memberof AssetResponseDto
*/
'modifiedAt': string;
'fileModifiedAt': string;
/**
*
* @type {string}
@ -2356,11 +2356,11 @@ export const APIKeyApiAxiosParamCreator = function (configuration?: Configuratio
},
/**
*
* @param {number} id
* @param {string} id
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
deleteKey: async (id: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
deleteKey: async (id: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'id' is not null or undefined
assertParamExists('deleteKey', 'id', id)
const localVarPath = `/api-key/{id}`
@ -2389,11 +2389,11 @@ export const APIKeyApiAxiosParamCreator = function (configuration?: Configuratio
},
/**
*
* @param {number} id
* @param {string} id
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getKey: async (id: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
getKey: async (id: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'id' is not null or undefined
assertParamExists('getKey', 'id', id)
const localVarPath = `/api-key/{id}`
@ -2451,12 +2451,12 @@ export const APIKeyApiAxiosParamCreator = function (configuration?: Configuratio
},
/**
*
* @param {number} id
* @param {string} id
* @param {APIKeyUpdateDto} aPIKeyUpdateDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
updateKey: async (id: number, aPIKeyUpdateDto: APIKeyUpdateDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
updateKey: async (id: string, aPIKeyUpdateDto: APIKeyUpdateDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'id' is not null or undefined
assertParamExists('updateKey', 'id', id)
// verify required parameter 'aPIKeyUpdateDto' is not null or undefined
@ -2510,21 +2510,21 @@ export const APIKeyApiFp = function(configuration?: Configuration) {
},
/**
*
* @param {number} id
* @param {string} id
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async deleteKey(id: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
async deleteKey(id: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.deleteKey(id, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {number} id
* @param {string} id
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getKey(id: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<APIKeyResponseDto>> {
async getKey(id: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<APIKeyResponseDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getKey(id, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
@ -2539,12 +2539,12 @@ export const APIKeyApiFp = function(configuration?: Configuration) {
},
/**
*
* @param {number} id
* @param {string} id
* @param {APIKeyUpdateDto} aPIKeyUpdateDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async updateKey(id: number, aPIKeyUpdateDto: APIKeyUpdateDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<APIKeyResponseDto>> {
async updateKey(id: string, aPIKeyUpdateDto: APIKeyUpdateDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<APIKeyResponseDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.updateKey(id, aPIKeyUpdateDto, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
@ -2569,20 +2569,20 @@ export const APIKeyApiFactory = function (configuration?: Configuration, basePat
},
/**
*
* @param {number} id
* @param {string} id
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
deleteKey(id: number, options?: any): AxiosPromise<void> {
deleteKey(id: string, options?: any): AxiosPromise<void> {
return localVarFp.deleteKey(id, options).then((request) => request(axios, basePath));
},
/**
*
* @param {number} id
* @param {string} id
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getKey(id: number, options?: any): AxiosPromise<APIKeyResponseDto> {
getKey(id: string, options?: any): AxiosPromise<APIKeyResponseDto> {
return localVarFp.getKey(id, options).then((request) => request(axios, basePath));
},
/**
@ -2595,12 +2595,12 @@ export const APIKeyApiFactory = function (configuration?: Configuration, basePat
},
/**
*
* @param {number} id
* @param {string} id
* @param {APIKeyUpdateDto} aPIKeyUpdateDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
updateKey(id: number, aPIKeyUpdateDto: APIKeyUpdateDto, options?: any): AxiosPromise<APIKeyResponseDto> {
updateKey(id: string, aPIKeyUpdateDto: APIKeyUpdateDto, options?: any): AxiosPromise<APIKeyResponseDto> {
return localVarFp.updateKey(id, aPIKeyUpdateDto, options).then((request) => request(axios, basePath));
},
};
@ -2626,23 +2626,23 @@ export class APIKeyApi extends BaseAPI {
/**
*
* @param {number} id
* @param {string} id
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof APIKeyApi
*/
public deleteKey(id: number, options?: AxiosRequestConfig) {
public deleteKey(id: string, options?: AxiosRequestConfig) {
return APIKeyApiFp(this.configuration).deleteKey(id, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {number} id
* @param {string} id
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof APIKeyApi
*/
public getKey(id: number, options?: AxiosRequestConfig) {
public getKey(id: string, options?: AxiosRequestConfig) {
return APIKeyApiFp(this.configuration).getKey(id, options).then((request) => request(this.axios, this.basePath));
}
@ -2658,13 +2658,13 @@ export class APIKeyApi extends BaseAPI {
/**
*
* @param {number} id
* @param {string} id
* @param {APIKeyUpdateDto} aPIKeyUpdateDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof APIKeyApi
*/
public updateKey(id: number, aPIKeyUpdateDto: APIKeyUpdateDto, options?: AxiosRequestConfig) {
public updateKey(id: string, aPIKeyUpdateDto: APIKeyUpdateDto, options?: AxiosRequestConfig) {
return APIKeyApiFp(this.configuration).updateKey(id, aPIKeyUpdateDto, options).then((request) => request(this.axios, this.basePath));
}
}
@ -4432,8 +4432,8 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
* @param {any} assetData
* @param {string} deviceAssetId
* @param {string} deviceId
* @param {string} createdAt
* @param {string} modifiedAt
* @param {string} fileCreatedAt
* @param {string} fileModifiedAt
* @param {boolean} isFavorite
* @param {string} fileExtension
* @param {any} [livePhotoData]
@ -4442,7 +4442,7 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
uploadFile: async (assetType: AssetTypeEnum, assetData: any, deviceAssetId: string, deviceId: string, createdAt: string, modifiedAt: string, isFavorite: boolean, fileExtension: string, livePhotoData?: any, isVisible?: boolean, duration?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
uploadFile: async (assetType: AssetTypeEnum, assetData: any, deviceAssetId: string, deviceId: string, fileCreatedAt: string, fileModifiedAt: string, isFavorite: boolean, fileExtension: string, livePhotoData?: any, isVisible?: boolean, duration?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'assetType' is not null or undefined
assertParamExists('uploadFile', 'assetType', assetType)
// verify required parameter 'assetData' is not null or undefined
@ -4451,10 +4451,10 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
assertParamExists('uploadFile', 'deviceAssetId', deviceAssetId)
// verify required parameter 'deviceId' is not null or undefined
assertParamExists('uploadFile', 'deviceId', deviceId)
// verify required parameter 'createdAt' is not null or undefined
assertParamExists('uploadFile', 'createdAt', createdAt)
// verify required parameter 'modifiedAt' is not null or undefined
assertParamExists('uploadFile', 'modifiedAt', modifiedAt)
// verify required parameter 'fileCreatedAt' is not null or undefined
assertParamExists('uploadFile', 'fileCreatedAt', fileCreatedAt)
// verify required parameter 'fileModifiedAt' is not null or undefined
assertParamExists('uploadFile', 'fileModifiedAt', fileModifiedAt)
// verify required parameter 'isFavorite' is not null or undefined
assertParamExists('uploadFile', 'isFavorite', isFavorite)
// verify required parameter 'fileExtension' is not null or undefined
@ -4497,12 +4497,12 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
localVarFormParams.append('deviceId', deviceId as any);
}
if (createdAt !== undefined) {
localVarFormParams.append('createdAt', createdAt as any);
if (fileCreatedAt !== undefined) {
localVarFormParams.append('fileCreatedAt', fileCreatedAt as any);
}
if (modifiedAt !== undefined) {
localVarFormParams.append('modifiedAt', modifiedAt as any);
if (fileModifiedAt !== undefined) {
localVarFormParams.append('fileModifiedAt', fileModifiedAt as any);
}
if (isFavorite !== undefined) {
@ -4772,8 +4772,8 @@ export const AssetApiFp = function(configuration?: Configuration) {
* @param {any} assetData
* @param {string} deviceAssetId
* @param {string} deviceId
* @param {string} createdAt
* @param {string} modifiedAt
* @param {string} fileCreatedAt
* @param {string} fileModifiedAt
* @param {boolean} isFavorite
* @param {string} fileExtension
* @param {any} [livePhotoData]
@ -4782,8 +4782,8 @@ export const AssetApiFp = function(configuration?: Configuration) {
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async uploadFile(assetType: AssetTypeEnum, assetData: any, deviceAssetId: string, deviceId: string, createdAt: string, modifiedAt: string, isFavorite: boolean, fileExtension: string, livePhotoData?: any, isVisible?: boolean, duration?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AssetFileUploadResponseDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.uploadFile(assetType, assetData, deviceAssetId, deviceId, createdAt, modifiedAt, isFavorite, fileExtension, livePhotoData, isVisible, duration, options);
async uploadFile(assetType: AssetTypeEnum, assetData: any, deviceAssetId: string, deviceId: string, fileCreatedAt: string, fileModifiedAt: string, isFavorite: boolean, fileExtension: string, livePhotoData?: any, isVisible?: boolean, duration?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AssetFileUploadResponseDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.uploadFile(assetType, assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, fileExtension, livePhotoData, isVisible, duration, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
}
@ -5002,8 +5002,8 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
* @param {any} assetData
* @param {string} deviceAssetId
* @param {string} deviceId
* @param {string} createdAt
* @param {string} modifiedAt
* @param {string} fileCreatedAt
* @param {string} fileModifiedAt
* @param {boolean} isFavorite
* @param {string} fileExtension
* @param {any} [livePhotoData]
@ -5012,8 +5012,8 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
uploadFile(assetType: AssetTypeEnum, assetData: any, deviceAssetId: string, deviceId: string, createdAt: string, modifiedAt: string, isFavorite: boolean, fileExtension: string, livePhotoData?: any, isVisible?: boolean, duration?: string, options?: any): AxiosPromise<AssetFileUploadResponseDto> {
return localVarFp.uploadFile(assetType, assetData, deviceAssetId, deviceId, createdAt, modifiedAt, isFavorite, fileExtension, livePhotoData, isVisible, duration, options).then((request) => request(axios, basePath));
uploadFile(assetType: AssetTypeEnum, assetData: any, deviceAssetId: string, deviceId: string, fileCreatedAt: string, fileModifiedAt: string, isFavorite: boolean, fileExtension: string, livePhotoData?: any, isVisible?: boolean, duration?: string, options?: any): AxiosPromise<AssetFileUploadResponseDto> {
return localVarFp.uploadFile(assetType, assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, fileExtension, livePhotoData, isVisible, duration, options).then((request) => request(axios, basePath));
},
};
};
@ -5275,8 +5275,8 @@ export class AssetApi extends BaseAPI {
* @param {any} assetData
* @param {string} deviceAssetId
* @param {string} deviceId
* @param {string} createdAt
* @param {string} modifiedAt
* @param {string} fileCreatedAt
* @param {string} fileModifiedAt
* @param {boolean} isFavorite
* @param {string} fileExtension
* @param {any} [livePhotoData]
@ -5286,8 +5286,8 @@ export class AssetApi extends BaseAPI {
* @throws {RequiredError}
* @memberof AssetApi
*/
public uploadFile(assetType: AssetTypeEnum, assetData: any, deviceAssetId: string, deviceId: string, createdAt: string, modifiedAt: string, isFavorite: boolean, fileExtension: string, livePhotoData?: any, isVisible?: boolean, duration?: string, options?: AxiosRequestConfig) {
return AssetApiFp(this.configuration).uploadFile(assetType, assetData, deviceAssetId, deviceId, createdAt, modifiedAt, isFavorite, fileExtension, livePhotoData, isVisible, duration, options).then((request) => request(this.axios, this.basePath));
public uploadFile(assetType: AssetTypeEnum, assetData: any, deviceAssetId: string, deviceId: string, fileCreatedAt: string, fileModifiedAt: string, isFavorite: boolean, fileExtension: string, livePhotoData?: any, isVisible?: boolean, duration?: string, options?: AxiosRequestConfig) {
return AssetApiFp(this.configuration).uploadFile(assetType, assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, fileExtension, livePhotoData, isVisible, duration, options).then((request) => request(this.axios, this.basePath));
}
}

View File

@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.47.2
* The version of the OpenAPI document: 1.47.3
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.47.2
* The version of the OpenAPI document: 1.47.3
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.47.2
* The version of the OpenAPI document: 1.47.3
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.47.2
* The version of the OpenAPI document: 1.47.3
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@ -96,8 +96,8 @@
};
const getDateRange = () => {
const startDate = new Date(album.assets[0].createdAt);
const endDate = new Date(album.assets[album.assetCount - 1].createdAt);
const startDate = new Date(album.assets[0].fileCreatedAt);
const endDate = new Date(album.assets[album.assetCount - 1].fileCreatedAt);
const startDateString = startDate.toLocaleDateString(locale, albumDateFormat);
const endDateString = endDate.toLocaleDateString(locale, albumDateFormat);

View File

@ -31,7 +31,7 @@
let hoveredDateGroup = '';
$: assetsGroupByDate = lodash
.chain(assets)
.groupBy((a) => new Date(a.createdAt).toLocaleDateString(locale, groupDateFormat))
.groupBy((a) => new Date(a.fileCreatedAt).toLocaleDateString(locale, groupDateFormat))
.sortBy((group) => assets.indexOf(group[0]))
.value();
@ -114,7 +114,7 @@
bind:clientHeight={actualBucketHeight}
>
{#each assetsGroupByDate as assetsInDateGroup, groupIndex (assetsInDateGroup[0].id)}
{@const dateGroupTitle = new Date(assetsInDateGroup[0].createdAt).toLocaleDateString(
{@const dateGroupTitle = new Date(assetsInDateGroup[0].fileCreatedAt).toLocaleDateString(
locale,
groupDateFormat
)}

View File

@ -65,7 +65,7 @@ function createAssetInteractionStore() {
const navigateAsset = async (direction: 'next' | 'previous') => {
// Flatten and sort the asset by date if there are new assets
if (assetSortedByDate.length === 0 || savedAssetLength !== _assetGridState.assets.length) {
assetSortedByDate = sortBy(_assetGridState.assets, (a) => a.createdAt);
assetSortedByDate = sortBy(_assetGridState.assets, (a) => a.fileCreatedAt);
savedAssetLength = _assetGridState.assets.length;
}

View File

@ -69,7 +69,7 @@ async function fileUploader(
const assetType = mimeType.split('/')[0].toUpperCase();
const fileExtension = getFilenameExtension(asset.name);
const formData = new FormData();
const createdAt = new Date(asset.lastModified).toISOString();
const fileCreatedAt = new Date(asset.lastModified).toISOString();
const deviceAssetId = 'web' + '-' + asset.name + '-' + asset.lastModified;
try {
@ -83,10 +83,10 @@ async function fileUploader(
formData.append('assetType', assetType);
// Get Asset Created Date
formData.append('createdAt', createdAt);
formData.append('fileCreatedAt', fileCreatedAt);
// Get Asset Modified At
formData.append('modifiedAt', new Date(asset.lastModified).toISOString());
formData.append('fileModifiedAt', new Date(asset.lastModified).toISOString());
// Set Asset is Favorite to false
formData.append('isFavorite', 'false');