fix(server): file uploads for files with extension only filenames (#30024)

Co-authored-by: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com>
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
This commit is contained in:
Rust
2026-07-21 11:57:28 +00:00
committed by GitHub
co-authored by Daniel Dietzler Daniel Dietzler
parent 8365cdd59e
commit 1b4d41324e
5 changed files with 24 additions and 15 deletions
@@ -269,6 +269,10 @@ describe(AssetMediaService.name, () => {
'random-uuid.jpg',
);
});
it('should accept filenames with just an extension', () => {
expect(sut.getUploadFilename(uploadFile.filename(UploadFieldName.ASSET_DATA, '.jpg'))).toEqual('random-uuid.jpg');
});
});
describe('getUploadFolder', () => {
+1 -3
View File
@@ -1,5 +1,4 @@
import { BadRequestException, Injectable, InternalServerErrorException, NotFoundException } from '@nestjs/common';
import { extname } from 'node:path';
import sanitize from 'sanitize-filename';
import { StorageCore } from 'src/cores/storage.core';
import { AuthSharedLink } from 'src/database';
@@ -92,8 +91,7 @@ export class AssetMediaService extends BaseService {
getUploadFilename({ auth, fieldName, file, body }: UploadRequest): string {
requireUploadAccess(auth);
const extension = extname(body.filename || file.originalName);
const extension = getFilenameExtension(body.filename || file.originalName);
const lookup = {
[UploadFieldName.ASSET_DATA]: extension,
[UploadFieldName.SIDECAR_DATA]: '.xmp',
@@ -20,7 +20,7 @@ import { ArgOf } from 'src/repositories/event.repository';
import { BaseService } from 'src/services/base.service';
import { JobOf, StorageAsset } from 'src/types';
import { getAssetFile } from 'src/utils/asset.util';
import { getLivePhotoMotionFilename } from 'src/utils/file';
import { getFilenameExtension, getLivePhotoMotionFilename } from 'src/utils/file';
const storageTokens = {
secondOptions: ['s', 'ss', 'SSS'],
@@ -267,10 +267,10 @@ export class StorageTemplateService extends BaseService {
const { storageLabel, filename } = metadata;
try {
const filenameWithoutExtension = path.basename(filename, path.extname(filename));
const filenameWithoutExtension = path.basename(filename, getFilenameExtension(filename));
const source = asset.originalPath;
let extension = path.extname(source).split('.').pop() as string;
let extension = getFilenameExtension(source).split('.').pop() as string;
const sanitized = sanitize(path.basename(filenameWithoutExtension, `.${extension}`));
extension = extension?.toLowerCase();
const rootPath = StorageCore.getLibraryFolder({ id: asset.ownerId, storageLabel });
+8 -4
View File
@@ -9,15 +9,19 @@ import { ImmichReadStream } from 'src/repositories/storage.repository';
import { isConnectionAborted } from 'src/utils/misc';
export function getFileNameWithoutExtension(path: string): string {
return basename(path, extname(path));
return basename(path, getFilenameExtension(path));
}
export function getFilenameExtension(path: string): string {
return extname(path);
export function getFilenameExtension(path: string) {
const extension = extname(path);
if (!extension && path.startsWith('.') && !path.includes('.', 1)) {
return path;
}
return extension;
}
export function getLivePhotoMotionFilename(stillName: string, motionName: string) {
return getFileNameWithoutExtension(stillName) + extname(motionName);
return getFileNameWithoutExtension(stillName) + getFilenameExtension(motionName);
}
export class ImmichFileResponse {
+8 -5
View File
@@ -1,5 +1,5 @@
import { extname } from 'node:path';
import { AssetType } from 'src/enum';
import { getFilenameExtension } from 'src/utils/file';
const raw = {
'.3fr': ['image/3fr', 'image/x-hasselblad-3fr'],
@@ -132,10 +132,12 @@ const sidecar: Record<string, string[]> = {
const types = { ...image, ...video, ...sidecar };
const isType = (filename: string, record: Record<string, string[]>) =>
Object.hasOwn(record, extname(filename).toLowerCase());
const isType = (filename: string, r: Record<string, string[]>) =>
Object.hasOwn(r, getFilenameExtension(filename).toLowerCase());
const lookup = (filename: string) =>
types[getFilenameExtension(filename).toLowerCase()]?.[0] ?? 'application/octet-stream';
const lookup = (filename: string) => types[extname(filename).toLowerCase()]?.[0] ?? 'application/octet-stream';
const toExtension = (mimeType: string) => {
return (
extensionOverrides[mimeType] || Object.entries(types).find(([, mimeTypes]) => mimeTypes.includes(mimeType))?.[0]
@@ -158,7 +160,8 @@ export const mimeTypes = {
isProfile: (filename: string) => isType(filename, profile),
isSidecar: (filename: string) => isType(filename, sidecar),
isVideo: (filename: string) => isType(filename, video),
canBeTransparent: (filename: string) => transparentCapableExtensions.has(extname(filename).toLowerCase()),
canBeTransparent: (filename: string) =>
transparentCapableExtensions.has(getFilenameExtension(filename).toLowerCase()),
isRaw: (filename: string) => isType(filename, raw),
lookup,
/** return an extension (including a leading `.`) for a mime-type */