forked from Cutlery/immich
		
	chore(server): save original file name with extension (#7679)
* chore(server): save original file name with extension * extract extension * update e2e test * update e2e test * download archive * fix download archive appending name * pr feedback * remove unused code * test * unit test * remove unused code * migration * noops * pr feedback * Update server/src/domain/download/download.service.ts Co-authored-by: Mert <101130780+mertalev@users.noreply.github.com> --------- Co-authored-by: Mert <101130780+mertalev@users.noreply.github.com>
This commit is contained in:
		
							parent
							
								
									f88343019d
								
							
						
					
					
						commit
						3da2b05428
					
				
							
								
								
									
										1
									
								
								e2e/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								e2e/package-lock.json
									
									
									
										generated
									
									
									
								
							@ -49,7 +49,6 @@
 | 
				
			|||||||
      },
 | 
					      },
 | 
				
			||||||
      "devDependencies": {
 | 
					      "devDependencies": {
 | 
				
			||||||
        "@immich/sdk": "file:../open-api/typescript-sdk",
 | 
					        "@immich/sdk": "file:../open-api/typescript-sdk",
 | 
				
			||||||
        "@testcontainers/postgresql": "^10.7.1",
 | 
					 | 
				
			||||||
        "@types/byte-size": "^8.1.0",
 | 
					        "@types/byte-size": "^8.1.0",
 | 
				
			||||||
        "@types/cli-progress": "^3.11.0",
 | 
					        "@types/cli-progress": "^3.11.0",
 | 
				
			||||||
        "@types/lodash-es": "^4.17.12",
 | 
					        "@types/lodash-es": "^4.17.12",
 | 
				
			||||||
 | 
				
			|||||||
@ -494,7 +494,7 @@ describe('/asset', () => {
 | 
				
			|||||||
        input: 'formats/jpg/el_torcal_rocks.jpg',
 | 
					        input: 'formats/jpg/el_torcal_rocks.jpg',
 | 
				
			||||||
        expected: {
 | 
					        expected: {
 | 
				
			||||||
          type: AssetTypeEnum.Image,
 | 
					          type: AssetTypeEnum.Image,
 | 
				
			||||||
          originalFileName: 'el_torcal_rocks',
 | 
					          originalFileName: 'el_torcal_rocks.jpg',
 | 
				
			||||||
          resized: true,
 | 
					          resized: true,
 | 
				
			||||||
          exifInfo: {
 | 
					          exifInfo: {
 | 
				
			||||||
            dateTimeOriginal: '2012-08-05T11:39:59.000Z',
 | 
					            dateTimeOriginal: '2012-08-05T11:39:59.000Z',
 | 
				
			||||||
@ -518,7 +518,7 @@ describe('/asset', () => {
 | 
				
			|||||||
        input: 'formats/heic/IMG_2682.heic',
 | 
					        input: 'formats/heic/IMG_2682.heic',
 | 
				
			||||||
        expected: {
 | 
					        expected: {
 | 
				
			||||||
          type: AssetTypeEnum.Image,
 | 
					          type: AssetTypeEnum.Image,
 | 
				
			||||||
          originalFileName: 'IMG_2682',
 | 
					          originalFileName: 'IMG_2682.heic',
 | 
				
			||||||
          resized: true,
 | 
					          resized: true,
 | 
				
			||||||
          fileCreatedAt: '2019-03-21T16:04:22.348Z',
 | 
					          fileCreatedAt: '2019-03-21T16:04:22.348Z',
 | 
				
			||||||
          exifInfo: {
 | 
					          exifInfo: {
 | 
				
			||||||
@ -543,7 +543,7 @@ describe('/asset', () => {
 | 
				
			|||||||
        input: 'formats/png/density_plot.png',
 | 
					        input: 'formats/png/density_plot.png',
 | 
				
			||||||
        expected: {
 | 
					        expected: {
 | 
				
			||||||
          type: AssetTypeEnum.Image,
 | 
					          type: AssetTypeEnum.Image,
 | 
				
			||||||
          originalFileName: 'density_plot',
 | 
					          originalFileName: 'density_plot.png',
 | 
				
			||||||
          resized: true,
 | 
					          resized: true,
 | 
				
			||||||
          exifInfo: {
 | 
					          exifInfo: {
 | 
				
			||||||
            exifImageWidth: 800,
 | 
					            exifImageWidth: 800,
 | 
				
			||||||
@ -558,7 +558,7 @@ describe('/asset', () => {
 | 
				
			|||||||
        input: 'formats/raw/Nikon/D80/glarus.nef',
 | 
					        input: 'formats/raw/Nikon/D80/glarus.nef',
 | 
				
			||||||
        expected: {
 | 
					        expected: {
 | 
				
			||||||
          type: AssetTypeEnum.Image,
 | 
					          type: AssetTypeEnum.Image,
 | 
				
			||||||
          originalFileName: 'glarus',
 | 
					          originalFileName: 'glarus.nef',
 | 
				
			||||||
          resized: true,
 | 
					          resized: true,
 | 
				
			||||||
          fileCreatedAt: '2010-07-20T17:27:12.000Z',
 | 
					          fileCreatedAt: '2010-07-20T17:27:12.000Z',
 | 
				
			||||||
          exifInfo: {
 | 
					          exifInfo: {
 | 
				
			||||||
@ -580,7 +580,7 @@ describe('/asset', () => {
 | 
				
			|||||||
        input: 'formats/raw/Nikon/D700/philadelphia.nef',
 | 
					        input: 'formats/raw/Nikon/D700/philadelphia.nef',
 | 
				
			||||||
        expected: {
 | 
					        expected: {
 | 
				
			||||||
          type: AssetTypeEnum.Image,
 | 
					          type: AssetTypeEnum.Image,
 | 
				
			||||||
          originalFileName: 'philadelphia',
 | 
					          originalFileName: 'philadelphia.nef',
 | 
				
			||||||
          resized: true,
 | 
					          resized: true,
 | 
				
			||||||
          fileCreatedAt: '2016-09-22T22:10:29.060Z',
 | 
					          fileCreatedAt: '2016-09-22T22:10:29.060Z',
 | 
				
			||||||
          exifInfo: {
 | 
					          exifInfo: {
 | 
				
			||||||
 | 
				
			|||||||
@ -194,7 +194,7 @@ describe('/shared-link', () => {
 | 
				
			|||||||
      expect(body.assets).toHaveLength(1);
 | 
					      expect(body.assets).toHaveLength(1);
 | 
				
			||||||
      expect(body.assets[0]).toEqual(
 | 
					      expect(body.assets[0]).toEqual(
 | 
				
			||||||
        expect.objectContaining({
 | 
					        expect.objectContaining({
 | 
				
			||||||
          originalFileName: 'example',
 | 
					          originalFileName: 'example.png',
 | 
				
			||||||
          localDateTime: expect.any(String),
 | 
					          localDateTime: expect.any(String),
 | 
				
			||||||
          fileCreatedAt: expect.any(String),
 | 
					          fileCreatedAt: expect.any(String),
 | 
				
			||||||
          exifInfo: expect.any(Object),
 | 
					          exifInfo: expect.any(Object),
 | 
				
			||||||
 | 
				
			|||||||
@ -609,6 +609,42 @@ describe(`${AssetController.name} (e2e)`, () => {
 | 
				
			|||||||
      expect(asset).toMatchObject({ id: body.id, isFavorite: true });
 | 
					      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 () => {
 | 
					    it('should not upload the same asset twice', async () => {
 | 
				
			||||||
      const content = randomBytes(32);
 | 
					      const content = randomBytes(32);
 | 
				
			||||||
      await api.assetApi.upload(server, user1.accessToken, 'example-image', { content });
 | 
					      await api.assetApi.upload(server, user1.accessToken, 'example-image', { content });
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,6 @@
 | 
				
			|||||||
import { AssetEntity } from '@app/infra/entities';
 | 
					import { AssetEntity } from '@app/infra/entities';
 | 
				
			||||||
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
 | 
					import { BadRequestException, Inject, Injectable } from '@nestjs/common';
 | 
				
			||||||
import { extname } from 'node:path';
 | 
					import { parse } from 'node:path';
 | 
				
			||||||
import { AccessCore, Permission } from '../access';
 | 
					import { AccessCore, Permission } from '../access';
 | 
				
			||||||
import { AssetIdsDto } from '../asset';
 | 
					import { AssetIdsDto } from '../asset';
 | 
				
			||||||
import { AuthDto } from '../auth';
 | 
					import { AuthDto } from '../auth';
 | 
				
			||||||
@ -91,12 +91,13 @@ export class DownloadService {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const { originalPath, originalFileName } = asset;
 | 
					      const { originalPath, originalFileName } = asset;
 | 
				
			||||||
      const extension = extname(originalPath);
 | 
					
 | 
				
			||||||
      let filename = `${originalFileName}${extension}`;
 | 
					      let filename = originalFileName;
 | 
				
			||||||
      const count = paths[filename] || 0;
 | 
					      const count = paths[filename] || 0;
 | 
				
			||||||
      paths[filename] = count + 1;
 | 
					      paths[filename] = count + 1;
 | 
				
			||||||
      if (count !== 0) {
 | 
					      if (count !== 0) {
 | 
				
			||||||
        filename = `${originalFileName}+${count}${extension}`;
 | 
					        const parsedFilename = parse(originalFileName);
 | 
				
			||||||
 | 
					        filename = `${parsedFilename.name}+${count}${parsedFilename.ext}`;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      zip.addFile(originalPath, filename);
 | 
					      zip.addFile(originalPath, filename);
 | 
				
			||||||
 | 
				
			|||||||
@ -26,7 +26,6 @@ import {
 | 
				
			|||||||
  InternalServerErrorException,
 | 
					  InternalServerErrorException,
 | 
				
			||||||
  NotFoundException,
 | 
					  NotFoundException,
 | 
				
			||||||
} from '@nestjs/common';
 | 
					} from '@nestjs/common';
 | 
				
			||||||
import { parse } from 'node:path';
 | 
					 | 
				
			||||||
import { QueryFailedError } from 'typeorm';
 | 
					import { QueryFailedError } from 'typeorm';
 | 
				
			||||||
import { IAssetRepositoryV1 } from './asset-repository';
 | 
					import { IAssetRepositoryV1 } from './asset-repository';
 | 
				
			||||||
import { AssetBulkUploadCheckDto } from './dto/asset-check.dto';
 | 
					import { AssetBulkUploadCheckDto } from './dto/asset-check.dto';
 | 
				
			||||||
@ -356,7 +355,7 @@ export class AssetService {
 | 
				
			|||||||
      duration: dto.duration || null,
 | 
					      duration: dto.duration || null,
 | 
				
			||||||
      isVisible: dto.isVisible ?? true,
 | 
					      isVisible: dto.isVisible ?? true,
 | 
				
			||||||
      livePhotoVideo: livePhotoAssetId === null ? null : ({ id: livePhotoAssetId } as AssetEntity),
 | 
					      livePhotoVideo: livePhotoAssetId === null ? null : ({ id: livePhotoAssetId } as AssetEntity),
 | 
				
			||||||
      originalFileName: parse(file.originalName).name,
 | 
					      originalFileName: file.originalName,
 | 
				
			||||||
      sidecarPath: sidecarPath || null,
 | 
					      sidecarPath: sidecarPath || null,
 | 
				
			||||||
      isReadOnly: dto.isReadOnly ?? false,
 | 
					      isReadOnly: dto.isReadOnly ?? false,
 | 
				
			||||||
      isOffline: dto.isOffline ?? false,
 | 
					      isOffline: dto.isOffline ?? false,
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,20 @@
 | 
				
			|||||||
 | 
					import { MigrationInterface, QueryRunner } from 'typeorm';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class AddExtensionToOriginalFileName1709763765506 implements MigrationInterface {
 | 
				
			||||||
 | 
					  public async up(queryRunner: QueryRunner): Promise<void> {
 | 
				
			||||||
 | 
					    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<void> {
 | 
				
			||||||
 | 
					    // noop
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										4
									
								
								server/test/fixtures/asset.stub.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								server/test/fixtures/asset.stub.ts
									
									
									
									
										vendored
									
									
								
							@ -16,7 +16,7 @@ export const assetStackStub = (stackId: string, assets: AssetEntity[]): AssetSta
 | 
				
			|||||||
export const assetStub = {
 | 
					export const assetStub = {
 | 
				
			||||||
  noResizePath: Object.freeze<AssetEntity>({
 | 
					  noResizePath: Object.freeze<AssetEntity>({
 | 
				
			||||||
    id: 'asset-id',
 | 
					    id: 'asset-id',
 | 
				
			||||||
    originalFileName: 'IMG_123',
 | 
					    originalFileName: 'IMG_123.jpg',
 | 
				
			||||||
    deviceAssetId: 'device-asset-id',
 | 
					    deviceAssetId: 'device-asset-id',
 | 
				
			||||||
    fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
 | 
					    fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
 | 
				
			||||||
    fileCreatedAt: 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,
 | 
					    livePhotoVideoId: null,
 | 
				
			||||||
    tags: [],
 | 
					    tags: [],
 | 
				
			||||||
    sharedLinks: [],
 | 
					    sharedLinks: [],
 | 
				
			||||||
    originalFileName: 'IMG_456',
 | 
					    originalFileName: 'IMG_456.jpg',
 | 
				
			||||||
    faces: [],
 | 
					    faces: [],
 | 
				
			||||||
    sidecarPath: null,
 | 
					    sidecarPath: null,
 | 
				
			||||||
    isReadOnly: false,
 | 
					    isReadOnly: false,
 | 
				
			||||||
 | 
				
			|||||||
@ -102,14 +102,14 @@ export const downloadFile = async (asset: AssetResponseDto) => {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
  const assets = [
 | 
					  const assets = [
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
      filename: `${asset.originalFileName}.${getFilenameExtension(asset.originalPath)}`,
 | 
					      filename: asset.originalFileName,
 | 
				
			||||||
      id: asset.id,
 | 
					      id: asset.id,
 | 
				
			||||||
      size: asset.exifInfo?.fileSizeInByte || 0,
 | 
					      size: asset.exifInfo?.fileSizeInByte || 0,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  ];
 | 
					  ];
 | 
				
			||||||
  if (asset.livePhotoVideoId) {
 | 
					  if (asset.livePhotoVideoId) {
 | 
				
			||||||
    assets.push({
 | 
					    assets.push({
 | 
				
			||||||
      filename: `${asset.originalFileName}.mov`,
 | 
					      filename: asset.originalFileName,
 | 
				
			||||||
      id: asset.livePhotoVideoId,
 | 
					      id: asset.livePhotoVideoId,
 | 
				
			||||||
      size: 0,
 | 
					      size: 0,
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user