1
0
mirror of https://github.com/immich-app/immich.git synced 2024-12-25 10:43:13 +02:00

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:
Alex 2024-03-06 20:34:55 -06:00 committed by GitHub
parent f88343019d
commit 3da2b05428
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 72 additions and 17 deletions

1
e2e/package-lock.json generated
View File

@ -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",

View File

@ -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: {

View File

@ -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),

View File

@ -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 });

View File

@ -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);

View File

@ -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,

View File

@ -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
}
}

View File

@ -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,

View File

@ -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,
}); });