mirror of
https://github.com/immich-app/immich.git
synced 2024-12-29 15:11:58 +00: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:
parent
f88343019d
commit
3da2b05428
9 changed files with 72 additions and 17 deletions
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…
Reference in a new issue