mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-04 03:39:37 -05:00 
			
		
		
		
	feat(server): Storage template support album condition (#12000)
feat(server): Storage template support album condition ([Request](https://github.com/immich-app/immich/discussions/11999))
This commit is contained in:
		
							parent
							
								
									9894b9513b
								
							
						
					
					
						commit
						b051b29eca
					
				@ -15,6 +15,7 @@ import { IStorageRepository } from 'src/interfaces/storage.interface';
 | 
				
			|||||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
 | 
					import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
 | 
				
			||||||
import { IUserRepository } from 'src/interfaces/user.interface';
 | 
					import { IUserRepository } from 'src/interfaces/user.interface';
 | 
				
			||||||
import { StorageTemplateService } from 'src/services/storage-template.service';
 | 
					import { StorageTemplateService } from 'src/services/storage-template.service';
 | 
				
			||||||
 | 
					import { albumStub } from 'test/fixtures/album.stub';
 | 
				
			||||||
import { assetStub } from 'test/fixtures/asset.stub';
 | 
					import { assetStub } from 'test/fixtures/asset.stub';
 | 
				
			||||||
import { userStub } from 'test/fixtures/user.stub';
 | 
					import { userStub } from 'test/fixtures/user.stub';
 | 
				
			||||||
import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock';
 | 
					import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock';
 | 
				
			||||||
@ -83,7 +84,7 @@ describe(StorageTemplateService.name, () => {
 | 
				
			|||||||
          newConfig: {
 | 
					          newConfig: {
 | 
				
			||||||
            storageTemplate: {
 | 
					            storageTemplate: {
 | 
				
			||||||
              template:
 | 
					              template:
 | 
				
			||||||
                '{{y}}{{M}}{{W}}{{d}}{{h}}{{m}}{{s}}{{filename}}{{ext}}{{filetype}}{{filetypefull}}{{assetId}}{{album}}',
 | 
					                '{{y}}{{M}}{{W}}{{d}}{{h}}{{m}}{{s}}{{filename}}{{ext}}{{filetype}}{{filetypefull}}{{assetId}}{{#if album}}{{album}}{{else}}other{{/if}}',
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
          } as SystemConfig,
 | 
					          } as SystemConfig,
 | 
				
			||||||
          oldConfig: {} as SystemConfig,
 | 
					          oldConfig: {} as SystemConfig,
 | 
				
			||||||
@ -163,6 +164,47 @@ describe(StorageTemplateService.name, () => {
 | 
				
			|||||||
        originalPath: newMotionPicturePath,
 | 
					        originalPath: newMotionPicturePath,
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					    it('Should use handlebar if condition for album', async () => {
 | 
				
			||||||
 | 
					      const asset = assetStub.image;
 | 
				
			||||||
 | 
					      const user = userStub.user1;
 | 
				
			||||||
 | 
					      const album = albumStub.oneAsset;
 | 
				
			||||||
 | 
					      const config = structuredClone(defaults);
 | 
				
			||||||
 | 
					      config.storageTemplate.template = '{{y}}/{{#if album}}{{album}}{{else}}other/{{MM}}{{/if}}/{{filename}}';
 | 
				
			||||||
 | 
					      SystemConfigCore.create(systemMock, loggerMock).config$.next(config);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      userMock.get.mockResolvedValue(user);
 | 
				
			||||||
 | 
					      assetMock.getByIds.mockResolvedValueOnce([asset]);
 | 
				
			||||||
 | 
					      albumMock.getByAssetId.mockResolvedValueOnce([album]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(await sut.handleMigrationSingle({ id: asset.id })).toBe(JobStatus.SUCCESS);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(moveMock.create).toHaveBeenCalledWith({
 | 
				
			||||||
 | 
					        entityId: asset.id,
 | 
				
			||||||
 | 
					        newPath: `upload/library/${user.id}/${asset.fileCreatedAt.getFullYear()}/${album.albumName}/${asset.originalFileName}`,
 | 
				
			||||||
 | 
					        oldPath: asset.originalPath,
 | 
				
			||||||
 | 
					        pathType: AssetPathType.ORIGINAL,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    it('Should use handlebar else condition for album', async () => {
 | 
				
			||||||
 | 
					      const asset = assetStub.image;
 | 
				
			||||||
 | 
					      const user = userStub.user1;
 | 
				
			||||||
 | 
					      const config = structuredClone(defaults);
 | 
				
			||||||
 | 
					      config.storageTemplate.template = '{{y}}/{{#if album}}{{album}}{{else}}other//{{MM}}{{/if}}/{{filename}}';
 | 
				
			||||||
 | 
					      SystemConfigCore.create(systemMock, loggerMock).config$.next(config);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      userMock.get.mockResolvedValue(user);
 | 
				
			||||||
 | 
					      assetMock.getByIds.mockResolvedValueOnce([asset]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(await sut.handleMigrationSingle({ id: asset.id })).toBe(JobStatus.SUCCESS);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const month = (asset.fileCreatedAt.getMonth() + 1).toString().padStart(2, '0');
 | 
				
			||||||
 | 
					      expect(moveMock.create).toHaveBeenCalledWith({
 | 
				
			||||||
 | 
					        entityId: asset.id,
 | 
				
			||||||
 | 
					        newPath: `upload/library/${user.id}/${asset.fileCreatedAt.getFullYear()}/other/${month}/${asset.originalFileName}`,
 | 
				
			||||||
 | 
					        oldPath: asset.originalPath,
 | 
				
			||||||
 | 
					        pathType: AssetPathType.ORIGINAL,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
    it('should migrate previously failed move from original path when it still exists', async () => {
 | 
					    it('should migrate previously failed move from original path when it still exists', async () => {
 | 
				
			||||||
      userMock.get.mockResolvedValue(userStub.user1);
 | 
					      userMock.get.mockResolvedValue(userStub.user1);
 | 
				
			||||||
      const previousFailedNewPath = `upload/library/${userStub.user1.id}/2023/Feb/${assetStub.image.id}.jpg`;
 | 
					      const previousFailedNewPath = `upload/library/${userStub.user1.id}/2023/Feb/${assetStub.image.id}.jpg`;
 | 
				
			||||||
 | 
				
			|||||||
@ -308,7 +308,7 @@ export class StorageTemplateService {
 | 
				
			|||||||
      filetypefull: asset.type == AssetType.IMAGE ? 'IMAGE' : 'VIDEO',
 | 
					      filetypefull: asset.type == AssetType.IMAGE ? 'IMAGE' : 'VIDEO',
 | 
				
			||||||
      assetId: asset.id,
 | 
					      assetId: asset.id,
 | 
				
			||||||
      //just throw into the root if it doesn't belong to an album
 | 
					      //just throw into the root if it doesn't belong to an album
 | 
				
			||||||
      album: (albumName && sanitize(albumName.replaceAll(/\.+/g, ''))) || '.',
 | 
					      album: (albumName && sanitize(albumName.replaceAll(/\.+/g, ''))) || '',
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const systemTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
 | 
					    const systemTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
 | 
				
			||||||
@ -329,6 +329,6 @@ export class StorageTemplateService {
 | 
				
			|||||||
      substitutions[token] = dt.toFormat(token);
 | 
					      substitutions[token] = dt.toFormat(token);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return template(substitutions);
 | 
					    return template(substitutions).replaceAll(/\/{2,}/gm, '/');
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user