2023-06-16 21:54:17 +02:00
import { when } from 'jest-when' ;
2023-12-29 19:41:33 +01:00
import { Stats } from 'node:fs' ;
2024-03-20 21:20:38 +01:00
import { SystemConfigCore , defaults } from 'src/cores/system-config.core' ;
2024-03-20 22:02:51 +01:00
import { AssetPathType } from 'src/entities/move.entity' ;
import { SystemConfig , SystemConfigKey } from 'src/entities/system-config.entity' ;
2024-03-21 12:59:49 +01:00
import { IAlbumRepository } from 'src/interfaces/album.interface' ;
import { IAssetRepository } from 'src/interfaces/asset.interface' ;
import { ICryptoRepository } from 'src/interfaces/crypto.interface' ;
import { IDatabaseRepository } from 'src/interfaces/database.interface' ;
import { JobStatus } from 'src/interfaces/job.interface' ;
import { IMoveRepository } from 'src/interfaces/move.interface' ;
import { IPersonRepository } from 'src/interfaces/person.interface' ;
import { IStorageRepository } from 'src/interfaces/storage.interface' ;
import { ISystemConfigRepository } from 'src/interfaces/system-config.interface' ;
import { IUserRepository } from 'src/interfaces/user.interface' ;
2024-03-21 00:07:30 +01:00
import { StorageTemplateService } from 'src/services/storage-template.service' ;
2024-03-20 19:32:04 +01:00
import { assetStub } from 'test/fixtures/asset.stub' ;
import { userStub } from 'test/fixtures/user.stub' ;
import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock' ;
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock' ;
import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock' ;
import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock' ;
import { newMoveRepositoryMock } from 'test/repositories/move.repository.mock' ;
import { newPersonRepositoryMock } from 'test/repositories/person.repository.mock' ;
import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock' ;
import { newSystemConfigRepositoryMock } from 'test/repositories/system-config.repository.mock' ;
import { newUserRepositoryMock } from 'test/repositories/user.repository.mock' ;
2023-02-25 15:12:03 +01:00
describe ( StorageTemplateService . name , ( ) = > {
let sut : StorageTemplateService ;
2023-10-23 20:00:31 +02:00
let albumMock : jest.Mocked < IAlbumRepository > ;
2023-02-25 15:12:03 +01:00
let assetMock : jest.Mocked < IAssetRepository > ;
let configMock : jest.Mocked < ISystemConfigRepository > ;
2023-10-11 04:14:44 +02:00
let moveMock : jest.Mocked < IMoveRepository > ;
let personMock : jest.Mocked < IPersonRepository > ;
2023-02-25 15:12:03 +01:00
let storageMock : jest.Mocked < IStorageRepository > ;
2023-05-22 05:18:10 +02:00
let userMock : jest.Mocked < IUserRepository > ;
2023-12-29 19:41:33 +01:00
let cryptoMock : jest.Mocked < ICryptoRepository > ;
2024-01-01 19:16:44 +01:00
let databaseMock : jest.Mocked < IDatabaseRepository > ;
2023-02-25 15:12:03 +01:00
it ( 'should work' , ( ) = > {
expect ( sut ) . toBeDefined ( ) ;
} ) ;
2024-03-05 23:23:06 +01:00
beforeEach ( ( ) = > {
2023-12-29 19:41:33 +01:00
configMock = newSystemConfigRepositoryMock ( ) ;
2023-02-25 15:12:03 +01:00
assetMock = newAssetRepositoryMock ( ) ;
2023-10-23 20:00:31 +02:00
albumMock = newAlbumRepositoryMock ( ) ;
2023-10-11 04:14:44 +02:00
moveMock = newMoveRepositoryMock ( ) ;
personMock = newPersonRepositoryMock ( ) ;
2023-02-25 15:12:03 +01:00
storageMock = newStorageRepositoryMock ( ) ;
2023-05-22 05:18:10 +02:00
userMock = newUserRepositoryMock ( ) ;
2023-12-29 19:41:33 +01:00
cryptoMock = newCryptoRepositoryMock ( ) ;
2024-01-01 19:16:44 +01:00
databaseMock = newDatabaseRepositoryMock ( ) ;
configMock . load . mockResolvedValue ( [ { key : SystemConfigKey.STORAGE_TEMPLATE_ENABLED , value : true } ] ) ;
2023-05-22 05:18:10 +02:00
2023-10-23 20:00:31 +02:00
sut = new StorageTemplateService (
albumMock ,
assetMock ,
configMock ,
moveMock ,
personMock ,
storageMock ,
userMock ,
2023-12-29 19:41:33 +01:00
cryptoMock ,
2024-01-01 19:16:44 +01:00
databaseMock ,
2023-10-23 20:00:31 +02:00
) ;
2023-12-29 19:41:33 +01:00
2024-01-01 19:16:44 +01:00
SystemConfigCore . create ( configMock ) . config $ . next ( defaults ) ;
2023-02-25 15:12:03 +01:00
} ) ;
2024-03-22 23:24:02 +01:00
describe ( 'onValidateConfig' , ( ) = > {
2024-03-17 20:16:02 +01:00
it ( 'should allow valid templates' , ( ) = > {
expect ( ( ) = >
2024-03-22 23:24:02 +01:00
sut . onValidateConfig ( {
2024-03-17 20:16:02 +01:00
newConfig : {
storageTemplate : {
template :
'{{y}}{{M}}{{W}}{{d}}{{h}}{{m}}{{s}}{{filename}}{{ext}}{{filetype}}{{filetypefull}}{{assetId}}{{album}}' ,
} ,
} as SystemConfig ,
oldConfig : { } as SystemConfig ,
} ) ,
) . not . toThrow ( ) ;
} ) ;
it ( 'should fail for an invalid template' , ( ) = > {
expect ( ( ) = >
2024-03-22 23:24:02 +01:00
sut . onValidateConfig ( {
2024-03-17 20:16:02 +01:00
newConfig : {
storageTemplate : {
template : '{{foo}}' ,
} ,
} as SystemConfig ,
oldConfig : { } as SystemConfig ,
} ) ,
) . toThrow ( /Invalid storage template.*/ ) ;
} ) ;
} ) ;
2023-09-11 17:56:38 +02:00
describe ( 'handleMigrationSingle' , ( ) = > {
2023-12-29 19:41:33 +01:00
it ( 'should skip when storage template is disabled' , async ( ) = > {
configMock . load . mockResolvedValue ( [ { key : SystemConfigKey.STORAGE_TEMPLATE_ENABLED , value : false } ] ) ;
2024-03-15 14:16:54 +01:00
await expect ( sut . handleMigrationSingle ( { id : assetStub.image.id } ) ) . resolves . toBe ( JobStatus . SKIPPED ) ;
2023-12-29 19:41:33 +01:00
expect ( assetMock . getByIds ) . not . toHaveBeenCalled ( ) ;
expect ( storageMock . checkFileExists ) . not . toHaveBeenCalled ( ) ;
expect ( storageMock . rename ) . not . toHaveBeenCalled ( ) ;
expect ( storageMock . copyFile ) . not . toHaveBeenCalled ( ) ;
2024-03-20 03:42:10 +01:00
expect ( assetMock . update ) . not . toHaveBeenCalled ( ) ;
2023-12-29 19:41:33 +01:00
expect ( moveMock . create ) . not . toHaveBeenCalled ( ) ;
expect ( moveMock . update ) . not . toHaveBeenCalled ( ) ;
expect ( storageMock . stat ) . not . toHaveBeenCalled ( ) ;
} ) ;
2023-09-11 17:56:38 +02:00
it ( 'should migrate single moving picture' , async ( ) = > {
userMock . get . mockResolvedValue ( userStub . user1 ) ;
2023-12-29 19:41:33 +01:00
const newMotionPicturePath = ` upload/library/ ${ userStub . user1 . id } /2022/2022-06-19/ ${ assetStub . livePhotoStillAsset . id } .mp4 ` ;
const newStillPicturePath = ` upload/library/ ${ userStub . user1 . id } /2022/2022-06-19/ ${ assetStub . livePhotoStillAsset . id } .jpeg ` ;
2023-09-11 17:56:38 +02:00
when ( assetMock . getByIds )
2024-03-14 06:58:09 +01:00
. calledWith ( [ assetStub . livePhotoStillAsset . id ] , { exifInfo : true } )
2023-09-11 17:56:38 +02:00
. mockResolvedValue ( [ assetStub . livePhotoStillAsset ] ) ;
when ( assetMock . getByIds )
2024-03-14 06:58:09 +01:00
. calledWith ( [ assetStub . livePhotoMotionAsset . id ] , { exifInfo : true } )
2023-09-11 17:56:38 +02:00
. mockResolvedValue ( [ assetStub . livePhotoMotionAsset ] ) ;
2023-12-29 19:41:33 +01:00
when ( moveMock . create )
. calledWith ( {
entityId : assetStub.livePhotoStillAsset.id ,
pathType : AssetPathType.ORIGINAL ,
oldPath : assetStub.livePhotoStillAsset.originalPath ,
newPath : newStillPicturePath ,
} )
. mockResolvedValue ( {
id : '123' ,
entityId : assetStub.livePhotoStillAsset.id ,
pathType : AssetPathType.ORIGINAL ,
oldPath : assetStub.livePhotoStillAsset.originalPath ,
newPath : newStillPicturePath ,
} ) ;
when ( moveMock . create )
. calledWith ( {
entityId : assetStub.livePhotoMotionAsset.id ,
pathType : AssetPathType.ORIGINAL ,
oldPath : assetStub.livePhotoMotionAsset.originalPath ,
newPath : newMotionPicturePath ,
} )
. mockResolvedValue ( {
id : '124' ,
entityId : assetStub.livePhotoMotionAsset.id ,
pathType : AssetPathType.ORIGINAL ,
oldPath : assetStub.livePhotoMotionAsset.originalPath ,
newPath : newMotionPicturePath ,
} ) ;
2024-03-15 14:16:54 +01:00
await expect ( sut . handleMigrationSingle ( { id : assetStub.livePhotoStillAsset.id } ) ) . resolves . toBe (
JobStatus . SUCCESS ,
) ;
2023-09-11 17:56:38 +02:00
2024-03-14 06:58:09 +01:00
expect ( assetMock . getByIds ) . toHaveBeenCalledWith ( [ assetStub . livePhotoStillAsset . id ] , { exifInfo : true } ) ;
expect ( assetMock . getByIds ) . toHaveBeenCalledWith ( [ assetStub . livePhotoMotionAsset . id ] , { exifInfo : true } ) ;
2023-12-29 19:41:33 +01:00
expect ( storageMock . checkFileExists ) . toHaveBeenCalledTimes ( 2 ) ;
2024-03-20 03:42:10 +01:00
expect ( assetMock . update ) . toHaveBeenCalledWith ( {
2023-12-29 19:41:33 +01:00
id : assetStub.livePhotoStillAsset.id ,
originalPath : newStillPicturePath ,
} ) ;
2024-03-20 03:42:10 +01:00
expect ( assetMock . update ) . toHaveBeenCalledWith ( {
2023-12-29 19:41:33 +01:00
id : assetStub.livePhotoMotionAsset.id ,
originalPath : newMotionPicturePath ,
} ) ;
} ) ;
it ( 'should migrate previously failed move from original path when it still exists' , async ( ) = > {
userMock . get . mockResolvedValue ( userStub . user1 ) ;
const previousFailedNewPath = ` upload/library/ ${ userStub . user1 . id } /2023/Feb/ ${ assetStub . image . id } .jpg ` ;
const newPath = ` upload/library/ ${ userStub . user1 . id } /2023/2023-02-23/ ${ assetStub . image . id } .jpg ` ;
when ( storageMock . checkFileExists ) . calledWith ( assetStub . image . originalPath ) . mockResolvedValue ( true ) ;
when ( storageMock . checkFileExists ) . calledWith ( previousFailedNewPath ) . mockResolvedValue ( false ) ;
when ( moveMock . getByEntity ) . calledWith ( assetStub . image . id , AssetPathType . ORIGINAL ) . mockResolvedValue ( {
id : '123' ,
entityId : assetStub.image.id ,
pathType : AssetPathType.ORIGINAL ,
oldPath : assetStub.image.originalPath ,
newPath : previousFailedNewPath ,
} ) ;
2024-03-14 06:58:09 +01:00
when ( assetMock . getByIds )
. calledWith ( [ assetStub . image . id ] , { exifInfo : true } )
. mockResolvedValue ( [ assetStub . image ] ) ;
2023-12-29 19:41:33 +01:00
when ( moveMock . update )
. calledWith ( {
id : '123' ,
oldPath : assetStub.image.originalPath ,
newPath ,
} )
. mockResolvedValue ( {
id : '123' ,
entityId : assetStub.image.id ,
pathType : AssetPathType.ORIGINAL ,
oldPath : assetStub.image.originalPath ,
newPath ,
} ) ;
2024-03-15 14:16:54 +01:00
await expect ( sut . handleMigrationSingle ( { id : assetStub.image.id } ) ) . resolves . toBe ( JobStatus . SUCCESS ) ;
2023-12-29 19:41:33 +01:00
2024-03-14 06:58:09 +01:00
expect ( assetMock . getByIds ) . toHaveBeenCalledWith ( [ assetStub . image . id ] , { exifInfo : true } ) ;
2023-12-29 19:41:33 +01:00
expect ( storageMock . checkFileExists ) . toHaveBeenCalledTimes ( 3 ) ;
expect ( storageMock . rename ) . toHaveBeenCalledWith ( assetStub . image . originalPath , newPath ) ;
expect ( moveMock . update ) . toHaveBeenCalledWith ( {
id : '123' ,
oldPath : assetStub.image.originalPath ,
newPath ,
} ) ;
2024-03-20 03:42:10 +01:00
expect ( assetMock . update ) . toHaveBeenCalledWith ( {
2023-12-29 19:41:33 +01:00
id : assetStub.image.id ,
originalPath : newPath ,
} ) ;
} ) ;
it ( 'should migrate previously failed move from previous new path when old path no longer exists, should validate file size still matches before moving' , async ( ) = > {
userMock . get . mockResolvedValue ( userStub . user1 ) ;
const previousFailedNewPath = ` upload/library/ ${ userStub . user1 . id } /2023/Feb/ ${ assetStub . image . id } .jpg ` ;
const newPath = ` upload/library/ ${ userStub . user1 . id } /2023/2023-02-23/ ${ assetStub . image . id } .jpg ` ;
when ( storageMock . checkFileExists ) . calledWith ( assetStub . image . originalPath ) . mockResolvedValue ( false ) ;
when ( storageMock . checkFileExists ) . calledWith ( previousFailedNewPath ) . mockResolvedValue ( true ) ;
when ( storageMock . stat )
. calledWith ( previousFailedNewPath )
. mockResolvedValue ( { size : 5000 } as Stats ) ;
when ( cryptoMock . hashFile ) . calledWith ( previousFailedNewPath ) . mockResolvedValue ( assetStub . image . checksum ) ;
when ( moveMock . getByEntity ) . calledWith ( assetStub . image . id , AssetPathType . ORIGINAL ) . mockResolvedValue ( {
id : '123' ,
entityId : assetStub.image.id ,
pathType : AssetPathType.ORIGINAL ,
oldPath : assetStub.image.originalPath ,
newPath : previousFailedNewPath ,
} ) ;
2024-03-14 06:58:09 +01:00
when ( assetMock . getByIds )
. calledWith ( [ assetStub . image . id ] , { exifInfo : true } )
. mockResolvedValue ( [ assetStub . image ] ) ;
2023-12-29 19:41:33 +01:00
when ( moveMock . update )
. calledWith ( {
id : '123' ,
oldPath : previousFailedNewPath ,
newPath ,
} )
. mockResolvedValue ( {
id : '123' ,
entityId : assetStub.image.id ,
pathType : AssetPathType.ORIGINAL ,
oldPath : previousFailedNewPath ,
newPath ,
} ) ;
2024-03-15 14:16:54 +01:00
await expect ( sut . handleMigrationSingle ( { id : assetStub.image.id } ) ) . resolves . toBe ( JobStatus . SUCCESS ) ;
2023-12-29 19:41:33 +01:00
2024-03-14 06:58:09 +01:00
expect ( assetMock . getByIds ) . toHaveBeenCalledWith ( [ assetStub . image . id ] , { exifInfo : true } ) ;
2023-12-29 19:41:33 +01:00
expect ( storageMock . checkFileExists ) . toHaveBeenCalledTimes ( 3 ) ;
expect ( storageMock . stat ) . toHaveBeenCalledWith ( previousFailedNewPath ) ;
expect ( storageMock . rename ) . toHaveBeenCalledWith ( previousFailedNewPath , newPath ) ;
expect ( storageMock . copyFile ) . not . toHaveBeenCalled ( ) ;
expect ( moveMock . update ) . toHaveBeenCalledWith ( {
id : '123' ,
oldPath : previousFailedNewPath ,
newPath ,
} ) ;
2024-03-20 03:42:10 +01:00
expect ( assetMock . update ) . toHaveBeenCalledWith ( {
2023-12-29 19:41:33 +01:00
id : assetStub.image.id ,
originalPath : newPath ,
} ) ;
} ) ;
it ( 'should fail move if copying and hash of asset and the new file do not match' , async ( ) = > {
userMock . get . mockResolvedValue ( userStub . user1 ) ;
const newPath = ` upload/library/ ${ userStub . user1 . id } /2023/2023-02-23/ ${ assetStub . image . id } .jpg ` ;
when ( storageMock . rename ) . calledWith ( assetStub . image . originalPath , newPath ) . mockRejectedValue ( { code : 'EXDEV' } ) ;
when ( storageMock . stat )
. calledWith ( newPath )
. mockResolvedValue ( { size : 5000 } as Stats ) ;
2024-02-02 04:18:00 +01:00
when ( cryptoMock . hashFile ) . calledWith ( newPath ) . mockResolvedValue ( Buffer . from ( 'different-hash' , 'utf8' ) ) ;
2023-12-29 19:41:33 +01:00
2024-03-14 06:58:09 +01:00
when ( assetMock . getByIds )
. calledWith ( [ assetStub . image . id ] , { exifInfo : true } )
. mockResolvedValue ( [ assetStub . image ] ) ;
2023-12-29 19:41:33 +01:00
when ( moveMock . create )
. calledWith ( {
entityId : assetStub.image.id ,
pathType : AssetPathType.ORIGINAL ,
oldPath : assetStub.image.originalPath ,
newPath : newPath ,
} )
. mockResolvedValue ( {
id : '123' ,
entityId : assetStub.image.id ,
pathType : AssetPathType.ORIGINAL ,
oldPath : assetStub.image.originalPath ,
newPath ,
} ) ;
2024-03-15 14:16:54 +01:00
await expect ( sut . handleMigrationSingle ( { id : assetStub.image.id } ) ) . resolves . toBe ( JobStatus . SUCCESS ) ;
2023-12-29 19:41:33 +01:00
2024-03-14 06:58:09 +01:00
expect ( assetMock . getByIds ) . toHaveBeenCalledWith ( [ assetStub . image . id ] , { exifInfo : true } ) ;
2023-12-29 19:41:33 +01:00
expect ( storageMock . checkFileExists ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( storageMock . stat ) . toHaveBeenCalledWith ( newPath ) ;
expect ( moveMock . create ) . toHaveBeenCalledWith ( {
entityId : assetStub.image.id ,
pathType : AssetPathType.ORIGINAL ,
oldPath : assetStub.image.originalPath ,
newPath : newPath ,
} ) ;
expect ( storageMock . rename ) . toHaveBeenCalledWith ( assetStub . image . originalPath , newPath ) ;
expect ( storageMock . copyFile ) . toHaveBeenCalledWith ( assetStub . image . originalPath , newPath ) ;
expect ( storageMock . unlink ) . toHaveBeenCalledWith ( newPath ) ;
expect ( storageMock . unlink ) . toHaveBeenCalledTimes ( 1 ) ;
2024-03-20 03:42:10 +01:00
expect ( assetMock . update ) . not . toHaveBeenCalled ( ) ;
2023-09-11 17:56:38 +02:00
} ) ;
2023-12-29 19:41:33 +01:00
it . each `
2024-02-02 04:18:00 +01:00
failedPathChecksum | failedPathSize | reason
$ { assetStub . image . checksum } | $ { 500 } | $ { 'file size' }
$ { Buffer . from ( 'bad checksum' , 'utf8' ) } | $ { assetStub . image . exifInfo ? . fileSizeInByte } | $ { 'checksum' }
2023-12-29 19:41:33 +01:00
` (
'should fail to migrate previously failed move from previous new path when old path no longer exists if $reason validation fails' ,
async ( { failedPathChecksum , failedPathSize } ) = > {
userMock . get . mockResolvedValue ( userStub . user1 ) ;
const previousFailedNewPath = ` upload/library/ ${ userStub . user1 . id } /2023/Feb/ ${ assetStub . image . id } .jpg ` ;
const newPath = ` upload/library/ ${ userStub . user1 . id } /2023/2023-02-23/ ${ assetStub . image . id } .jpg ` ;
when ( storageMock . checkFileExists ) . calledWith ( assetStub . image . originalPath ) . mockResolvedValue ( false ) ;
when ( storageMock . checkFileExists ) . calledWith ( previousFailedNewPath ) . mockResolvedValue ( true ) ;
when ( storageMock . stat )
. calledWith ( previousFailedNewPath )
. mockResolvedValue ( { size : failedPathSize } as Stats ) ;
when ( cryptoMock . hashFile ) . calledWith ( previousFailedNewPath ) . mockResolvedValue ( failedPathChecksum ) ;
when ( moveMock . getByEntity ) . calledWith ( assetStub . image . id , AssetPathType . ORIGINAL ) . mockResolvedValue ( {
id : '123' ,
entityId : assetStub.image.id ,
pathType : AssetPathType.ORIGINAL ,
oldPath : assetStub.image.originalPath ,
newPath : previousFailedNewPath ,
} ) ;
2024-03-14 06:58:09 +01:00
when ( assetMock . getByIds )
. calledWith ( [ assetStub . image . id ] , { exifInfo : true } )
. mockResolvedValue ( [ assetStub . image ] ) ;
2023-12-29 19:41:33 +01:00
when ( moveMock . update )
. calledWith ( {
id : '123' ,
oldPath : previousFailedNewPath ,
newPath ,
} )
. mockResolvedValue ( {
id : '123' ,
entityId : assetStub.image.id ,
pathType : AssetPathType.ORIGINAL ,
oldPath : previousFailedNewPath ,
newPath ,
} ) ;
2024-03-15 14:16:54 +01:00
await expect ( sut . handleMigrationSingle ( { id : assetStub.image.id } ) ) . resolves . toBe ( JobStatus . SUCCESS ) ;
2023-12-29 19:41:33 +01:00
2024-03-14 06:58:09 +01:00
expect ( assetMock . getByIds ) . toHaveBeenCalledWith ( [ assetStub . image . id ] , { exifInfo : true } ) ;
2023-12-29 19:41:33 +01:00
expect ( storageMock . checkFileExists ) . toHaveBeenCalledTimes ( 3 ) ;
expect ( storageMock . stat ) . toHaveBeenCalledWith ( previousFailedNewPath ) ;
expect ( storageMock . rename ) . not . toHaveBeenCalled ( ) ;
expect ( storageMock . copyFile ) . not . toHaveBeenCalled ( ) ;
expect ( moveMock . update ) . not . toHaveBeenCalled ( ) ;
2024-03-20 03:42:10 +01:00
expect ( assetMock . update ) . not . toHaveBeenCalled ( ) ;
2023-12-29 19:41:33 +01:00
} ,
) ;
2023-09-11 17:56:38 +02:00
} ) ;
2023-02-25 15:12:03 +01:00
describe ( 'handle template migration' , ( ) = > {
it ( 'should handle no assets' , async ( ) = > {
2023-05-22 20:05:06 +02:00
assetMock . getAll . mockResolvedValue ( {
items : [ ] ,
hasNextPage : false ,
} ) ;
2023-05-22 05:18:10 +02:00
userMock . getList . mockResolvedValue ( [ ] ) ;
2023-02-25 15:12:03 +01:00
2023-05-26 14:52:52 +02:00
await sut . handleMigration ( ) ;
2023-02-25 15:12:03 +01:00
expect ( assetMock . getAll ) . toHaveBeenCalled ( ) ;
} ) ;
it ( 'should handle an asset with a duplicate destination' , async ( ) = > {
2023-05-22 20:05:06 +02:00
assetMock . getAll . mockResolvedValue ( {
2023-08-01 03:28:07 +02:00
items : [ assetStub . image ] ,
2023-05-22 20:05:06 +02:00
hasNextPage : false ,
} ) ;
2023-08-01 03:28:07 +02:00
userMock . getList . mockResolvedValue ( [ userStub . user1 ] ) ;
2023-10-11 04:14:44 +02:00
moveMock . create . mockResolvedValue ( {
id : '123' ,
entityId : assetStub.image.id ,
pathType : AssetPathType.ORIGINAL ,
oldPath : assetStub.image.originalPath ,
newPath : 'upload/library/user-id/2023/2023-02-23/asset-id+1.jpg' ,
} ) ;
2023-02-25 15:12:03 +01:00
when ( storageMock . checkFileExists )
2023-07-10 19:56:45 +02:00
. calledWith ( 'upload/library/user-id/2023/2023-02-23/asset-id.jpg' )
2023-02-25 15:12:03 +01:00
. mockResolvedValue ( true ) ;
when ( storageMock . checkFileExists )
2023-07-10 19:56:45 +02:00
. calledWith ( 'upload/library/user-id/2023/2023-02-23/asset-id+1.jpg' )
2023-02-25 15:12:03 +01:00
. mockResolvedValue ( false ) ;
2023-05-26 14:52:52 +02:00
await sut . handleMigration ( ) ;
2023-02-25 15:12:03 +01:00
expect ( assetMock . getAll ) . toHaveBeenCalled ( ) ;
expect ( storageMock . checkFileExists ) . toHaveBeenCalledTimes ( 2 ) ;
2024-03-20 03:42:10 +01:00
expect ( assetMock . update ) . toHaveBeenCalledWith ( {
2023-08-01 03:28:07 +02:00
id : assetStub.image.id ,
2023-07-10 19:56:45 +02:00
originalPath : 'upload/library/user-id/2023/2023-02-23/asset-id+1.jpg' ,
2023-02-25 15:12:03 +01:00
} ) ;
2023-05-22 05:18:10 +02:00
expect ( userMock . getList ) . toHaveBeenCalled ( ) ;
2023-02-25 15:12:03 +01:00
} ) ;
it ( 'should skip when an asset already matches the template' , async ( ) = > {
2023-05-22 20:05:06 +02:00
assetMock . getAll . mockResolvedValue ( {
items : [
{
2023-08-01 03:28:07 +02:00
. . . assetStub . image ,
2023-07-10 19:56:45 +02:00
originalPath : 'upload/library/user-id/2023/2023-02-23/asset-id.jpg' ,
2023-05-22 20:05:06 +02:00
} ,
] ,
hasNextPage : false ,
} ) ;
2023-08-01 03:28:07 +02:00
userMock . getList . mockResolvedValue ( [ userStub . user1 ] ) ;
2023-02-25 15:12:03 +01:00
2023-05-26 14:52:52 +02:00
await sut . handleMigration ( ) ;
2023-02-25 15:12:03 +01:00
expect ( assetMock . getAll ) . toHaveBeenCalled ( ) ;
2023-12-29 19:41:33 +01:00
expect ( storageMock . rename ) . not . toHaveBeenCalled ( ) ;
expect ( storageMock . copyFile ) . not . toHaveBeenCalled ( ) ;
2023-02-25 15:12:03 +01:00
expect ( storageMock . checkFileExists ) . not . toHaveBeenCalledTimes ( 2 ) ;
2024-03-20 03:42:10 +01:00
expect ( assetMock . update ) . not . toHaveBeenCalled ( ) ;
2023-02-25 15:12:03 +01:00
} ) ;
it ( 'should skip when an asset is probably a duplicate' , async ( ) = > {
2023-05-22 20:05:06 +02:00
assetMock . getAll . mockResolvedValue ( {
items : [
{
2023-08-01 03:28:07 +02:00
. . . assetStub . image ,
2023-07-10 19:56:45 +02:00
originalPath : 'upload/library/user-id/2023/2023-02-23/asset-id+1.jpg' ,
2023-05-22 20:05:06 +02:00
} ,
] ,
hasNextPage : false ,
} ) ;
2023-08-01 03:28:07 +02:00
userMock . getList . mockResolvedValue ( [ userStub . user1 ] ) ;
2023-02-25 15:12:03 +01:00
2023-05-26 14:52:52 +02:00
await sut . handleMigration ( ) ;
2023-02-25 15:12:03 +01:00
expect ( assetMock . getAll ) . toHaveBeenCalled ( ) ;
2023-12-29 19:41:33 +01:00
expect ( storageMock . rename ) . not . toHaveBeenCalled ( ) ;
expect ( storageMock . copyFile ) . not . toHaveBeenCalled ( ) ;
2023-02-25 15:12:03 +01:00
expect ( storageMock . checkFileExists ) . not . toHaveBeenCalledTimes ( 2 ) ;
2024-03-20 03:42:10 +01:00
expect ( assetMock . update ) . not . toHaveBeenCalled ( ) ;
2023-02-25 15:12:03 +01:00
} ) ;
it ( 'should move an asset' , async ( ) = > {
2023-05-22 20:05:06 +02:00
assetMock . getAll . mockResolvedValue ( {
2023-08-01 03:28:07 +02:00
items : [ assetStub . image ] ,
2023-05-22 20:05:06 +02:00
hasNextPage : false ,
} ) ;
2023-08-01 03:28:07 +02:00
userMock . getList . mockResolvedValue ( [ userStub . user1 ] ) ;
2023-10-11 04:14:44 +02:00
moveMock . create . mockResolvedValue ( {
id : '123' ,
entityId : assetStub.image.id ,
pathType : AssetPathType.ORIGINAL ,
oldPath : assetStub.image.originalPath ,
newPath : 'upload/library/user-id/2023/2023-02-23/asset-id.jpg' ,
} ) ;
2023-02-25 15:12:03 +01:00
2023-05-26 14:52:52 +02:00
await sut . handleMigration ( ) ;
2023-02-25 15:12:03 +01:00
expect ( assetMock . getAll ) . toHaveBeenCalled ( ) ;
2023-12-29 19:41:33 +01:00
expect ( storageMock . rename ) . toHaveBeenCalledWith (
2023-07-10 19:56:45 +02:00
'/original/path.jpg' ,
'upload/library/user-id/2023/2023-02-23/asset-id.jpg' ,
2023-02-25 15:12:03 +01:00
) ;
2024-03-20 03:42:10 +01:00
expect ( assetMock . update ) . toHaveBeenCalledWith ( {
2023-08-01 03:28:07 +02:00
id : assetStub.image.id ,
2023-07-10 19:56:45 +02:00
originalPath : 'upload/library/user-id/2023/2023-02-23/asset-id.jpg' ,
2023-02-25 15:12:03 +01:00
} ) ;
} ) ;
2023-05-22 05:18:10 +02:00
it ( 'should use the user storage label' , async ( ) = > {
2023-05-22 20:05:06 +02:00
assetMock . getAll . mockResolvedValue ( {
2023-08-01 03:28:07 +02:00
items : [ assetStub . image ] ,
2023-05-22 20:05:06 +02:00
hasNextPage : false ,
} ) ;
2023-08-01 03:28:07 +02:00
userMock . getList . mockResolvedValue ( [ userStub . storageLabel ] ) ;
2023-10-11 04:14:44 +02:00
moveMock . create . mockResolvedValue ( {
id : '123' ,
entityId : assetStub.image.id ,
pathType : AssetPathType.ORIGINAL ,
oldPath : assetStub.image.originalPath ,
newPath : 'upload/library/user-id/2023/2023-02-23/asset-id.jpg' ,
} ) ;
2023-05-22 05:18:10 +02:00
2023-05-26 14:52:52 +02:00
await sut . handleMigration ( ) ;
2023-05-22 05:18:10 +02:00
expect ( assetMock . getAll ) . toHaveBeenCalled ( ) ;
2023-12-29 19:41:33 +01:00
expect ( storageMock . rename ) . toHaveBeenCalledWith (
2023-07-10 19:56:45 +02:00
'/original/path.jpg' ,
'upload/library/label-1/2023/2023-02-23/asset-id.jpg' ,
2023-05-22 05:18:10 +02:00
) ;
2024-03-20 03:42:10 +01:00
expect ( assetMock . update ) . toHaveBeenCalledWith ( {
2023-08-01 03:28:07 +02:00
id : assetStub.image.id ,
2023-07-10 19:56:45 +02:00
originalPath : 'upload/library/label-1/2023/2023-02-23/asset-id.jpg' ,
2023-05-22 05:18:10 +02:00
} ) ;
} ) ;
2023-12-29 19:41:33 +01:00
it ( 'should copy the file if rename fails due to EXDEV (rename across filesystems)' , async ( ) = > {
const newPath = 'upload/library/user-id/2023/2023-02-23/asset-id.jpg' ;
assetMock . getAll . mockResolvedValue ( {
items : [ assetStub . image ] ,
hasNextPage : false ,
} ) ;
storageMock . rename . mockRejectedValue ( { code : 'EXDEV' } ) ;
userMock . getList . mockResolvedValue ( [ userStub . user1 ] ) ;
moveMock . create . mockResolvedValue ( {
id : '123' ,
entityId : assetStub.image.id ,
pathType : AssetPathType.ORIGINAL ,
oldPath : assetStub.image.originalPath ,
newPath ,
} ) ;
when ( storageMock . stat )
. calledWith ( newPath )
. mockResolvedValue ( {
size : 5000 ,
} as Stats ) ;
2024-02-12 05:40:34 +01:00
when ( storageMock . stat )
. calledWith ( assetStub . image . originalPath )
. mockResolvedValue ( {
atime : new Date ( ) ,
mtime : new Date ( ) ,
} as Stats ) ;
2023-12-29 19:41:33 +01:00
when ( cryptoMock . hashFile ) . calledWith ( newPath ) . mockResolvedValue ( assetStub . image . checksum ) ;
await sut . handleMigration ( ) ;
expect ( assetMock . getAll ) . toHaveBeenCalled ( ) ;
expect ( storageMock . rename ) . toHaveBeenCalledWith ( '/original/path.jpg' , newPath ) ;
expect ( storageMock . copyFile ) . toHaveBeenCalledWith ( '/original/path.jpg' , newPath ) ;
expect ( storageMock . stat ) . toHaveBeenCalledWith ( newPath ) ;
2024-02-12 05:40:34 +01:00
expect ( storageMock . stat ) . toHaveBeenCalledWith ( assetStub . image . originalPath ) ;
expect ( storageMock . utimes ) . toHaveBeenCalledWith ( newPath , expect . any ( Date ) , expect . any ( Date ) ) ;
2023-12-29 19:41:33 +01:00
expect ( storageMock . unlink ) . toHaveBeenCalledWith ( assetStub . image . originalPath ) ;
expect ( storageMock . unlink ) . toHaveBeenCalledTimes ( 1 ) ;
2024-03-20 03:42:10 +01:00
expect ( assetMock . update ) . toHaveBeenCalledWith ( {
2023-12-29 19:41:33 +01:00
id : assetStub.image.id ,
originalPath : newPath ,
} ) ;
} ) ;
it ( 'should not update the database if the move fails due to incorrect newPath filesize' , async ( ) = > {
assetMock . getAll . mockResolvedValue ( {
items : [ assetStub . image ] ,
hasNextPage : false ,
} ) ;
storageMock . rename . mockRejectedValue ( { code : 'EXDEV' } ) ;
userMock . getList . mockResolvedValue ( [ userStub . user1 ] ) ;
moveMock . create . mockResolvedValue ( {
id : '123' ,
entityId : assetStub.image.id ,
pathType : AssetPathType.ORIGINAL ,
oldPath : assetStub.image.originalPath ,
newPath : 'upload/library/user-id/2023/2023-02-23/asset-id.jpg' ,
} ) ;
when ( storageMock . stat )
. calledWith ( 'upload/library/user-id/2023/2023-02-23/asset-id.jpg' )
. mockResolvedValue ( {
size : 100 ,
} as Stats ) ;
await sut . handleMigration ( ) ;
expect ( assetMock . getAll ) . toHaveBeenCalled ( ) ;
expect ( storageMock . rename ) . toHaveBeenCalledWith (
'/original/path.jpg' ,
'upload/library/user-id/2023/2023-02-23/asset-id.jpg' ,
) ;
expect ( storageMock . copyFile ) . toHaveBeenCalledWith (
'/original/path.jpg' ,
'upload/library/user-id/2023/2023-02-23/asset-id.jpg' ,
) ;
expect ( storageMock . stat ) . toHaveBeenCalledWith ( 'upload/library/user-id/2023/2023-02-23/asset-id.jpg' ) ;
2024-03-20 03:42:10 +01:00
expect ( assetMock . update ) . not . toHaveBeenCalled ( ) ;
2023-12-29 19:41:33 +01:00
} ) ;
2023-02-25 15:12:03 +01:00
it ( 'should not update the database if the move fails' , async ( ) = > {
2023-05-22 20:05:06 +02:00
assetMock . getAll . mockResolvedValue ( {
2023-08-01 03:28:07 +02:00
items : [ assetStub . image ] ,
2023-05-22 20:05:06 +02:00
hasNextPage : false ,
} ) ;
2023-12-29 19:41:33 +01:00
storageMock . rename . mockRejectedValue ( new Error ( 'Read only system' ) ) ;
storageMock . copyFile . mockRejectedValue ( new Error ( 'Read only system' ) ) ;
2023-10-11 04:14:44 +02:00
moveMock . create . mockResolvedValue ( {
id : 'move-123' ,
entityId : '123' ,
pathType : AssetPathType.ORIGINAL ,
oldPath : assetStub.image.originalPath ,
newPath : '' ,
} ) ;
2023-08-01 03:28:07 +02:00
userMock . getList . mockResolvedValue ( [ userStub . user1 ] ) ;
2023-02-25 15:12:03 +01:00
2023-05-26 14:52:52 +02:00
await sut . handleMigration ( ) ;
2023-02-25 15:12:03 +01:00
expect ( assetMock . getAll ) . toHaveBeenCalled ( ) ;
2023-12-29 19:41:33 +01:00
expect ( storageMock . rename ) . toHaveBeenCalledWith (
2023-07-10 19:56:45 +02:00
'/original/path.jpg' ,
'upload/library/user-id/2023/2023-02-23/asset-id.jpg' ,
2023-02-25 15:12:03 +01:00
) ;
2024-03-20 03:42:10 +01:00
expect ( assetMock . update ) . not . toHaveBeenCalled ( ) ;
2023-02-25 15:12:03 +01:00
} ) ;
feat(server): support for read-only assets and importing existing items in the filesystem (#2715)
* Added read-only flag for assets, endpoint to trigger file import vs upload
* updated fixtures with new property
* if upload is 'read-only', ensure there is no existing asset at the designated originalPath
* added test for file import as well as detecting existing image at read-only destination location
* Added storage service test for a case where it should not move read-only assets
* upload doesn't need the read-only flag available, just importing
* default isReadOnly on import endpoint to true
* formatting fixes
* create-asset dto needs isReadOnly, so set it to false by default on create, updated api generation
* updated code to reflect changes in MR
* fixed read stream promise return type
* new index for originalPath, check for existing path on import, reglardless of user, to prevent duplicates
* refactor: import asset
* chore: open api
* chore: tests
* Added externalPath support for individual users, updated UI to allow this to be set by admin
* added missing var for externalPath in ui
* chore: open api
* fix: compilation issues
* fix: server test
* built api, fixed user-response dto to include externalPath
* reverted accidental commit
* bad commit of duplicate externalPath in user response dto
* fixed tests to include externalPath on expected result
* fix: unit tests
* centralized supported filetypes, perform file type checking of asset and sidecar during file import process
* centralized supported filetype check method to keep regex DRY
* fixed typo
* combined migrations into one
* update api
* Removed externalPath from shared-link code, added column to admin user page whether external paths / import is enabled or not
* update mimetype
* Fixed detect correct mimetype
* revert asset-upload config
* reverted domain.constant
* refactor
* fix mime-type issue
* fix format
---------
Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-06-22 04:33:20 +02:00
it ( 'should not move read-only asset' , async ( ) = > {
assetMock . getAll . mockResolvedValue ( {
items : [
{
2023-08-01 03:28:07 +02:00
. . . assetStub . image ,
2023-07-10 19:56:45 +02:00
originalPath : 'upload/library/user-id/2023/2023-02-23/asset-id+1.jpg' ,
feat(server): support for read-only assets and importing existing items in the filesystem (#2715)
* Added read-only flag for assets, endpoint to trigger file import vs upload
* updated fixtures with new property
* if upload is 'read-only', ensure there is no existing asset at the designated originalPath
* added test for file import as well as detecting existing image at read-only destination location
* Added storage service test for a case where it should not move read-only assets
* upload doesn't need the read-only flag available, just importing
* default isReadOnly on import endpoint to true
* formatting fixes
* create-asset dto needs isReadOnly, so set it to false by default on create, updated api generation
* updated code to reflect changes in MR
* fixed read stream promise return type
* new index for originalPath, check for existing path on import, reglardless of user, to prevent duplicates
* refactor: import asset
* chore: open api
* chore: tests
* Added externalPath support for individual users, updated UI to allow this to be set by admin
* added missing var for externalPath in ui
* chore: open api
* fix: compilation issues
* fix: server test
* built api, fixed user-response dto to include externalPath
* reverted accidental commit
* bad commit of duplicate externalPath in user response dto
* fixed tests to include externalPath on expected result
* fix: unit tests
* centralized supported filetypes, perform file type checking of asset and sidecar during file import process
* centralized supported filetype check method to keep regex DRY
* fixed typo
* combined migrations into one
* update api
* Removed externalPath from shared-link code, added column to admin user page whether external paths / import is enabled or not
* update mimetype
* Fixed detect correct mimetype
* revert asset-upload config
* reverted domain.constant
* refactor
* fix mime-type issue
* fix format
---------
Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-06-22 04:33:20 +02:00
isReadOnly : true ,
} ,
] ,
hasNextPage : false ,
} ) ;
2023-08-01 03:28:07 +02:00
userMock . getList . mockResolvedValue ( [ userStub . user1 ] ) ;
feat(server): support for read-only assets and importing existing items in the filesystem (#2715)
* Added read-only flag for assets, endpoint to trigger file import vs upload
* updated fixtures with new property
* if upload is 'read-only', ensure there is no existing asset at the designated originalPath
* added test for file import as well as detecting existing image at read-only destination location
* Added storage service test for a case where it should not move read-only assets
* upload doesn't need the read-only flag available, just importing
* default isReadOnly on import endpoint to true
* formatting fixes
* create-asset dto needs isReadOnly, so set it to false by default on create, updated api generation
* updated code to reflect changes in MR
* fixed read stream promise return type
* new index for originalPath, check for existing path on import, reglardless of user, to prevent duplicates
* refactor: import asset
* chore: open api
* chore: tests
* Added externalPath support for individual users, updated UI to allow this to be set by admin
* added missing var for externalPath in ui
* chore: open api
* fix: compilation issues
* fix: server test
* built api, fixed user-response dto to include externalPath
* reverted accidental commit
* bad commit of duplicate externalPath in user response dto
* fixed tests to include externalPath on expected result
* fix: unit tests
* centralized supported filetypes, perform file type checking of asset and sidecar during file import process
* centralized supported filetype check method to keep regex DRY
* fixed typo
* combined migrations into one
* update api
* Removed externalPath from shared-link code, added column to admin user page whether external paths / import is enabled or not
* update mimetype
* Fixed detect correct mimetype
* revert asset-upload config
* reverted domain.constant
* refactor
* fix mime-type issue
* fix format
---------
Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-06-22 04:33:20 +02:00
await sut . handleMigration ( ) ;
expect ( assetMock . getAll ) . toHaveBeenCalled ( ) ;
2023-12-29 19:41:33 +01:00
expect ( storageMock . rename ) . not . toHaveBeenCalled ( ) ;
expect ( storageMock . copyFile ) . not . toHaveBeenCalled ( ) ;
2024-03-20 03:42:10 +01:00
expect ( assetMock . update ) . not . toHaveBeenCalled ( ) ;
feat(server): support for read-only assets and importing existing items in the filesystem (#2715)
* Added read-only flag for assets, endpoint to trigger file import vs upload
* updated fixtures with new property
* if upload is 'read-only', ensure there is no existing asset at the designated originalPath
* added test for file import as well as detecting existing image at read-only destination location
* Added storage service test for a case where it should not move read-only assets
* upload doesn't need the read-only flag available, just importing
* default isReadOnly on import endpoint to true
* formatting fixes
* create-asset dto needs isReadOnly, so set it to false by default on create, updated api generation
* updated code to reflect changes in MR
* fixed read stream promise return type
* new index for originalPath, check for existing path on import, reglardless of user, to prevent duplicates
* refactor: import asset
* chore: open api
* chore: tests
* Added externalPath support for individual users, updated UI to allow this to be set by admin
* added missing var for externalPath in ui
* chore: open api
* fix: compilation issues
* fix: server test
* built api, fixed user-response dto to include externalPath
* reverted accidental commit
* bad commit of duplicate externalPath in user response dto
* fixed tests to include externalPath on expected result
* fix: unit tests
* centralized supported filetypes, perform file type checking of asset and sidecar during file import process
* centralized supported filetype check method to keep regex DRY
* fixed typo
* combined migrations into one
* update api
* Removed externalPath from shared-link code, added column to admin user page whether external paths / import is enabled or not
* update mimetype
* Fixed detect correct mimetype
* revert asset-upload config
* reverted domain.constant
* refactor
* fix mime-type issue
* fix format
---------
Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-06-22 04:33:20 +02:00
} ) ;
2023-02-25 15:12:03 +01:00
} ) ;
} ) ;