mirror of
https://github.com/immich-app/immich.git
synced 2026-05-27 10:02:31 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 79ca7ac444 |
@@ -9,7 +9,7 @@ import { AuthDto } from 'src/dtos/auth.dto';
|
|||||||
import { ImmichWorker, JobStatus, MetadataKey, QueueName, UserAvatarColor, UserStatus } from 'src/enum';
|
import { ImmichWorker, JobStatus, MetadataKey, QueueName, UserAvatarColor, UserStatus } from 'src/enum';
|
||||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||||
import { JobItem, JobSource } from 'src/types';
|
import { JobItem, JobSource, UploadFile } from 'src/types';
|
||||||
|
|
||||||
type EmitHandlers = Partial<{ [T in EmitEvent]: Array<EventItem<T>> }>;
|
type EmitHandlers = Partial<{ [T in EmitEvent]: Array<EventItem<T>> }>;
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ type EventMap = {
|
|||||||
AlbumInvite: [{ id: string; userId: string; senderName: string }];
|
AlbumInvite: [{ id: string; userId: string; senderName: string }];
|
||||||
|
|
||||||
// asset events
|
// asset events
|
||||||
AssetCreate: [{ asset: Asset }];
|
AssetCreate: [{ asset: Asset; file: UploadFile }];
|
||||||
AssetTag: [{ assetId: string }];
|
AssetTag: [{ assetId: string }];
|
||||||
AssetUntag: [{ assetId: string }];
|
AssetUntag: [{ assetId: string }];
|
||||||
AssetHide: [{ assetId: string; userId: string }];
|
AssetHide: [{ assetId: string; userId: string }];
|
||||||
|
|||||||
@@ -146,17 +146,79 @@ export class AssetMediaService extends BaseService {
|
|||||||
{ userId: auth.user.id, livePhotoVideoId: dto.livePhotoVideoId },
|
{ userId: auth.user.id, livePhotoVideoId: dto.livePhotoVideoId },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const asset = await this.create(auth.user.id, dto, file, sidecarFile);
|
|
||||||
|
const asset = await this.assetRepository.create({
|
||||||
|
ownerId: auth.user.id,
|
||||||
|
libraryId: null,
|
||||||
|
|
||||||
|
checksum: file.checksum,
|
||||||
|
checksumAlgorithm: ChecksumAlgorithm.sha1File,
|
||||||
|
originalPath: file.originalPath,
|
||||||
|
|
||||||
|
fileCreatedAt: dto.fileCreatedAt,
|
||||||
|
fileModifiedAt: dto.fileModifiedAt,
|
||||||
|
localDateTime: dto.fileCreatedAt,
|
||||||
|
|
||||||
|
type: mimeTypes.assetType(file.originalPath),
|
||||||
|
isFavorite: dto.isFavorite,
|
||||||
|
duration: dto.duration || null,
|
||||||
|
visibility: dto.visibility ?? AssetVisibility.Timeline,
|
||||||
|
livePhotoVideoId: dto.livePhotoVideoId,
|
||||||
|
originalFileName: dto.filename || file.originalName,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (dto.metadata?.length) {
|
||||||
|
await this.assetRepository.upsertMetadata(asset.id, dto.metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sidecarFile) {
|
||||||
|
await this.assetRepository.upsertFile({
|
||||||
|
assetId: asset.id,
|
||||||
|
path: sidecarFile.originalPath,
|
||||||
|
type: AssetFileType.Sidecar,
|
||||||
|
});
|
||||||
|
await this.storageRepository.utimes(sidecarFile.originalPath, new Date(), new Date(dto.fileModifiedAt));
|
||||||
|
}
|
||||||
|
await this.storageRepository.utimes(file.originalPath, new Date(), new Date(dto.fileModifiedAt));
|
||||||
|
await this.assetRepository.upsertExif({
|
||||||
|
exif: { assetId: asset.id, fileSizeInByte: file.size },
|
||||||
|
lockedPropertiesBehavior: 'override',
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.jobRepository.queue({ name: JobName.AssetExtractMetadata, data: { id: asset.id, source: 'upload' } });
|
||||||
|
|
||||||
if (auth.sharedLink) {
|
if (auth.sharedLink) {
|
||||||
await this.addToSharedLink(auth.sharedLink, asset.id);
|
await this.addToSharedLink(auth.sharedLink, asset.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.userRepository.updateUsage(auth.user.id, file.size);
|
await this.eventRepository.emit('AssetCreate', { asset, file });
|
||||||
|
|
||||||
return { id: asset.id, status: AssetMediaStatus.CREATED };
|
return { id: asset.id, status: AssetMediaStatus.CREATED };
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
return this.handleUploadError(error, auth, file, sidecarFile);
|
// clean up files
|
||||||
|
await this.jobRepository.queue({
|
||||||
|
name: JobName.FileDelete,
|
||||||
|
data: { files: [file.originalPath, sidecarFile?.originalPath] },
|
||||||
|
});
|
||||||
|
|
||||||
|
// handle duplicates with a success response
|
||||||
|
if (isAssetChecksumConstraint(error)) {
|
||||||
|
const duplicateId = await this.assetRepository.getUploadAssetIdByChecksum(auth.user.id, file.checksum);
|
||||||
|
if (!duplicateId) {
|
||||||
|
this.logger.error(`Error locating duplicate for checksum constraint`);
|
||||||
|
throw new InternalServerErrorException();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auth.sharedLink) {
|
||||||
|
await this.addToSharedLink(auth.sharedLink, duplicateId);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.debug(`Duplicate asset upload rejected: existing asset ${duplicateId}`);
|
||||||
|
return { status: AssetMediaStatus.DUPLICATE, id: duplicateId };
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.error(`Error uploading file ${error}`, error?.stack);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -290,78 +352,7 @@ export class AssetMediaService extends BaseService {
|
|||||||
auth: AuthDto,
|
auth: AuthDto,
|
||||||
file: UploadFile,
|
file: UploadFile,
|
||||||
sidecarFile?: UploadFile,
|
sidecarFile?: UploadFile,
|
||||||
): Promise<AssetMediaResponseDto> {
|
): Promise<AssetMediaResponseDto> {}
|
||||||
// clean up files
|
|
||||||
await this.jobRepository.queue({
|
|
||||||
name: JobName.FileDelete,
|
|
||||||
data: { files: [file.originalPath, sidecarFile?.originalPath] },
|
|
||||||
});
|
|
||||||
|
|
||||||
// handle duplicates with a success response
|
|
||||||
if (isAssetChecksumConstraint(error)) {
|
|
||||||
const duplicateId = await this.assetRepository.getUploadAssetIdByChecksum(auth.user.id, file.checksum);
|
|
||||||
if (!duplicateId) {
|
|
||||||
this.logger.error(`Error locating duplicate for checksum constraint`);
|
|
||||||
throw new InternalServerErrorException();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (auth.sharedLink) {
|
|
||||||
await this.addToSharedLink(auth.sharedLink, duplicateId);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.debug(`Duplicate asset upload rejected: existing asset ${duplicateId}`);
|
|
||||||
return { status: AssetMediaStatus.DUPLICATE, id: duplicateId };
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.error(`Error uploading file ${error}`, error?.stack);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async create(ownerId: string, dto: AssetMediaCreateDto, file: UploadFile, sidecarFile?: UploadFile) {
|
|
||||||
const asset = await this.assetRepository.create({
|
|
||||||
ownerId,
|
|
||||||
libraryId: null,
|
|
||||||
|
|
||||||
checksum: file.checksum,
|
|
||||||
checksumAlgorithm: ChecksumAlgorithm.sha1File,
|
|
||||||
originalPath: file.originalPath,
|
|
||||||
|
|
||||||
fileCreatedAt: dto.fileCreatedAt,
|
|
||||||
fileModifiedAt: dto.fileModifiedAt,
|
|
||||||
localDateTime: dto.fileCreatedAt,
|
|
||||||
|
|
||||||
type: mimeTypes.assetType(file.originalPath),
|
|
||||||
isFavorite: dto.isFavorite,
|
|
||||||
duration: dto.duration || null,
|
|
||||||
visibility: dto.visibility ?? AssetVisibility.Timeline,
|
|
||||||
livePhotoVideoId: dto.livePhotoVideoId,
|
|
||||||
originalFileName: dto.filename || file.originalName,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (dto.metadata?.length) {
|
|
||||||
await this.assetRepository.upsertMetadata(asset.id, dto.metadata);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sidecarFile) {
|
|
||||||
await this.assetRepository.upsertFile({
|
|
||||||
assetId: asset.id,
|
|
||||||
path: sidecarFile.originalPath,
|
|
||||||
type: AssetFileType.Sidecar,
|
|
||||||
});
|
|
||||||
await this.storageRepository.utimes(sidecarFile.originalPath, new Date(), new Date(dto.fileModifiedAt));
|
|
||||||
}
|
|
||||||
await this.storageRepository.utimes(file.originalPath, new Date(), new Date(dto.fileModifiedAt));
|
|
||||||
await this.assetRepository.upsertExif({
|
|
||||||
exif: { assetId: asset.id, fileSizeInByte: file.size },
|
|
||||||
lockedPropertiesBehavior: 'override',
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.eventRepository.emit('AssetCreate', { asset });
|
|
||||||
|
|
||||||
await this.jobRepository.queue({ name: JobName.AssetExtractMetadata, data: { id: asset.id, source: 'upload' } });
|
|
||||||
|
|
||||||
return asset;
|
|
||||||
}
|
|
||||||
|
|
||||||
private requireQuota(auth: AuthDto, size: number) {
|
private requireQuota(auth: AuthDto, size: number) {
|
||||||
if (auth.user.quotaSizeInBytes !== null && auth.user.quotaSizeInBytes < auth.user.quotaUsageInBytes + size) {
|
if (auth.user.quotaSizeInBytes !== null && auth.user.quotaSizeInBytes < auth.user.quotaUsageInBytes + size) {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Updateable } from 'kysely';
|
|||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { SALT_ROUNDS } from 'src/constants';
|
import { SALT_ROUNDS } from 'src/constants';
|
||||||
import { StorageCore } from 'src/cores/storage.core';
|
import { StorageCore } from 'src/cores/storage.core';
|
||||||
import { OnJob } from 'src/decorators';
|
import { OnEvent, OnJob } from 'src/decorators';
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto';
|
import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto';
|
||||||
import { OnboardingDto, OnboardingResponseDto } from 'src/dtos/onboarding.dto';
|
import { OnboardingDto, OnboardingResponseDto } from 'src/dtos/onboarding.dto';
|
||||||
@@ -11,6 +11,7 @@ import { UserPreferencesResponseDto, UserPreferencesUpdateDto, mapPreferences }
|
|||||||
import { CreateProfileImageResponseDto } from 'src/dtos/user-profile.dto';
|
import { CreateProfileImageResponseDto } from 'src/dtos/user-profile.dto';
|
||||||
import { UserAdminResponseDto, UserResponseDto, UserUpdateMeDto, mapUser, mapUserAdmin } from 'src/dtos/user.dto';
|
import { UserAdminResponseDto, UserResponseDto, UserUpdateMeDto, mapUser, mapUserAdmin } from 'src/dtos/user.dto';
|
||||||
import { CacheControl, JobName, JobStatus, QueueName, StorageFolder, UserMetadataKey } from 'src/enum';
|
import { CacheControl, JobName, JobStatus, QueueName, StorageFolder, UserMetadataKey } from 'src/enum';
|
||||||
|
import { ArgOf } from 'src/repositories/event.repository';
|
||||||
import { UserFindOptions } from 'src/repositories/user.repository';
|
import { UserFindOptions } from 'src/repositories/user.repository';
|
||||||
import { UserTable } from 'src/schema/tables/user.table';
|
import { UserTable } from 'src/schema/tables/user.table';
|
||||||
import { BaseService } from 'src/services/base.service';
|
import { BaseService } from 'src/services/base.service';
|
||||||
@@ -230,6 +231,11 @@ export class UserService extends BaseService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OnEvent({ name: 'AssetCreate' })
|
||||||
|
async onAssetCreate({ asset, file }: ArgOf<'AssetCreate'>) {
|
||||||
|
await this.userRepository.updateUsage(asset.ownerId, file.size);
|
||||||
|
}
|
||||||
|
|
||||||
@OnJob({ name: JobName.UserSyncUsage, queue: QueueName.BackgroundTask })
|
@OnJob({ name: JobName.UserSyncUsage, queue: QueueName.BackgroundTask })
|
||||||
async handleUserSyncUsage(): Promise<JobStatus> {
|
async handleUserSyncUsage(): Promise<JobStatus> {
|
||||||
await this.userRepository.syncUsage();
|
await this.userRepository.syncUsage();
|
||||||
|
|||||||
Reference in New Issue
Block a user