feat: change default media location to /data (#20367)

* feat!: change default media location to /data

* feat: dynamically detect media location
This commit is contained in:
Jason Rasmussen
2025-07-29 16:58:50 -04:00
committed by GitHub
parent 4cae15f28d
commit 58521c9efb
39 changed files with 316 additions and 209 deletions
@@ -1,6 +1,5 @@
import { Stats } from 'node:fs';
import { defaults, SystemConfig } from 'src/config';
import { APP_MEDIA_LOCATION } from 'src/constants';
import { AssetPathType, JobStatus } from 'src/enum';
import { StorageTemplateService } from 'src/services/storage-template.service';
import { albumStub } from 'test/fixtures/album.stub';
@@ -111,10 +110,8 @@ describe(StorageTemplateService.name, () => {
it('should migrate single moving picture', async () => {
mocks.user.get.mockResolvedValue(userStub.user1);
const newMotionPicturePath =
APP_MEDIA_LOCATION + `/library/${motionAsset.ownerId}/2022/2022-06-19/${motionAsset.originalFileName}`;
const newStillPicturePath =
APP_MEDIA_LOCATION + `/library/${stillAsset.ownerId}/2022/2022-06-19/${stillAsset.originalFileName}`;
const newMotionPicturePath = `/data/library/${motionAsset.ownerId}/2022/2022-06-19/${motionAsset.originalFileName}`;
const newStillPicturePath = `/data/library/${stillAsset.ownerId}/2022/2022-06-19/${stillAsset.originalFileName}`;
mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(stillAsset);
mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(motionAsset);
@@ -160,7 +157,7 @@ describe(StorageTemplateService.name, () => {
expect(mocks.move.create).toHaveBeenCalledWith({
entityId: asset.id,
newPath: expect.stringContaining(
`upload/library/${user.id}/${asset.fileCreatedAt.getFullYear()}/${album.albumName}/${asset.originalFileName}`,
`/data/library/${user.id}/${asset.fileCreatedAt.getFullYear()}/${album.albumName}/${asset.originalFileName}`,
),
oldPath: asset.originalPath,
pathType: AssetPathType.Original,
@@ -183,7 +180,7 @@ describe(StorageTemplateService.name, () => {
expect(mocks.move.create).toHaveBeenCalledWith({
entityId: asset.id,
newPath: expect.stringContaining(
`upload/library/${user.id}/${asset.fileCreatedAt.getFullYear()}/other/${month}/${asset.originalFileName}`,
`/data/library/${user.id}/${asset.fileCreatedAt.getFullYear()}/other/${month}/${asset.originalFileName}`,
),
oldPath: asset.originalPath,
pathType: AssetPathType.Original,
@@ -219,7 +216,7 @@ describe(StorageTemplateService.name, () => {
expect(mocks.move.create).toHaveBeenCalledWith({
entityId: asset.id,
newPath: expect.stringContaining(
`upload/library/${user.id}/${asset.fileCreatedAt.getFullYear()}/${month} - ${album.albumName}/${asset.originalFileName}`,
`/data/library/${user.id}/${asset.fileCreatedAt.getFullYear()}/${month} - ${album.albumName}/${asset.originalFileName}`,
),
oldPath: asset.originalPath,
pathType: AssetPathType.Original,
@@ -243,9 +240,7 @@ describe(StorageTemplateService.name, () => {
const month = (asset.fileCreatedAt.getMonth() + 1).toString().padStart(2, '0');
expect(mocks.move.create).toHaveBeenCalledWith({
entityId: asset.id,
newPath:
APP_MEDIA_LOCATION +
`/library/${user.id}/${asset.fileCreatedAt.getFullYear()}/${month}/${asset.originalFileName}`,
newPath: `/data/library/${user.id}/${asset.fileCreatedAt.getFullYear()}/${month}/${asset.originalFileName}`,
oldPath: asset.originalPath,
pathType: AssetPathType.Original,
});
@@ -255,9 +250,8 @@ describe(StorageTemplateService.name, () => {
mocks.user.get.mockResolvedValue(userStub.user1);
const asset = assetStub.storageAsset();
const previousFailedNewPath =
APP_MEDIA_LOCATION + `/library/${userStub.user1.id}/2023/Feb/${asset.originalFileName}`;
const newPath = APP_MEDIA_LOCATION + `/library/${userStub.user1.id}/2022/2022-06-19/${asset.originalFileName}`;
const previousFailedNewPath = `/data/library/${userStub.user1.id}/2023/Feb/${asset.originalFileName}`;
const newPath = `/data/library/${userStub.user1.id}/2022/2022-06-19/${asset.originalFileName}`;
mocks.storage.checkFileExists.mockImplementation((path) => Promise.resolve(path === asset.originalPath));
mocks.move.getByEntity.mockResolvedValue({
@@ -296,9 +290,8 @@ describe(StorageTemplateService.name, () => {
mocks.user.get.mockResolvedValue(userStub.user1);
const asset = assetStub.storageAsset({ fileSizeInByte: 5000 });
const previousFailedNewPath =
APP_MEDIA_LOCATION + `/library/${asset.ownerId}/2022/June/${asset.originalFileName}`;
const newPath = APP_MEDIA_LOCATION + `/library/${asset.ownerId}/2022/2022-06-19/${asset.originalFileName}`;
const previousFailedNewPath = `/data/library/${asset.ownerId}/2022/June/${asset.originalFileName}`;
const newPath = `/data/library/${asset.ownerId}/2022/2022-06-19/${asset.originalFileName}`;
mocks.storage.checkFileExists.mockImplementation((path) => Promise.resolve(path === previousFailedNewPath));
mocks.storage.stat.mockResolvedValue({ size: 5000 } as Stats);
@@ -332,8 +325,7 @@ describe(StorageTemplateService.name, () => {
it('should fail move if copying and hash of asset and the new file do not match', async () => {
mocks.user.get.mockResolvedValue(userStub.user1);
const newPath =
APP_MEDIA_LOCATION + `/library/${userStub.user1.id}/2022/2022-06-19/${testAsset.originalFileName}`;
const newPath = `/data/library/${userStub.user1.id}/2022/2022-06-19/${testAsset.originalFileName}`;
mocks.storage.rename.mockRejectedValue({ code: 'EXDEV' });
mocks.storage.stat.mockResolvedValue({ size: 5000 } as Stats);
@@ -375,8 +367,8 @@ describe(StorageTemplateService.name, () => {
'should fail to migrate previously failed move from previous new path when old path no longer exists if $reason validation fails',
async ({ failedPathChecksum, failedPathSize }) => {
mocks.user.get.mockResolvedValue(userStub.user1);
const previousFailedNewPath = `upload/library/${userStub.user1.id}/2023/Feb/${testAsset.originalFileName}`;
const newPath = `upload/library/${userStub.user1.id}/2023/2023-02-23/${testAsset.originalFileName}`;
const previousFailedNewPath = `/data/library/${userStub.user1.id}/2023/Feb/${testAsset.originalFileName}`;
const newPath = `/data/library/${userStub.user1.id}/2023/2023-02-23/${testAsset.originalFileName}`;
mocks.storage.checkFileExists.mockImplementation((path) => Promise.resolve(previousFailedNewPath === path));
mocks.storage.stat.mockResolvedValue({ size: failedPathSize } as Stats);
@@ -423,7 +415,7 @@ describe(StorageTemplateService.name, () => {
it('should handle an asset with a duplicate destination', async () => {
const asset = assetStub.storageAsset();
const oldPath = asset.originalPath;
const newPath = APP_MEDIA_LOCATION + `/library/user-id/2022/2022-06-19/${asset.originalFileName}`;
const newPath = `/data/library/user-id/2022/2022-06-19/${asset.originalFileName}`;
const newPath2 = newPath.replace('.jpg', '+1.jpg');
mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([asset]));
@@ -448,7 +440,7 @@ describe(StorageTemplateService.name, () => {
});
it('should skip when an asset already matches the template', async () => {
const asset = assetStub.storageAsset({ originalPath: 'upload/library/user-id/2023/2023-02-23/asset-id.jpg' });
const asset = assetStub.storageAsset({ originalPath: '/data/library/user-id/2023/2023-02-23/asset-id.jpg' });
mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([asset]));
mocks.user.getList.mockResolvedValue([userStub.user1]);
@@ -463,7 +455,7 @@ describe(StorageTemplateService.name, () => {
});
it('should skip when an asset is probably a duplicate', async () => {
const asset = assetStub.storageAsset({ originalPath: 'upload/library/user-id/2023/2023-02-23/asset-id+1.jpg' });
const asset = assetStub.storageAsset({ originalPath: '/data/library/user-id/2023/2023-02-23/asset-id+1.jpg' });
mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([asset]));
mocks.user.getList.mockResolvedValue([userStub.user1]);
@@ -480,7 +472,7 @@ describe(StorageTemplateService.name, () => {
it('should move an asset', async () => {
const asset = assetStub.storageAsset();
const oldPath = asset.originalPath;
const newPath = APP_MEDIA_LOCATION + `/library/user-id/2022/2022-06-19/${asset.originalFileName}`;
const newPath = `/data/library/user-id/2022/2022-06-19/${asset.originalFileName}`;
mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([asset]));
mocks.user.getList.mockResolvedValue([userStub.user1]);
mocks.move.create.mockResolvedValue({
@@ -508,7 +500,7 @@ describe(StorageTemplateService.name, () => {
entityId: asset.id,
pathType: AssetPathType.Original,
oldPath: asset.originalPath,
newPath: `upload/library/${user.storageLabel}/2023/2023-02-23/${asset.originalFileName}`,
newPath: `/data/library/${user.storageLabel}/2023/2023-02-23/${asset.originalFileName}`,
});
await sut.handleMigration();
@@ -516,12 +508,12 @@ describe(StorageTemplateService.name, () => {
expect(mocks.assetJob.streamForStorageTemplateJob).toHaveBeenCalled();
expect(mocks.storage.rename).toHaveBeenCalledWith(
'/original/path.jpg',
expect.stringContaining(`upload/library/${user.storageLabel}/2022/2022-06-19/${asset.originalFileName}`),
expect.stringContaining(`/data/library/${user.storageLabel}/2022/2022-06-19/${asset.originalFileName}`),
);
expect(mocks.asset.update).toHaveBeenCalledWith({
id: asset.id,
originalPath: expect.stringContaining(
`upload/library/${user.storageLabel}/2022/2022-06-19/${asset.originalFileName}`,
`/data/library/${user.storageLabel}/2022/2022-06-19/${asset.originalFileName}`,
),
});
});
@@ -529,7 +521,7 @@ describe(StorageTemplateService.name, () => {
it('should copy the file if rename fails due to EXDEV (rename across filesystems)', async () => {
const asset = assetStub.storageAsset({ originalPath: '/path/to/original.jpg', fileSizeInByte: 5000 });
const oldPath = asset.originalPath;
const newPath = APP_MEDIA_LOCATION + `/library/user-id/2022/2022-06-19/${asset.originalFileName}`;
const newPath = `/data/library/user-id/2022/2022-06-19/${asset.originalFileName}`;
mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([asset]));
mocks.storage.rename.mockRejectedValue({ code: 'EXDEV' });
mocks.user.getList.mockResolvedValue([userStub.user1]);
@@ -577,7 +569,7 @@ describe(StorageTemplateService.name, () => {
entityId: asset.id,
pathType: AssetPathType.Original,
oldPath: asset.originalPath,
newPath: `upload/library/user-id/2022/2022-06-19/${asset.originalFileName}`,
newPath: `/data/library/user-id/2022/2022-06-19/${asset.originalFileName}`,
});
mocks.storage.stat.mockResolvedValue({
size: 100,
@@ -588,14 +580,14 @@ describe(StorageTemplateService.name, () => {
expect(mocks.assetJob.streamForStorageTemplateJob).toHaveBeenCalled();
expect(mocks.storage.rename).toHaveBeenCalledWith(
'/original/path.jpg',
expect.stringContaining(`upload/library/user-id/2022/2022-06-19/${asset.originalFileName}`),
expect.stringContaining(`/data/library/user-id/2022/2022-06-19/${asset.originalFileName}`),
);
expect(mocks.storage.copyFile).toHaveBeenCalledWith(
'/original/path.jpg',
expect.stringContaining(`upload/library/user-id/2022/2022-06-19/${asset.originalFileName}`),
expect.stringContaining(`/data/library/user-id/2022/2022-06-19/${asset.originalFileName}`),
);
expect(mocks.storage.stat).toHaveBeenCalledWith(
expect.stringContaining(`upload/library/user-id/2022/2022-06-19/${asset.originalFileName}`),
expect.stringContaining(`/data/library/user-id/2022/2022-06-19/${asset.originalFileName}`),
);
expect(mocks.asset.update).not.toHaveBeenCalled();
});
@@ -619,7 +611,7 @@ describe(StorageTemplateService.name, () => {
expect(mocks.assetJob.streamForStorageTemplateJob).toHaveBeenCalled();
expect(mocks.storage.rename).toHaveBeenCalledWith(
'/original/path.jpg',
expect.stringContaining(`upload/library/user-id/2022/2022-06-19/${asset.originalFileName}`),
expect.stringContaining(`/data/library/user-id/2022/2022-06-19/${asset.originalFileName}`),
);
expect(mocks.asset.update).not.toHaveBeenCalled();
});
@@ -630,7 +622,7 @@ describe(StorageTemplateService.name, () => {
const user = factory.userAdmin({ storageLabel: 'label-1' });
const asset = assetStub.storageAsset({
ownerId: user.id,
originalPath: `upload/library/${user.id}/2022/2022-06-19/IMG_7065.heic`,
originalPath: `/data/library/${user.id}/2022/2022-06-19/IMG_7065.heic`,
originalFileName: 'IMG_7065.HEIC',
});
mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([asset]));
@@ -639,16 +631,16 @@ describe(StorageTemplateService.name, () => {
id: '123',
entityId: asset.id,
pathType: AssetPathType.Original,
oldPath: `upload/library/${user.id}/2022/2022-06-19/IMG_7065.heic`,
newPath: `upload/library/${user.id}/2023/2023-02-23/IMG_7065.heic`,
oldPath: `/data/library/${user.id}/2022/2022-06-19/IMG_7065.heic`,
newPath: `/data/library/${user.id}/2023/2023-02-23/IMG_7065.heic`,
});
await sut.handleMigration();
expect(mocks.assetJob.streamForStorageTemplateJob).toHaveBeenCalled();
expect(mocks.storage.rename).toHaveBeenCalledWith(
expect.stringContaining(`upload/library/${user.id}/2022/2022-06-19/IMG_7065.heic`),
expect.stringContaining(`upload/library/${user.storageLabel}/2022/2022-06-19/IMG_7065.heic`),
expect.stringContaining(`/data/library/${user.id}/2022/2022-06-19/IMG_7065.heic`),
expect.stringContaining(`/data/library/${user.storageLabel}/2022/2022-06-19/IMG_7065.heic`),
);
});
@@ -656,7 +648,7 @@ describe(StorageTemplateService.name, () => {
const user = factory.userAdmin();
const asset = assetStub.storageAsset({
ownerId: user.id,
originalPath: `upload/library/${user.id}/2022/2022-06-19/IMG_7065.HEIC`,
originalPath: `/data/library/${user.id}/2022/2022-06-19/IMG_7065.HEIC`,
originalFileName: 'IMG_7065.HEIC',
});
mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([asset]));
@@ -665,16 +657,16 @@ describe(StorageTemplateService.name, () => {
id: '123',
entityId: asset.id,
pathType: AssetPathType.Original,
oldPath: `upload/library/${user.id}/2022/2022-06-19/IMG_7065.HEIC`,
newPath: `upload/library/${user.id}/2023/2023-02-23/IMG_7065.heic`,
oldPath: `/data/library/${user.id}/2022/2022-06-19/IMG_7065.HEIC`,
newPath: `/data/library/${user.id}/2023/2023-02-23/IMG_7065.heic`,
});
await sut.handleMigration();
expect(mocks.assetJob.streamForStorageTemplateJob).toHaveBeenCalled();
expect(mocks.storage.rename).toHaveBeenCalledWith(
expect.stringContaining(`upload/library/${user.id}/2022/2022-06-19/IMG_7065.HEIC`),
expect.stringContaining(`upload/library/${user.id}/2022/2022-06-19/IMG_7065.heic`),
expect.stringContaining(`/data/library/${user.id}/2022/2022-06-19/IMG_7065.HEIC`),
expect.stringContaining(`/data/library/${user.id}/2022/2022-06-19/IMG_7065.heic`),
);
});
@@ -682,7 +674,7 @@ describe(StorageTemplateService.name, () => {
const user = factory.userAdmin();
const asset = assetStub.storageAsset({
ownerId: user.id,
originalPath: `upload/library/${user.id}/2022/2022-06-19/IMG_7065.JPEG`,
originalPath: `/data/library/${user.id}/2022/2022-06-19/IMG_7065.JPEG`,
originalFileName: 'IMG_7065.JPEG',
});
mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([asset]));
@@ -691,16 +683,16 @@ describe(StorageTemplateService.name, () => {
id: '123',
entityId: asset.id,
pathType: AssetPathType.Original,
oldPath: `upload/library/${user.id}/2022/2022-06-19/IMG_7065.JPEG`,
newPath: `upload/library/${user.id}/2023/2023-02-23/IMG_7065.jpg`,
oldPath: `/data/library/${user.id}/2022/2022-06-19/IMG_7065.JPEG`,
newPath: `/data/library/${user.id}/2023/2023-02-23/IMG_7065.jpg`,
});
await sut.handleMigration();
expect(mocks.assetJob.streamForStorageTemplateJob).toHaveBeenCalled();
expect(mocks.storage.rename).toHaveBeenCalledWith(
expect.stringContaining(`upload/library/${user.id}/2022/2022-06-19/IMG_7065.JPEG`),
expect.stringContaining(`upload/library/${user.id}/2022/2022-06-19/IMG_7065.jpg`),
expect.stringContaining(`/data/library/${user.id}/2022/2022-06-19/IMG_7065.JPEG`),
expect.stringContaining(`/data/library/${user.id}/2022/2022-06-19/IMG_7065.jpg`),
);
});
@@ -708,7 +700,7 @@ describe(StorageTemplateService.name, () => {
const user = factory.userAdmin();
const asset = assetStub.storageAsset({
ownerId: user.id,
originalPath: 'upload/library/user-id/2022/2022-06-19/IMG_7065.JPG',
originalPath: '/data/library/user-id/2022/2022-06-19/IMG_7065.JPG',
originalFileName: 'IMG_7065.JPG',
});
mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([asset]));
@@ -717,16 +709,16 @@ describe(StorageTemplateService.name, () => {
id: '123',
entityId: asset.id,
pathType: AssetPathType.Original,
oldPath: `upload/library/${user.id}/2022/2022-06-19/IMG_7065.JPG`,
newPath: `upload/library/${user.id}/2023/2023-02-23/IMG_7065.jpg`,
oldPath: `/data/library/${user.id}/2022/2022-06-19/IMG_7065.JPG`,
newPath: `/data/library/${user.id}/2023/2023-02-23/IMG_7065.jpg`,
});
await sut.handleMigration();
expect(mocks.assetJob.streamForStorageTemplateJob).toHaveBeenCalled();
expect(mocks.storage.rename).toHaveBeenCalledWith(
expect.stringContaining(`upload/library/${user.id}/2022/2022-06-19/IMG_7065.JPG`),
expect.stringContaining(`upload/library/${user.id}/2022/2022-06-19/IMG_7065.jpg`),
expect.stringContaining(`/data/library/${user.id}/2022/2022-06-19/IMG_7065.JPG`),
expect.stringContaining(`/data/library/${user.id}/2022/2022-06-19/IMG_7065.jpg`),
);
});
});