diff --git a/e2e/package-lock.json b/e2e/package-lock.json index 561fdef537..bfb64e1a98 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -49,7 +49,6 @@ }, "devDependencies": { "@immich/sdk": "file:../open-api/typescript-sdk", - "@testcontainers/postgresql": "^10.7.1", "@types/byte-size": "^8.1.0", "@types/cli-progress": "^3.11.0", "@types/lodash-es": "^4.17.12", diff --git a/e2e/src/api/specs/asset.e2e-spec.ts b/e2e/src/api/specs/asset.e2e-spec.ts index 2bb0e7c4d1..30dfa5d643 100644 --- a/e2e/src/api/specs/asset.e2e-spec.ts +++ b/e2e/src/api/specs/asset.e2e-spec.ts @@ -494,7 +494,7 @@ describe('/asset', () => { input: 'formats/jpg/el_torcal_rocks.jpg', expected: { type: AssetTypeEnum.Image, - originalFileName: 'el_torcal_rocks', + originalFileName: 'el_torcal_rocks.jpg', resized: true, exifInfo: { dateTimeOriginal: '2012-08-05T11:39:59.000Z', @@ -518,7 +518,7 @@ describe('/asset', () => { input: 'formats/heic/IMG_2682.heic', expected: { type: AssetTypeEnum.Image, - originalFileName: 'IMG_2682', + originalFileName: 'IMG_2682.heic', resized: true, fileCreatedAt: '2019-03-21T16:04:22.348Z', exifInfo: { @@ -543,7 +543,7 @@ describe('/asset', () => { input: 'formats/png/density_plot.png', expected: { type: AssetTypeEnum.Image, - originalFileName: 'density_plot', + originalFileName: 'density_plot.png', resized: true, exifInfo: { exifImageWidth: 800, @@ -558,7 +558,7 @@ describe('/asset', () => { input: 'formats/raw/Nikon/D80/glarus.nef', expected: { type: AssetTypeEnum.Image, - originalFileName: 'glarus', + originalFileName: 'glarus.nef', resized: true, fileCreatedAt: '2010-07-20T17:27:12.000Z', exifInfo: { @@ -580,7 +580,7 @@ describe('/asset', () => { input: 'formats/raw/Nikon/D700/philadelphia.nef', expected: { type: AssetTypeEnum.Image, - originalFileName: 'philadelphia', + originalFileName: 'philadelphia.nef', resized: true, fileCreatedAt: '2016-09-22T22:10:29.060Z', exifInfo: { diff --git a/e2e/src/api/specs/shared-link.e2e-spec.ts b/e2e/src/api/specs/shared-link.e2e-spec.ts index f2e5b01867..7ff4bb6bf7 100644 --- a/e2e/src/api/specs/shared-link.e2e-spec.ts +++ b/e2e/src/api/specs/shared-link.e2e-spec.ts @@ -194,7 +194,7 @@ describe('/shared-link', () => { expect(body.assets).toHaveLength(1); expect(body.assets[0]).toEqual( expect.objectContaining({ - originalFileName: 'example', + originalFileName: 'example.png', localDateTime: expect.any(String), fileCreatedAt: expect.any(String), exifInfo: expect.any(Object), diff --git a/server/e2e/api/specs/asset.e2e-spec.ts b/server/e2e/api/specs/asset.e2e-spec.ts index 3f49fb407a..6badd4c674 100644 --- a/server/e2e/api/specs/asset.e2e-spec.ts +++ b/server/e2e/api/specs/asset.e2e-spec.ts @@ -609,6 +609,42 @@ describe(`${AssetController.name} (e2e)`, () => { expect(asset).toMatchObject({ id: body.id, isFavorite: true }); }); + it('should have correct original file name and extension (simple)', async () => { + const { body, status } = await request(server) + .post('/asset/upload') + .set('Authorization', `Bearer ${user1.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', randomBytes(32), 'example.jpg'); + expect(status).toBe(201); + expect(body).toEqual({ id: expect.any(String), duplicate: false }); + + const asset = await api.assetApi.get(server, user1.accessToken, body.id); + expect(asset).toMatchObject({ id: body.id, originalFileName: 'example.jpg' }); + }); + + it('should have correct original file name and extension (complex)', async () => { + const { body, status } = await request(server) + .post('/asset/upload') + .set('Authorization', `Bearer ${user1.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', randomBytes(32), 'example.complex.ext.jpg'); + expect(status).toBe(201); + expect(body).toEqual({ id: expect.any(String), duplicate: false }); + + const asset = await api.assetApi.get(server, user1.accessToken, body.id); + expect(asset).toMatchObject({ id: body.id, originalFileName: 'example.complex.ext.jpg' }); + }); + it('should not upload the same asset twice', async () => { const content = randomBytes(32); await api.assetApi.upload(server, user1.accessToken, 'example-image', { content }); diff --git a/server/src/domain/download/download.service.ts b/server/src/domain/download/download.service.ts index afd57b7d17..fcad2b6e7e 100644 --- a/server/src/domain/download/download.service.ts +++ b/server/src/domain/download/download.service.ts @@ -1,6 +1,6 @@ import { AssetEntity } from '@app/infra/entities'; import { BadRequestException, Inject, Injectable } from '@nestjs/common'; -import { extname } from 'node:path'; +import { parse } from 'node:path'; import { AccessCore, Permission } from '../access'; import { AssetIdsDto } from '../asset'; import { AuthDto } from '../auth'; @@ -91,12 +91,13 @@ export class DownloadService { } const { originalPath, originalFileName } = asset; - const extension = extname(originalPath); - let filename = `${originalFileName}${extension}`; + + let filename = originalFileName; const count = paths[filename] || 0; paths[filename] = count + 1; if (count !== 0) { - filename = `${originalFileName}+${count}${extension}`; + const parsedFilename = parse(originalFileName); + filename = `${parsedFilename.name}+${count}${parsedFilename.ext}`; } zip.addFile(originalPath, filename); diff --git a/server/src/immich/api-v1/asset/asset.service.ts b/server/src/immich/api-v1/asset/asset.service.ts index b8c3222ee1..923cb4ebe8 100644 --- a/server/src/immich/api-v1/asset/asset.service.ts +++ b/server/src/immich/api-v1/asset/asset.service.ts @@ -26,7 +26,6 @@ import { InternalServerErrorException, NotFoundException, } from '@nestjs/common'; -import { parse } from 'node:path'; import { QueryFailedError } from 'typeorm'; import { IAssetRepositoryV1 } from './asset-repository'; import { AssetBulkUploadCheckDto } from './dto/asset-check.dto'; @@ -356,7 +355,7 @@ export class AssetService { duration: dto.duration || null, isVisible: dto.isVisible ?? true, livePhotoVideo: livePhotoAssetId === null ? null : ({ id: livePhotoAssetId } as AssetEntity), - originalFileName: parse(file.originalName).name, + originalFileName: file.originalName, sidecarPath: sidecarPath || null, isReadOnly: dto.isReadOnly ?? false, isOffline: dto.isOffline ?? false, diff --git a/server/src/infra/migrations/1709763765506-AddExtensionToOriginalFileName.ts b/server/src/infra/migrations/1709763765506-AddExtensionToOriginalFileName.ts new file mode 100644 index 0000000000..526d09ccf4 --- /dev/null +++ b/server/src/infra/migrations/1709763765506-AddExtensionToOriginalFileName.ts @@ -0,0 +1,20 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddExtensionToOriginalFileName1709763765506 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + WITH extension AS (WITH cte AS (SELECT a.id, STRING_TO_ARRAY(a."originalPath", '.')::TEXT[] AS arr + FROM assets a) + SELECT cte.id, cte.arr[ARRAY_UPPER(cte.arr, 1)] AS "ext" + FROM cte) + UPDATE assets + SET "originalFileName" = assets."originalFileName" || '.' || extension."ext" + FROM extension + INNER JOIN assets a ON a.id = extension.id; + `); + } + + public async down(): Promise { + // noop + } +} diff --git a/server/test/fixtures/asset.stub.ts b/server/test/fixtures/asset.stub.ts index 3d880143eb..ea1617d6ae 100644 --- a/server/test/fixtures/asset.stub.ts +++ b/server/test/fixtures/asset.stub.ts @@ -16,7 +16,7 @@ export const assetStackStub = (stackId: string, assets: AssetEntity[]): AssetSta export const assetStub = { noResizePath: Object.freeze({ id: 'asset-id', - originalFileName: 'IMG_123', + originalFileName: 'IMG_123.jpg', deviceAssetId: 'device-asset-id', fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), @@ -77,7 +77,7 @@ export const assetStub = { livePhotoVideoId: null, tags: [], sharedLinks: [], - originalFileName: 'IMG_456', + originalFileName: 'IMG_456.jpg', faces: [], sidecarPath: null, isReadOnly: false, diff --git a/web/src/lib/utils/asset-utils.ts b/web/src/lib/utils/asset-utils.ts index 94f451c3c1..f1a3d44be5 100644 --- a/web/src/lib/utils/asset-utils.ts +++ b/web/src/lib/utils/asset-utils.ts @@ -102,14 +102,14 @@ export const downloadFile = async (asset: AssetResponseDto) => { } const assets = [ { - filename: `${asset.originalFileName}.${getFilenameExtension(asset.originalPath)}`, + filename: asset.originalFileName, id: asset.id, size: asset.exifInfo?.fileSizeInByte || 0, }, ]; if (asset.livePhotoVideoId) { assets.push({ - filename: `${asset.originalFileName}.mov`, + filename: asset.originalFileName, id: asset.livePhotoVideoId, size: 0, });