mirror of
https://github.com/immich-app/immich.git
synced 2026-07-28 14:47:30 -07:00
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:
co-authored by
Daniel Dietzler
Daniel Dietzler
parent
8365cdd59e
commit
1b4d41324e
@@ -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,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 });
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 */
|
||||
|
||||
Reference in New Issue
Block a user