Skip to content

Commit

Permalink
feat(server): restore modified at timestamp after upload, preserve wh…
Browse files Browse the repository at this point in the history
…en copying
  • Loading branch information
jextrevor committed Feb 9, 2024
1 parent d0b5623 commit a5dca64
Show file tree
Hide file tree
Showing 3 changed files with 32 additions and 2 deletions.
16 changes: 16 additions & 0 deletions server/src/immich/api-v1/asset/asset.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,15 @@ import {
newUserRepositoryMock,
} from '@test';
import { when } from 'jest-when';
import fs from 'node:fs/promises';
import { QueryFailedError } from 'typeorm';
import { IAssetRepositoryV1 } from './asset-repository';
import { AssetService } from './asset.service';
import { CreateAssetDto } from './dto/create-asset.dto';
import { AssetRejectReason, AssetUploadAction } from './response-dto/asset-check-response.dto';

jest.mock('node:fs/promises');

const _getCreateAssetDto = (): CreateAssetDto => {
const createAssetDto = new CreateAssetDto();
createAssetDto.deviceAssetId = 'deviceAssetId';
Expand Down Expand Up @@ -91,6 +94,8 @@ describe('AssetService', () => {
when(assetRepositoryMockV1.get)
.calledWith(assetStub.livePhotoMotionAsset.id)
.mockResolvedValue(assetStub.livePhotoMotionAsset);

when(fs.utimes).mockResolvedValue();
});

describe('uploadFile', () => {
Expand All @@ -113,6 +118,7 @@ describe('AssetService', () => {

expect(assetMock.create).toHaveBeenCalled();
expect(userMock.updateUsage).toHaveBeenCalledWith(authStub.user1.user.id, file.size);
expect(fs.utimes).toHaveBeenCalledWith(file.originalPath, expect.any(Date), new Date(dto.fileModifiedAt));
});

it('should handle a duplicate', async () => {
Expand Down Expand Up @@ -167,6 +173,16 @@ describe('AssetService', () => {
[{ name: JobName.METADATA_EXTRACTION, data: { id: assetStub.livePhotoStillAsset.id, source: 'upload' } }],
]);
expect(userMock.updateUsage).toHaveBeenCalledWith(authStub.user1.user.id, 111);
expect(fs.utimes).toHaveBeenCalledWith(
fileStub.livePhotoStill.originalPath,
expect.any(Date),
new Date(dto.fileModifiedAt),
);
expect(fs.utimes).toHaveBeenCalledWith(
fileStub.livePhotoMotion.originalPath,
expect.any(Date),
new Date(dto.fileModifiedAt),
);
});
});

Expand Down
10 changes: 10 additions & 0 deletions server/src/immich/api-v1/asset/asset.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
InternalServerErrorException,
NotFoundException,
} from '@nestjs/common';
import { utimes } from 'node:fs/promises';
import { parse } from 'node:path';
import { QueryFailedError } from 'typeorm';
import { IAssetRepositoryV1 } from './asset-repository';
Expand Down Expand Up @@ -80,10 +81,15 @@ export class AssetService {
const libraryId = await this.getLibraryId(auth, dto.libraryId);
await this.access.requirePermission(auth, Permission.ASSET_UPLOAD, libraryId);
this.requireQuota(auth, file.size);
await this.fixModifiedAtTimestamp(file, dto);
if (livePhotoFile) {
await this.fixModifiedAtTimestamp(livePhotoFile, dto);
const livePhotoDto = { ...dto, assetType: AssetType.VIDEO, isVisible: false, libraryId };
livePhotoAsset = await this.create(auth, livePhotoDto, livePhotoFile);
}
if (sidecarFile) {
await this.fixModifiedAtTimestamp(sidecarFile, dto);
}

const asset = await this.create(auth, { ...dto, libraryId }, file, livePhotoAsset?.id, sidecarFile?.originalPath);

Expand Down Expand Up @@ -369,4 +375,8 @@ export class AssetService {
throw new BadRequestException('Quota has been exceeded!');
}
}

private async fixModifiedAtTimestamp(file: UploadFile, dto: CreateAssetDto) {
await utimes(file.originalPath, new Date(), new Date(dto.fileModifiedAt));

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.
}
}
8 changes: 6 additions & 2 deletions server/src/infra/repositories/filesystem.provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import archiver from 'archiver';
import chokidar, { WatchOptions } from 'chokidar';
import { glob } from 'glob';
import { constants, createReadStream, existsSync, mkdirSync } from 'node:fs';
import fs, { copyFile, readdir, rename, writeFile } from 'node:fs/promises';
import fs, { readdir, rename, writeFile } from 'node:fs/promises';
import path from 'node:path';

export class FilesystemProvider implements IStorageRepository {
Expand Down Expand Up @@ -54,7 +54,11 @@ export class FilesystemProvider implements IStorageRepository {

rename = rename;

copyFile = copyFile;
async copyFile(source: string, target: string): Promise<void> {
await fs.copyFile(source, target);
const { atime, mtime } = await fs.stat(source);
await fs.utimes(target, atime, mtime);
}

async checkFileExists(filepath: string, mode = constants.F_OK): Promise<boolean> {
try {
Expand Down

0 comments on commit a5dca64

Please sign in to comment.