Merge branch 'main' into feat/album-asset-workflow-trigger

This commit is contained in:
Ben Beckford 2026-08-03 08:21:36 -07:00 committed by GitHub
commit 61620e6c27
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
41 changed files with 307 additions and 67 deletions

View file

@ -9,6 +9,7 @@ import semver from 'semver';
import {
EXTENSION_NAMES,
POSTGRES_VERSION_RANGE,
serverVersion,
VECTOR_EXTENSIONS,
VECTOR_INDEX_TABLES,
VECTOR_VERSION_RANGE,
@ -382,6 +383,17 @@ export class DatabaseRepository {
if (error) {
this.logger.error(`Migrations failed: ${error}`);
const missing =
error instanceof Error ? error.message.match(/previously executed migration (.+) is missing/u) : null;
if (missing) {
throw new Error(
`Migration "${missing[1]}" was already applied to this database but is not in this version of Immich (${serverVersion}). ` +
`This usually means the database was migrated by a newer version. Downgrades are not supported.`,
{ cause: error },
);
}
throw error;
}

View file

@ -317,6 +317,12 @@ describe(AssetMediaService.name, () => {
).rejects.toBeInstanceOf(BadRequestException);
expect(mocks.asset.create).not.toHaveBeenCalled();
expect(mocks.asset.remove).not.toHaveBeenCalled();
expect(mocks.job.queue).toHaveBeenCalledWith({
name: JobName.FileDelete,
data: { files: [file.originalPath, undefined] },
});
expect(mocks.event.emit).not.toHaveBeenCalled();
expect(mocks.user.updateUsage).not.toHaveBeenCalledWith(authStub.user1.user.id, file.size);
expect(mocks.storage.utimes).not.toHaveBeenCalledWith(
file.originalPath,

View file

@ -1,7 +1,7 @@
import { BadRequestException, Injectable, InternalServerErrorException, NotFoundException } from '@nestjs/common';
import sanitize from 'sanitize-filename';
import { StorageCore } from 'src/cores/storage.core';
import { AuthSharedLink } from 'src/database';
import { Asset, AuthSharedLink } from 'src/database';
import {
AssetBulkUploadCheckResponseDto,
AssetMediaResponseDto,
@ -128,6 +128,7 @@ export class AssetMediaService extends BaseService {
file: UploadFile,
sidecarFile?: UploadFile,
): Promise<AssetMediaResponseDto> {
let asset: Asset | undefined;
try {
await this.requireAccess({
auth,
@ -145,7 +146,7 @@ export class AssetMediaService extends BaseService {
);
}
const asset = await this.assetRepository.create({
asset = await this.assetRepository.create({
ownerId: auth.user.id,
libraryId: null,
@ -215,6 +216,11 @@ export class AssetMediaService extends BaseService {
return { status: AssetMediaStatus.DUPLICATE, id: duplicateId };
}
// clean up the asset row if one was created
if (asset) {
await this.assetRepository.remove({ id: asset.id });
}
this.logger.error(`Error uploading file ${error}`, error?.stack);
throw error;
}

View file

@ -54,6 +54,7 @@ describe(PartnerService.name, () => {
const auth = AuthFactory.create({ id: user1.id });
mocks.partner.get.mockResolvedValue(void 0);
mocks.user.get.mockResolvedValue(user2);
mocks.partner.create.mockResolvedValue(getForPartner(partner));
await expect(sut.create(auth, { sharedWithId: user2.id })).resolves.toBeDefined();
@ -76,6 +77,19 @@ describe(PartnerService.name, () => {
expect(mocks.partner.create).not.toHaveBeenCalled();
});
it('should throw an error when sharedWithId does not resolve to an existing (non-deleted) user', async () => {
const user1 = UserFactory.create();
const user2 = UserFactory.create();
const auth = AuthFactory.create({ id: user1.id });
mocks.partner.get.mockResolvedValue(void 0);
mocks.user.get.mockResolvedValue(void 0);
await expect(sut.create(auth, { sharedWithId: user2.id })).rejects.toBeInstanceOf(BadRequestException);
expect(mocks.partner.create).not.toHaveBeenCalled();
});
});
describe('remove', () => {

View file

@ -16,6 +16,12 @@ export class PartnerService extends BaseService {
throw new BadRequestException(`Partner already exists`);
}
const user = await this.userRepository.get(sharedWithId, {});
if (!user) {
this.logger.debug('Partner creation failed: user not found');
throw new BadRequestException('Invalid user');
}
const partner = await this.partnerRepository.create(partnerId);
return this.mapPartner(partner, PartnerDirection.SharedBy);
}