2025-02-18 00:26:44 +01:00
import { LibraryResponseDto , LoginResponseDto , getAllLibraries } from '@immich/sdk' ;
2024-12-22 03:50:07 +01:00
import { cpSync , existsSync , rmSync , unlinkSync } from 'node:fs' ;
2024-03-15 09:16:08 -04:00
import { Socket } from 'socket.io-client' ;
2024-02-29 15:10:08 -05:00
import { userDto , uuidDto } from 'src/fixtures' ;
import { errorDto } from 'src/responses' ;
2024-03-15 09:16:08 -04:00
import { app , asBearerAuth , testAssetDir , testAssetDirInternal , utils } from 'src/utils' ;
2024-02-29 15:10:08 -05:00
import request from 'supertest' ;
2024-04-12 15:15:41 -04:00
import { utimes } from 'utimes' ;
2024-03-15 09:16:08 -04:00
import { afterAll , beforeAll , beforeEach , describe , expect , it } from 'vitest' ;
2024-05-22 13:24:57 -04:00
describe ( '/libraries' , ( ) = > {
2024-02-29 15:10:08 -05:00
let admin : LoginResponseDto ;
let user : LoginResponseDto ;
let library : LibraryResponseDto ;
2024-03-15 09:16:08 -04:00
let websocket : Socket ;
2024-02-29 15:10:08 -05:00
beforeAll ( async ( ) = > {
2024-03-07 10:14:36 -05:00
await utils . resetDatabase ( ) ;
admin = await utils . adminSetup ( ) ;
2024-04-15 23:05:08 -04:00
await utils . resetAdminConfig ( admin . accessToken ) ;
2024-03-07 10:14:36 -05:00
user = await utils . userSetup ( admin . accessToken , userDto . user1 ) ;
2024-05-20 18:09:10 -04:00
library = await utils . createLibrary ( admin . accessToken , { ownerId : admin.userId } ) ;
2024-03-15 09:16:08 -04:00
websocket = await utils . connectWebsocket ( admin . accessToken ) ;
2024-04-12 15:15:41 -04:00
utils . createImageFile ( ` ${ testAssetDir } /temp/directoryA/assetA.png ` ) ;
utils . createImageFile ( ` ${ testAssetDir } /temp/directoryB/assetB.png ` ) ;
2024-03-15 09:16:08 -04:00
} ) ;
afterAll ( ( ) = > {
utils . disconnectWebsocket ( websocket ) ;
2024-04-15 23:05:08 -04:00
utils . resetTempFolder ( ) ;
2024-03-15 09:16:08 -04:00
} ) ;
beforeEach ( ( ) = > {
utils . resetEvents ( ) ;
2024-02-29 15:10:08 -05:00
} ) ;
2024-05-22 13:24:57 -04:00
describe ( 'GET /libraries' , ( ) = > {
2024-02-29 15:10:08 -05:00
it ( 'should require authentication' , async ( ) = > {
2024-05-22 13:24:57 -04:00
const { status , body } = await request ( app ) . get ( '/libraries' ) ;
2024-02-29 15:10:08 -05:00
expect ( status ) . toBe ( 401 ) ;
expect ( body ) . toEqual ( errorDto . unauthorized ) ;
} ) ;
} ) ;
2024-05-22 13:24:57 -04:00
describe ( 'POST /libraries' , ( ) = > {
2024-02-29 15:10:08 -05:00
it ( 'should require authentication' , async ( ) = > {
2024-05-22 13:24:57 -04:00
const { status , body } = await request ( app ) . post ( '/libraries' ) . send ( { } ) ;
2024-02-29 15:10:08 -05:00
expect ( status ) . toBe ( 401 ) ;
expect ( body ) . toEqual ( errorDto . unauthorized ) ;
} ) ;
it ( 'should require admin authentication' , async ( ) = > {
const { status , body } = await request ( app )
2024-05-22 13:24:57 -04:00
. post ( '/libraries' )
2024-02-29 15:10:08 -05:00
. set ( 'Authorization' , ` Bearer ${ user . accessToken } ` )
2024-05-20 18:09:10 -04:00
. send ( { ownerId : admin.userId } ) ;
2024-02-29 15:10:08 -05:00
expect ( status ) . toBe ( 403 ) ;
expect ( body ) . toEqual ( errorDto . forbidden ) ;
} ) ;
it ( 'should create an external library with defaults' , async ( ) = > {
const { status , body } = await request ( app )
2024-05-22 13:24:57 -04:00
. post ( '/libraries' )
2024-02-29 15:10:08 -05:00
. set ( 'Authorization' , ` Bearer ${ admin . accessToken } ` )
2024-05-20 18:09:10 -04:00
. send ( { ownerId : admin.userId } ) ;
2024-02-29 15:10:08 -05:00
expect ( status ) . toBe ( 201 ) ;
expect ( body ) . toEqual (
expect . objectContaining ( {
ownerId : admin.userId ,
name : 'New External Library' ,
refreshedAt : null ,
assetCount : 0 ,
importPaths : [ ] ,
2024-09-11 16:40:52 +02:00
exclusionPatterns : expect.any ( Array ) ,
2024-02-29 15:10:08 -05:00
} ) ,
) ;
} ) ;
it ( 'should create an external library with options' , async ( ) = > {
const { status , body } = await request ( app )
2024-05-22 13:24:57 -04:00
. post ( '/libraries' )
2024-02-29 15:10:08 -05:00
. set ( 'Authorization' , ` Bearer ${ admin . accessToken } ` )
. send ( {
2024-03-18 15:59:53 -05:00
ownerId : admin.userId ,
2024-02-29 15:10:08 -05:00
name : 'My Awesome Library' ,
importPaths : [ '/path/to/import' ] ,
exclusionPatterns : [ '**/Raw/**' ] ,
} ) ;
expect ( status ) . toBe ( 201 ) ;
expect ( body ) . toEqual (
expect . objectContaining ( {
name : 'My Awesome Library' ,
importPaths : [ '/path/to/import' ] ,
} ) ,
) ;
} ) ;
it ( 'should not create an external library with duplicate import paths' , async ( ) = > {
const { status , body } = await request ( app )
2024-05-22 13:24:57 -04:00
. post ( '/libraries' )
2024-02-29 15:10:08 -05:00
. set ( 'Authorization' , ` Bearer ${ admin . accessToken } ` )
. send ( {
2024-03-18 15:59:53 -05:00
ownerId : admin.userId ,
2024-02-29 15:10:08 -05:00
name : 'My Awesome Library' ,
importPaths : [ '/path' , '/path' ] ,
exclusionPatterns : [ '**/Raw/**' ] ,
} ) ;
expect ( status ) . toBe ( 400 ) ;
expect ( body ) . toEqual ( errorDto . badRequest ( [ "All importPaths's elements must be unique" ] ) ) ;
} ) ;
it ( 'should not create an external library with duplicate exclusion patterns' , async ( ) = > {
const { status , body } = await request ( app )
2024-05-22 13:24:57 -04:00
. post ( '/libraries' )
2024-02-29 15:10:08 -05:00
. set ( 'Authorization' , ` Bearer ${ admin . accessToken } ` )
. send ( {
2024-03-18 15:59:53 -05:00
ownerId : admin.userId ,
2024-02-29 15:10:08 -05:00
name : 'My Awesome Library' ,
importPaths : [ '/path/to/import' ] ,
exclusionPatterns : [ '**/Raw/**' , '**/Raw/**' ] ,
} ) ;
expect ( status ) . toBe ( 400 ) ;
expect ( body ) . toEqual ( errorDto . badRequest ( [ "All exclusionPatterns's elements must be unique" ] ) ) ;
} ) ;
} ) ;
2024-05-22 13:24:57 -04:00
describe ( 'PUT /libraries/:id' , ( ) = > {
2024-02-29 15:10:08 -05:00
it ( 'should require authentication' , async ( ) = > {
2024-05-22 13:24:57 -04:00
const { status , body } = await request ( app ) . put ( ` /libraries/ ${ uuidDto . notFound } ` ) . send ( { } ) ;
2024-02-29 15:10:08 -05:00
expect ( status ) . toBe ( 401 ) ;
expect ( body ) . toEqual ( errorDto . unauthorized ) ;
} ) ;
it ( 'should change the library name' , async ( ) = > {
const { status , body } = await request ( app )
2024-05-22 13:24:57 -04:00
. put ( ` /libraries/ ${ library . id } ` )
2024-02-29 15:10:08 -05:00
. set ( 'Authorization' , ` Bearer ${ admin . accessToken } ` )
. send ( { name : 'New Library Name' } ) ;
expect ( status ) . toBe ( 200 ) ;
expect ( body ) . toEqual (
expect . objectContaining ( {
name : 'New Library Name' ,
} ) ,
) ;
} ) ;
it ( 'should not set an empty name' , async ( ) = > {
const { status , body } = await request ( app )
2024-05-22 13:24:57 -04:00
. put ( ` /libraries/ ${ library . id } ` )
2024-02-29 15:10:08 -05:00
. set ( 'Authorization' , ` Bearer ${ admin . accessToken } ` )
. send ( { name : '' } ) ;
expect ( status ) . toBe ( 400 ) ;
expect ( body ) . toEqual ( errorDto . badRequest ( [ 'name should not be empty' ] ) ) ;
} ) ;
it ( 'should change the import paths' , async ( ) = > {
const { status , body } = await request ( app )
2024-05-22 13:24:57 -04:00
. put ( ` /libraries/ ${ library . id } ` )
2024-02-29 15:10:08 -05:00
. set ( 'Authorization' , ` Bearer ${ admin . accessToken } ` )
. send ( { importPaths : [ testAssetDirInternal ] } ) ;
expect ( status ) . toBe ( 200 ) ;
expect ( body ) . toEqual (
expect . objectContaining ( {
importPaths : [ testAssetDirInternal ] ,
} ) ,
) ;
} ) ;
it ( 'should reject an empty import path' , async ( ) = > {
const { status , body } = await request ( app )
2024-05-22 13:24:57 -04:00
. put ( ` /libraries/ ${ library . id } ` )
2024-02-29 15:10:08 -05:00
. set ( 'Authorization' , ` Bearer ${ admin . accessToken } ` )
. send ( { importPaths : [ '' ] } ) ;
expect ( status ) . toBe ( 400 ) ;
expect ( body ) . toEqual ( errorDto . badRequest ( [ 'each value in importPaths should not be empty' ] ) ) ;
} ) ;
it ( 'should reject duplicate import paths' , async ( ) = > {
const { status , body } = await request ( app )
2024-05-22 13:24:57 -04:00
. put ( ` /libraries/ ${ library . id } ` )
2024-02-29 15:10:08 -05:00
. set ( 'Authorization' , ` Bearer ${ admin . accessToken } ` )
. send ( { importPaths : [ '/path' , '/path' ] } ) ;
expect ( status ) . toBe ( 400 ) ;
expect ( body ) . toEqual ( errorDto . badRequest ( [ "All importPaths's elements must be unique" ] ) ) ;
} ) ;
it ( 'should change the exclusion pattern' , async ( ) = > {
const { status , body } = await request ( app )
2024-05-22 13:24:57 -04:00
. put ( ` /libraries/ ${ library . id } ` )
2024-02-29 15:10:08 -05:00
. set ( 'Authorization' , ` Bearer ${ admin . accessToken } ` )
. send ( { exclusionPatterns : [ '**/Raw/**' ] } ) ;
expect ( status ) . toBe ( 200 ) ;
expect ( body ) . toEqual (
expect . objectContaining ( {
exclusionPatterns : [ '**/Raw/**' ] ,
} ) ,
) ;
} ) ;
it ( 'should reject duplicate exclusion patterns' , async ( ) = > {
const { status , body } = await request ( app )
2024-05-22 13:24:57 -04:00
. put ( ` /libraries/ ${ library . id } ` )
2024-02-29 15:10:08 -05:00
. set ( 'Authorization' , ` Bearer ${ admin . accessToken } ` )
. send ( { exclusionPatterns : [ '**/*.jpg' , '**/*.jpg' ] } ) ;
expect ( status ) . toBe ( 400 ) ;
expect ( body ) . toEqual ( errorDto . badRequest ( [ "All exclusionPatterns's elements must be unique" ] ) ) ;
} ) ;
it ( 'should reject an empty exclusion pattern' , async ( ) = > {
const { status , body } = await request ( app )
2024-05-22 13:24:57 -04:00
. put ( ` /libraries/ ${ library . id } ` )
2024-02-29 15:10:08 -05:00
. set ( 'Authorization' , ` Bearer ${ admin . accessToken } ` )
. send ( { exclusionPatterns : [ '' ] } ) ;
expect ( status ) . toBe ( 400 ) ;
expect ( body ) . toEqual ( errorDto . badRequest ( [ 'each value in exclusionPatterns should not be empty' ] ) ) ;
} ) ;
} ) ;
2024-05-22 13:24:57 -04:00
describe ( 'GET /libraries/:id' , ( ) = > {
2024-02-29 15:10:08 -05:00
it ( 'should require authentication' , async ( ) = > {
2024-05-22 13:24:57 -04:00
const { status , body } = await request ( app ) . get ( ` /libraries/ ${ uuidDto . notFound } ` ) ;
2024-02-29 15:10:08 -05:00
expect ( status ) . toBe ( 401 ) ;
expect ( body ) . toEqual ( errorDto . unauthorized ) ;
} ) ;
it ( 'should require admin access' , async ( ) = > {
const { status , body } = await request ( app )
2024-05-22 13:24:57 -04:00
. get ( ` /libraries/ ${ uuidDto . notFound } ` )
2024-02-29 15:10:08 -05:00
. set ( 'Authorization' , ` Bearer ${ user . accessToken } ` ) ;
expect ( status ) . toBe ( 403 ) ;
expect ( body ) . toEqual ( errorDto . forbidden ) ;
} ) ;
it ( 'should get library by id' , async ( ) = > {
2024-05-20 18:09:10 -04:00
const library = await utils . createLibrary ( admin . accessToken , { ownerId : admin.userId } ) ;
2024-02-29 15:10:08 -05:00
const { status , body } = await request ( app )
2024-05-22 13:24:57 -04:00
. get ( ` /libraries/ ${ library . id } ` )
2024-02-29 15:10:08 -05:00
. set ( 'Authorization' , ` Bearer ${ admin . accessToken } ` ) ;
expect ( status ) . toBe ( 200 ) ;
expect ( body ) . toEqual (
expect . objectContaining ( {
ownerId : admin.userId ,
name : 'New External Library' ,
refreshedAt : null ,
assetCount : 0 ,
importPaths : [ ] ,
2024-09-11 16:40:52 +02:00
exclusionPatterns : expect.any ( Array ) ,
2024-02-29 15:10:08 -05:00
} ) ,
) ;
} ) ;
} ) ;
2024-05-22 13:24:57 -04:00
describe ( 'GET /libraries/:id/statistics' , ( ) = > {
2024-02-29 15:10:08 -05:00
it ( 'should require authentication' , async ( ) = > {
2024-05-22 13:24:57 -04:00
const { status , body } = await request ( app ) . get ( ` /libraries/ ${ uuidDto . notFound } /statistics ` ) ;
2024-02-29 15:10:08 -05:00
expect ( status ) . toBe ( 401 ) ;
expect ( body ) . toEqual ( errorDto . unauthorized ) ;
} ) ;
} ) ;
2024-05-22 13:24:57 -04:00
describe ( 'POST /libraries/:id/scan' , ( ) = > {
2024-02-29 15:10:08 -05:00
it ( 'should require authentication' , async ( ) = > {
2024-05-22 13:24:57 -04:00
const { status , body } = await request ( app ) . post ( ` /libraries/ ${ uuidDto . notFound } /scan ` ) . send ( { } ) ;
2024-02-29 15:10:08 -05:00
expect ( status ) . toBe ( 401 ) ;
expect ( body ) . toEqual ( errorDto . unauthorized ) ;
} ) ;
2024-03-15 09:16:08 -04:00
2024-09-25 19:26:19 +02:00
it ( 'should import new asset when scanning external library' , async ( ) = > {
2024-03-15 09:16:08 -04:00
const library = await utils . createLibrary ( admin . accessToken , {
2024-03-18 15:59:53 -05:00
ownerId : admin.userId ,
2024-03-15 09:16:08 -04:00
importPaths : [ ` ${ testAssetDirInternal } /temp/directoryA ` ] ,
} ) ;
2024-09-25 19:26:19 +02:00
const { status } = await request ( app )
. post ( ` /libraries/ ${ library . id } /scan ` )
. set ( 'Authorization' , ` Bearer ${ admin . accessToken } ` )
. send ( ) ;
expect ( status ) . toBe ( 204 ) ;
await utils . waitForQueueFinish ( admin . accessToken , 'library' ) ;
2025-02-18 00:26:44 +01:00
await utils . waitForQueueFinish ( admin . accessToken , 'sidecar' ) ;
2025-02-13 22:30:12 +01:00
await utils . waitForQueueFinish ( admin . accessToken , 'metadataExtraction' ) ;
2024-03-15 09:16:08 -04:00
2024-11-20 14:47:25 -05:00
const { assets } = await utils . searchAssets ( admin . accessToken , {
2024-03-15 09:16:08 -04:00
originalPath : ` ${ testAssetDirInternal } /temp/directoryA/assetA.png ` ,
2025-01-09 00:12:39 +01:00
libraryId : library.id ,
2024-03-15 09:16:08 -04:00
} ) ;
expect ( assets . count ) . toBe ( 1 ) ;
} ) ;
2025-01-09 00:12:39 +01:00
it ( 'should process metadata and thumbnails for external asset' , async ( ) = > {
const library = await utils . createLibrary ( admin . accessToken , {
ownerId : admin.userId ,
importPaths : [ ` ${ testAssetDirInternal } /temp/directoryA ` ] ,
} ) ;
2025-02-18 01:04:38 +01:00
await utils . scan ( admin . accessToken , library . id ) ;
2025-01-09 00:12:39 +01:00
const { assets } = await utils . searchAssets ( admin . accessToken , {
originalPath : ` ${ testAssetDirInternal } /temp/directoryA/assetA.png ` ,
libraryId : library.id ,
} ) ;
expect ( assets . count ) . toBe ( 1 ) ;
const asset = assets . items [ 0 ] ;
expect ( asset . exifInfo ) . not . toBe ( null ) ;
expect ( asset . exifInfo ? . dateTimeOriginal ) . not . toBe ( null ) ;
expect ( asset . thumbhash ) . not . toBe ( null ) ;
} ) ;
2024-03-15 09:16:08 -04:00
it ( 'should scan external library with exclusion pattern' , async ( ) = > {
const library = await utils . createLibrary ( admin . accessToken , {
2024-03-18 15:59:53 -05:00
ownerId : admin.userId ,
2024-03-15 09:16:08 -04:00
importPaths : [ ` ${ testAssetDirInternal } /temp ` ] ,
exclusionPatterns : [ '**/directoryA' ] ,
} ) ;
2025-02-18 01:04:38 +01:00
await utils . scan ( admin . accessToken , library . id ) ;
2024-03-15 09:16:08 -04:00
2024-11-20 14:47:25 -05:00
const { assets } = await utils . searchAssets ( admin . accessToken , { libraryId : library.id } ) ;
2024-03-15 09:16:08 -04:00
expect ( assets . count ) . toBe ( 1 ) ;
expect ( assets . items [ 0 ] . originalPath . includes ( 'directoryB' ) ) ;
} ) ;
it ( 'should scan multiple import paths' , async ( ) = > {
const library = await utils . createLibrary ( admin . accessToken , {
2024-03-18 15:59:53 -05:00
ownerId : admin.userId ,
2024-03-15 09:16:08 -04:00
importPaths : [ ` ${ testAssetDirInternal } /temp/directoryA ` , ` ${ testAssetDirInternal } /temp/directoryB ` ] ,
} ) ;
2025-02-18 01:04:38 +01:00
await utils . scan ( admin . accessToken , library . id ) ;
2024-03-15 09:16:08 -04:00
2024-11-20 14:47:25 -05:00
const { assets } = await utils . searchAssets ( admin . accessToken , { libraryId : library.id } ) ;
2024-03-15 09:16:08 -04:00
expect ( assets . count ) . toBe ( 2 ) ;
expect ( assets . items . find ( ( asset ) = > asset . originalPath . includes ( 'directoryA' ) ) ) . toBeDefined ( ) ;
expect ( assets . items . find ( ( asset ) = > asset . originalPath . includes ( 'directoryB' ) ) ) . toBeDefined ( ) ;
} ) ;
2024-10-07 21:43:21 +02:00
it ( 'should scan multiple import paths with commas' , async ( ) = > {
// https://github.com/immich-app/immich/issues/10699
const library = await utils . createLibrary ( admin . accessToken , {
ownerId : admin.userId ,
importPaths : [ ` ${ testAssetDirInternal } /temp/folder, a ` , ` ${ testAssetDirInternal } /temp/folder, b ` ] ,
} ) ;
utils . createImageFile ( ` ${ testAssetDir } /temp/folder, a/assetA.png ` ) ;
utils . createImageFile ( ` ${ testAssetDir } /temp/folder, b/assetB.png ` ) ;
2025-02-18 01:04:38 +01:00
await utils . scan ( admin . accessToken , library . id ) ;
2024-10-07 21:43:21 +02:00
2024-11-20 14:47:25 -05:00
const { assets } = await utils . searchAssets ( admin . accessToken , { libraryId : library.id } ) ;
2024-10-07 21:43:21 +02:00
expect ( assets . count ) . toBe ( 2 ) ;
expect ( assets . items . find ( ( asset ) = > asset . originalPath . includes ( 'folder, a' ) ) ) . toBeDefined ( ) ;
expect ( assets . items . find ( ( asset ) = > asset . originalPath . includes ( 'folder, b' ) ) ) . toBeDefined ( ) ;
utils . removeImageFile ( ` ${ testAssetDir } /temp/folder, a/assetA.png ` ) ;
utils . removeImageFile ( ` ${ testAssetDir } /temp/folder, b/assetB.png ` ) ;
} ) ;
it ( 'should scan multiple import paths with braces' , async ( ) = > {
// https://github.com/immich-app/immich/issues/10699
const library = await utils . createLibrary ( admin . accessToken , {
ownerId : admin.userId ,
importPaths : [ ` ${ testAssetDirInternal } /temp/folder{ a ` , ` ${ testAssetDirInternal } /temp/folder} b ` ] ,
} ) ;
utils . createImageFile ( ` ${ testAssetDir } /temp/folder{ a/assetA.png ` ) ;
utils . createImageFile ( ` ${ testAssetDir } /temp/folder} b/assetB.png ` ) ;
2025-02-18 01:04:38 +01:00
await utils . scan ( admin . accessToken , library . id ) ;
2024-10-07 21:43:21 +02:00
2024-11-20 14:47:25 -05:00
const { assets } = await utils . searchAssets ( admin . accessToken , { libraryId : library.id } ) ;
2024-10-07 21:43:21 +02:00
expect ( assets . count ) . toBe ( 2 ) ;
expect ( assets . items . find ( ( asset ) = > asset . originalPath . includes ( 'folder{ a' ) ) ) . toBeDefined ( ) ;
expect ( assets . items . find ( ( asset ) = > asset . originalPath . includes ( 'folder} b' ) ) ) . toBeDefined ( ) ;
utils . removeImageFile ( ` ${ testAssetDir } /temp/folder{ a/assetA.png ` ) ;
utils . removeImageFile ( ` ${ testAssetDir } /temp/folder} b/assetB.png ` ) ;
} ) ;
2024-12-22 23:22:16 +01:00
const annoyingChars = [
"'" ,
'"' ,
'`' ,
'*' ,
'{' ,
'}' ,
',' ,
'(' ,
')' ,
'[' ,
']' ,
'?' ,
'!' ,
'@' ,
'#' ,
'$' ,
'%' ,
'^' ,
'&' ,
'=' ,
'+' ,
'~' ,
'|' ,
'<' ,
'>' ,
';' ,
':' ,
'/' , // We never got backslashes to work
] ;
it . each ( annoyingChars ) ( 'should scan multiple import paths with %s' , async ( char ) = > {
const library = await utils . createLibrary ( admin . accessToken , {
ownerId : admin.userId ,
importPaths : [ ` ${ testAssetDirInternal } /temp/folder ${ char } 1 ` , ` ${ testAssetDirInternal } /temp/folder ${ char } 2 ` ] ,
} ) ;
utils . createImageFile ( ` ${ testAssetDir } /temp/folder ${ char } 1/asset1.png ` ) ;
utils . createImageFile ( ` ${ testAssetDir } /temp/folder ${ char } 2/asset2.png ` ) ;
2025-02-18 01:04:38 +01:00
await utils . scan ( admin . accessToken , library . id ) ;
2024-12-22 23:22:16 +01:00
const { assets } = await utils . searchAssets ( admin . accessToken , { libraryId : library.id } ) ;
expect ( assets . items ) . toEqual (
expect . arrayContaining ( [
expect . objectContaining ( { originalPath : expect.stringContaining ( ` folder ${ char } 1/asset1.png ` ) } ) ,
expect . objectContaining ( { originalPath : expect.stringContaining ( ` folder ${ char } 2/asset2.png ` ) } ) ,
] ) ,
) ;
utils . removeImageFile ( ` ${ testAssetDir } /temp/folder ${ char } 1/asset1.png ` ) ;
utils . removeImageFile ( ` ${ testAssetDir } /temp/folder ${ char } 2/asset2.png ` ) ;
} ) ;
2024-09-25 19:26:19 +02:00
it ( 'should reimport a modified file' , async ( ) = > {
2024-03-15 09:16:08 -04:00
const library = await utils . createLibrary ( admin . accessToken , {
2024-03-18 15:59:53 -05:00
ownerId : admin.userId ,
2024-12-22 03:50:07 +01:00
importPaths : [ ` ${ testAssetDirInternal } /temp/reimport ` ] ,
2024-03-15 09:16:08 -04:00
} ) ;
2024-12-22 03:50:07 +01:00
utils . createImageFile ( ` ${ testAssetDir } /temp/reimport/asset.jpg ` ) ;
await utimes ( ` ${ testAssetDir } /temp/reimport/asset.jpg ` , 447 _775_200_000 ) ;
2024-04-12 15:15:41 -04:00
2025-02-18 00:26:44 +01:00
await utils . scan ( admin . accessToken , library . id ) ;
2024-04-12 15:15:41 -04:00
2024-12-22 03:50:07 +01:00
cpSync ( ` ${ testAssetDir } /albums/nature/tanners_ridge.jpg ` , ` ${ testAssetDir } /temp/reimport/asset.jpg ` ) ;
await utimes ( ` ${ testAssetDir } /temp/reimport/asset.jpg ` , 447 _775_200_001 ) ;
2024-08-29 01:51:25 +02:00
2025-02-18 01:04:38 +01:00
await utils . scan ( admin . accessToken , library . id ) ;
2024-08-28 19:05:48 +02:00
2024-11-20 14:47:25 -05:00
const { assets } = await utils . searchAssets ( admin . accessToken , {
2024-09-25 19:26:19 +02:00
libraryId : library.id ,
} ) ;
2024-12-22 03:50:07 +01:00
expect ( assets . count ) . toEqual ( 1 ) ;
const asset = await utils . getAssetInfo ( admin . accessToken , assets . items [ 0 ] . id ) ;
expect ( asset ) . toEqual (
expect . objectContaining ( {
originalFileName : 'asset.jpg' ,
exifInfo : expect.objectContaining ( {
model : 'NIKON D750' ,
} ) ,
} ) ,
) ;
utils . removeImageFile ( ` ${ testAssetDir } /temp/reimport/asset.jpg ` ) ;
2024-08-29 01:51:25 +02:00
} ) ;
2024-08-28 19:05:48 +02:00
2024-09-25 19:26:19 +02:00
it ( 'should not reimport unmodified files' , async ( ) = > {
2024-08-29 01:51:25 +02:00
const library = await utils . createLibrary ( admin . accessToken , {
ownerId : admin.userId ,
2024-12-22 03:50:07 +01:00
importPaths : [ ` ${ testAssetDirInternal } /temp/reimport ` ] ,
2024-08-29 01:51:25 +02:00
} ) ;
2024-12-22 03:50:07 +01:00
utils . createImageFile ( ` ${ testAssetDir } /temp/reimport/asset.jpg ` ) ;
await utimes ( ` ${ testAssetDir } /temp/reimport/asset.jpg ` , 447 _775_200_000 ) ;
2024-06-10 13:04:34 -04:00
2025-02-18 00:26:44 +01:00
await utils . scan ( admin . accessToken , library . id ) ;
2024-06-10 13:04:34 -04:00
2024-12-22 03:50:07 +01:00
cpSync ( ` ${ testAssetDir } /albums/nature/tanners_ridge.jpg ` , ` ${ testAssetDir } /temp/reimport/asset.jpg ` ) ;
await utimes ( ` ${ testAssetDir } /temp/reimport/asset.jpg ` , 447 _775_200_000 ) ;
2024-06-10 13:04:34 -04:00
2025-02-18 01:04:38 +01:00
await utils . scan ( admin . accessToken , library . id ) ;
2024-06-10 13:04:34 -04:00
2024-11-20 14:47:25 -05:00
const { assets } = await utils . searchAssets ( admin . accessToken , {
2024-06-10 13:04:34 -04:00
libraryId : library.id ,
} ) ;
2024-12-22 03:50:07 +01:00
expect ( assets . count ) . toEqual ( 1 ) ;
const asset = await utils . getAssetInfo ( admin . accessToken , assets . items [ 0 ] . id ) ;
expect ( asset ) . toEqual (
expect . objectContaining ( {
originalFileName : 'asset.jpg' ,
exifInfo : expect.not.objectContaining ( {
model : 'NIKON D750' ,
} ) ,
} ) ,
) ;
utils . removeImageFile ( ` ${ testAssetDir } /temp/reimport/asset.jpg ` ) ;
2024-06-10 13:04:34 -04:00
} ) ;
2025-02-27 17:45:16 +01:00
it ( 'should not reimport a modified file more than once' , async ( ) = > {
const library = await utils . createLibrary ( admin . accessToken , {
ownerId : admin.userId ,
importPaths : [ ` ${ testAssetDirInternal } /temp/reimport ` ] ,
} ) ;
utils . createImageFile ( ` ${ testAssetDir } /temp/reimport/asset.jpg ` ) ;
await utimes ( ` ${ testAssetDir } /temp/reimport/asset.jpg ` , 447 _775_200_000 ) ;
await utils . scan ( admin . accessToken , library . id ) ;
cpSync ( ` ${ testAssetDir } /albums/nature/tanners_ridge.jpg ` , ` ${ testAssetDir } /temp/reimport/asset.jpg ` ) ;
await utimes ( ` ${ testAssetDir } /temp/reimport/asset.jpg ` , 447 _775_200_001 ) ;
await utils . scan ( admin . accessToken , library . id ) ;
cpSync ( ` ${ testAssetDir } /albums/nature/el_torcal_rocks.jpg ` , ` ${ testAssetDir } /temp/reimport/asset.jpg ` ) ;
await utimes ( ` ${ testAssetDir } /temp/reimport/asset.jpg ` , 447 _775_200_001 ) ;
await utils . scan ( admin . accessToken , library . id ) ;
const { assets } = await utils . searchAssets ( admin . accessToken , {
libraryId : library.id ,
} ) ;
expect ( assets . count ) . toEqual ( 1 ) ;
const asset = await utils . getAssetInfo ( admin . accessToken , assets . items [ 0 ] . id ) ;
expect ( asset ) . toEqual (
expect . objectContaining ( {
originalFileName : 'asset.jpg' ,
exifInfo : expect.objectContaining ( {
model : 'NIKON D750' ,
} ) ,
} ) ,
) ;
utils . removeImageFile ( ` ${ testAssetDir } /temp/reimport/asset.jpg ` ) ;
} ) ;
2024-09-25 19:26:19 +02:00
it ( 'should set an asset offline if its file is missing' , async ( ) = > {
2024-04-12 15:15:41 -04:00
const library = await utils . createLibrary ( admin . accessToken , {
ownerId : admin.userId ,
2024-09-25 19:26:19 +02:00
importPaths : [ ` ${ testAssetDirInternal } /temp/offline ` ] ,
2024-04-12 15:15:41 -04:00
} ) ;
2024-09-25 19:26:19 +02:00
utils . createImageFile ( ` ${ testAssetDir } /temp/offline/offline.png ` ) ;
2024-04-12 15:15:41 -04:00
2025-02-18 00:26:44 +01:00
await utils . scan ( admin . accessToken , library . id ) ;
2024-04-12 15:15:41 -04:00
2024-11-20 14:47:25 -05:00
const { assets } = await utils . searchAssets ( admin . accessToken , { libraryId : library.id } ) ;
2024-09-25 19:26:19 +02:00
expect ( assets . count ) . toBe ( 1 ) ;
2024-04-12 15:15:41 -04:00
2024-09-25 19:26:19 +02:00
utils . removeImageFile ( ` ${ testAssetDir } /temp/offline/offline.png ` ) ;
2024-04-12 15:15:41 -04:00
2025-02-18 01:04:38 +01:00
await utils . scan ( admin . accessToken , library . id ) ;
2024-02-29 15:10:08 -05:00
2024-09-25 19:26:19 +02:00
const trashedAsset = await utils . getAssetInfo ( admin . accessToken , assets . items [ 0 ] . id ) ;
expect ( trashedAsset . originalPath ) . toBe ( ` ${ testAssetDirInternal } /temp/offline/offline.png ` ) ;
expect ( trashedAsset . isOffline ) . toEqual ( true ) ;
2024-02-29 15:10:08 -05:00
2024-11-20 14:47:25 -05:00
const { assets : newAssets } = await utils . searchAssets ( admin . accessToken , { libraryId : library.id } ) ;
2024-09-25 19:26:19 +02:00
expect ( newAssets . items ) . toEqual ( [ ] ) ;
2024-02-29 15:10:08 -05:00
} ) ;
2024-04-12 15:15:41 -04:00
2025-01-07 19:25:43 +01:00
it ( 'should set an asset offline if its file is not in any import path' , async ( ) = > {
2024-10-11 20:40:29 +02:00
utils . createImageFile ( ` ${ testAssetDir } /temp/offline/offline.png ` ) ;
2024-04-12 15:15:41 -04:00
const library = await utils . createLibrary ( admin . accessToken , {
ownerId : admin.userId ,
2024-09-02 01:06:35 +02:00
importPaths : [ ` ${ testAssetDirInternal } /temp/offline ` ] ,
2024-04-12 15:15:41 -04:00
} ) ;
2025-02-18 00:26:44 +01:00
await utils . scan ( admin . accessToken , library . id ) ;
2024-04-12 15:15:41 -04:00
2024-11-20 14:47:25 -05:00
const { assets } = await utils . searchAssets ( admin . accessToken , { libraryId : library.id } ) ;
2024-09-25 19:26:19 +02:00
expect ( assets . count ) . toBe ( 1 ) ;
2024-04-12 15:15:41 -04:00
2024-09-25 19:26:19 +02:00
utils . createDirectory ( ` ${ testAssetDir } /temp/another-path/ ` ) ;
2024-04-12 15:15:41 -04:00
2025-01-07 19:25:43 +01:00
await utils . updateLibrary ( admin . accessToken , library . id , {
importPaths : [ ` ${ testAssetDirInternal } /temp/another-path/ ` ] ,
} ) ;
2024-04-12 15:15:41 -04:00
2025-02-18 01:04:38 +01:00
await utils . scan ( admin . accessToken , library . id ) ;
2024-04-12 15:15:41 -04:00
2024-09-25 19:26:19 +02:00
const trashedAsset = await utils . getAssetInfo ( admin . accessToken , assets . items [ 0 ] . id ) ;
expect ( trashedAsset . originalPath ) . toBe ( ` ${ testAssetDirInternal } /temp/offline/offline.png ` ) ;
expect ( trashedAsset . isOffline ) . toBe ( true ) ;
2024-04-12 15:15:41 -04:00
2024-11-20 14:47:25 -05:00
const { assets : newAssets } = await utils . searchAssets ( admin . accessToken , { libraryId : library.id } ) ;
2024-09-25 19:26:19 +02:00
expect ( newAssets . items ) . toEqual ( [ ] ) ;
2024-09-02 01:06:35 +02:00
2024-09-25 19:26:19 +02:00
utils . removeImageFile ( ` ${ testAssetDir } /temp/offline/offline.png ` ) ;
utils . removeDirectory ( ` ${ testAssetDir } /temp/another-path/ ` ) ;
2024-09-02 01:06:35 +02:00
} ) ;
2024-09-25 19:26:19 +02:00
it ( 'should set an asset offline if its file is covered by an exclusion pattern' , async ( ) = > {
2024-09-02 01:06:35 +02:00
const library = await utils . createLibrary ( admin . accessToken , {
ownerId : admin.userId ,
2024-09-25 19:26:19 +02:00
importPaths : [ ` ${ testAssetDirInternal } /temp ` ] ,
2024-09-02 01:06:35 +02:00
} ) ;
2025-02-18 00:26:44 +01:00
await utils . scan ( admin . accessToken , library . id ) ;
2024-09-02 01:06:35 +02:00
2024-11-20 14:47:25 -05:00
const { assets } = await utils . searchAssets ( admin . accessToken , {
2024-09-02 01:06:35 +02:00
libraryId : library.id ,
2024-09-25 19:26:19 +02:00
originalFileName : 'assetB.png' ,
2024-09-02 01:06:35 +02:00
} ) ;
2024-09-25 19:26:19 +02:00
expect ( assets . count ) . toBe ( 1 ) ;
2024-09-02 01:06:35 +02:00
2025-01-07 19:25:43 +01:00
await utils . updateLibrary ( admin . accessToken , library . id , { exclusionPatterns : [ '**/directoryB/**' ] } ) ;
2024-09-02 01:06:35 +02:00
2025-02-18 00:26:44 +01:00
await utils . scan ( admin . accessToken , library . id ) ;
2024-09-02 01:06:35 +02:00
2024-09-25 19:26:19 +02:00
const trashedAsset = await utils . getAssetInfo ( admin . accessToken , assets . items [ 0 ] . id ) ;
expect ( trashedAsset . isTrashed ) . toBe ( true ) ;
expect ( trashedAsset . originalPath ) . toBe ( ` ${ testAssetDirInternal } /temp/directoryB/assetB.png ` ) ;
expect ( trashedAsset . isOffline ) . toBe ( true ) ;
2024-09-02 01:06:35 +02:00
2024-11-20 14:47:25 -05:00
const { assets : newAssets } = await utils . searchAssets ( admin . accessToken , { libraryId : library.id } ) ;
2024-09-02 01:06:35 +02:00
2024-09-25 19:26:19 +02:00
expect ( newAssets . items ) . toEqual ( [
expect . objectContaining ( {
originalFileName : 'assetA.png' ,
} ) ,
] ) ;
2024-04-12 15:15:41 -04:00
} ) ;
2025-01-07 19:25:43 +01:00
it ( 'should not set an asset offline if its file exists, is in an import path, and not covered by an exclusion pattern' , async ( ) = > {
2024-04-12 15:15:41 -04:00
const library = await utils . createLibrary ( admin . accessToken , {
ownerId : admin.userId ,
importPaths : [ ` ${ testAssetDirInternal } /temp ` ] ,
} ) ;
2025-02-18 00:26:44 +01:00
await utils . scan ( admin . accessToken , library . id ) ;
2024-04-12 15:15:41 -04:00
2024-11-20 14:47:25 -05:00
const { assets : assetsBefore } = await utils . searchAssets ( admin . accessToken , { libraryId : library.id } ) ;
2024-04-12 15:15:41 -04:00
expect ( assetsBefore . count ) . toBeGreaterThan ( 1 ) ;
2025-02-18 01:04:38 +01:00
await utils . scan ( admin . accessToken , library . id ) ;
2024-04-12 15:15:41 -04:00
2024-11-20 14:47:25 -05:00
const { assets } = await utils . searchAssets ( admin . accessToken , { libraryId : library.id } ) ;
2024-04-12 15:15:41 -04:00
expect ( assets ) . toEqual ( assetsBefore ) ;
} ) ;
2024-12-22 03:50:07 +01:00
describe ( 'xmp metadata' , async ( ) = > {
it ( 'should import metadata from file.xmp' , async ( ) = > {
const library = await utils . createLibrary ( admin . accessToken , {
ownerId : admin.userId ,
importPaths : [ ` ${ testAssetDirInternal } /temp/xmp ` ] ,
} ) ;
cpSync ( ` ${ testAssetDir } /metadata/xmp/dates/2000.xmp ` , ` ${ testAssetDir } /temp/xmp/glarus.xmp ` ) ;
cpSync ( ` ${ testAssetDir } /formats/raw/Nikon/D80/glarus.nef ` , ` ${ testAssetDir } /temp/xmp/glarus.nef ` ) ;
2025-02-18 00:26:44 +01:00
await utils . scan ( admin . accessToken , library . id ) ;
2024-12-22 03:50:07 +01:00
const { assets : newAssets } = await utils . searchAssets ( admin . accessToken , { libraryId : library.id } ) ;
expect ( newAssets . items ) . toEqual ( [
expect . objectContaining ( {
originalFileName : 'glarus.nef' ,
fileCreatedAt : '2000-09-27T12:35:33.000Z' ,
} ) ,
] ) ;
rmSync ( ` ${ testAssetDir } /temp/xmp ` , { recursive : true , force : true } ) ;
} ) ;
it ( 'should import metadata from file.ext.xmp' , async ( ) = > {
const library = await utils . createLibrary ( admin . accessToken , {
ownerId : admin.userId ,
importPaths : [ ` ${ testAssetDirInternal } /temp/xmp ` ] ,
} ) ;
cpSync ( ` ${ testAssetDir } /metadata/xmp/dates/2000.xmp ` , ` ${ testAssetDir } /temp/xmp/glarus.nef.xmp ` ) ;
cpSync ( ` ${ testAssetDir } /formats/raw/Nikon/D80/glarus.nef ` , ` ${ testAssetDir } /temp/xmp/glarus.nef ` ) ;
2025-02-18 00:26:44 +01:00
await utils . scan ( admin . accessToken , library . id ) ;
2024-12-22 03:50:07 +01:00
const { assets : newAssets } = await utils . searchAssets ( admin . accessToken , { libraryId : library.id } ) ;
expect ( newAssets . items ) . toEqual ( [
expect . objectContaining ( {
originalFileName : 'glarus.nef' ,
fileCreatedAt : '2000-09-27T12:35:33.000Z' ,
} ) ,
] ) ;
rmSync ( ` ${ testAssetDir } /temp/xmp ` , { recursive : true , force : true } ) ;
} ) ;
it ( 'should import metadata in file.ext.xmp before file.xmp if both exist' , async ( ) = > {
const library = await utils . createLibrary ( admin . accessToken , {
ownerId : admin.userId ,
importPaths : [ ` ${ testAssetDirInternal } /temp/xmp ` ] ,
} ) ;
cpSync ( ` ${ testAssetDir } /metadata/xmp/dates/2000.xmp ` , ` ${ testAssetDir } /temp/xmp/glarus.nef.xmp ` ) ;
cpSync ( ` ${ testAssetDir } /metadata/xmp/dates/2010.xmp ` , ` ${ testAssetDir } /temp/xmp/glarus.xmp ` ) ;
cpSync ( ` ${ testAssetDir } /formats/raw/Nikon/D80/glarus.nef ` , ` ${ testAssetDir } /temp/xmp/glarus.nef ` ) ;
2025-02-18 00:26:44 +01:00
await utils . scan ( admin . accessToken , library . id ) ;
2024-12-22 03:50:07 +01:00
const { assets : newAssets } = await utils . searchAssets ( admin . accessToken , { libraryId : library.id } ) ;
expect ( newAssets . items ) . toEqual ( [
expect . objectContaining ( {
originalFileName : 'glarus.nef' ,
fileCreatedAt : '2000-09-27T12:35:33.000Z' ,
} ) ,
] ) ;
rmSync ( ` ${ testAssetDir } /temp/xmp ` , { recursive : true , force : true } ) ;
} ) ;
it ( 'should switch from using file.xmp to file.ext.xmp when asset refreshes' , async ( ) = > {
const library = await utils . createLibrary ( admin . accessToken , {
ownerId : admin.userId ,
importPaths : [ ` ${ testAssetDirInternal } /temp/xmp ` ] ,
} ) ;
cpSync ( ` ${ testAssetDir } /metadata/xmp/dates/2000.xmp ` , ` ${ testAssetDir } /temp/xmp/glarus.xmp ` ) ;
cpSync ( ` ${ testAssetDir } /formats/raw/Nikon/D80/glarus.nef ` , ` ${ testAssetDir } /temp/xmp/glarus.nef ` ) ;
await utimes ( ` ${ testAssetDir } /temp/xmp/glarus.nef ` , 447 _775_200_000 ) ;
2025-02-18 00:26:44 +01:00
await utils . scan ( admin . accessToken , library . id ) ;
2024-12-22 03:50:07 +01:00
cpSync ( ` ${ testAssetDir } /metadata/xmp/dates/2010.xmp ` , ` ${ testAssetDir } /temp/xmp/glarus.nef.xmp ` ) ;
unlinkSync ( ` ${ testAssetDir } /temp/xmp/glarus.xmp ` ) ;
await utimes ( ` ${ testAssetDir } /temp/xmp/glarus.nef ` , 447 _775_200_001 ) ;
2025-02-18 00:26:44 +01:00
await utils . scan ( admin . accessToken , library . id ) ;
2024-12-22 03:50:07 +01:00
const { assets : newAssets } = await utils . searchAssets ( admin . accessToken , { libraryId : library.id } ) ;
expect ( newAssets . items ) . toEqual ( [
expect . objectContaining ( {
originalFileName : 'glarus.nef' ,
fileCreatedAt : '2010-09-27T12:35:33.000Z' ,
} ) ,
] ) ;
rmSync ( ` ${ testAssetDir } /temp/xmp ` , { recursive : true , force : true } ) ;
} ) ;
it ( 'should switch from using file metadata to file.xmp metadata when asset refreshes' , async ( ) = > {
const library = await utils . createLibrary ( admin . accessToken , {
ownerId : admin.userId ,
importPaths : [ ` ${ testAssetDirInternal } /temp/xmp ` ] ,
} ) ;
cpSync ( ` ${ testAssetDir } /formats/raw/Nikon/D80/glarus.nef ` , ` ${ testAssetDir } /temp/xmp/glarus.nef ` ) ;
await utimes ( ` ${ testAssetDir } /temp/xmp/glarus.nef ` , 447 _775_200_000 ) ;
2025-02-18 00:26:44 +01:00
await utils . scan ( admin . accessToken , library . id ) ;
2024-12-22 03:50:07 +01:00
cpSync ( ` ${ testAssetDir } /metadata/xmp/dates/2000.xmp ` , ` ${ testAssetDir } /temp/xmp/glarus.xmp ` ) ;
await utimes ( ` ${ testAssetDir } /temp/xmp/glarus.nef ` , 447 _775_200_001 ) ;
2025-02-18 00:26:44 +01:00
await utils . scan ( admin . accessToken , library . id ) ;
2024-12-22 03:50:07 +01:00
const { assets : newAssets } = await utils . searchAssets ( admin . accessToken , { libraryId : library.id } ) ;
expect ( newAssets . items ) . toEqual ( [
expect . objectContaining ( {
originalFileName : 'glarus.nef' ,
fileCreatedAt : '2000-09-27T12:35:33.000Z' ,
} ) ,
] ) ;
rmSync ( ` ${ testAssetDir } /temp/xmp ` , { recursive : true , force : true } ) ;
} ) ;
it ( 'should switch from using file metadata to file.xmp metadata when asset refreshes' , async ( ) = > {
const library = await utils . createLibrary ( admin . accessToken , {
ownerId : admin.userId ,
importPaths : [ ` ${ testAssetDirInternal } /temp/xmp ` ] ,
} ) ;
cpSync ( ` ${ testAssetDir } /formats/raw/Nikon/D80/glarus.nef ` , ` ${ testAssetDir } /temp/xmp/glarus.nef ` ) ;
await utimes ( ` ${ testAssetDir } /temp/xmp/glarus.nef ` , 447 _775_200_000 ) ;
2025-02-18 00:26:44 +01:00
await utils . scan ( admin . accessToken , library . id ) ;
2024-12-22 03:50:07 +01:00
cpSync ( ` ${ testAssetDir } /metadata/xmp/dates/2000.xmp ` , ` ${ testAssetDir } /temp/xmp/glarus.nef.xmp ` ) ;
await utimes ( ` ${ testAssetDir } /temp/xmp/glarus.nef ` , 447 _775_200_001 ) ;
2025-02-18 00:26:44 +01:00
await utils . scan ( admin . accessToken , library . id ) ;
2024-12-22 03:50:07 +01:00
const { assets : newAssets } = await utils . searchAssets ( admin . accessToken , { libraryId : library.id } ) ;
expect ( newAssets . items ) . toEqual ( [
expect . objectContaining ( {
originalFileName : 'glarus.nef' ,
fileCreatedAt : '2000-09-27T12:35:33.000Z' ,
} ) ,
] ) ;
rmSync ( ` ${ testAssetDir } /temp/xmp ` , { recursive : true , force : true } ) ;
} ) ;
it ( 'should switch from using file.ext.xmp to file.xmp when asset refreshes' , async ( ) = > {
const library = await utils . createLibrary ( admin . accessToken , {
ownerId : admin.userId ,
importPaths : [ ` ${ testAssetDirInternal } /temp/xmp ` ] ,
} ) ;
cpSync ( ` ${ testAssetDir } /metadata/xmp/dates/2000.xmp ` , ` ${ testAssetDir } /temp/xmp/glarus.nef.xmp ` ) ;
cpSync ( ` ${ testAssetDir } /formats/raw/Nikon/D80/glarus.nef ` , ` ${ testAssetDir } /temp/xmp/glarus.nef ` ) ;
await utimes ( ` ${ testAssetDir } /temp/xmp/glarus.nef ` , 447 _775_200_000 ) ;
2025-02-18 00:26:44 +01:00
await utils . scan ( admin . accessToken , library . id ) ;
2024-12-22 03:50:07 +01:00
cpSync ( ` ${ testAssetDir } /metadata/xmp/dates/2010.xmp ` , ` ${ testAssetDir } /temp/xmp/glarus.xmp ` ) ;
unlinkSync ( ` ${ testAssetDir } /temp/xmp/glarus.nef.xmp ` ) ;
await utimes ( ` ${ testAssetDir } /temp/xmp/glarus.nef ` , 447 _775_200_001 ) ;
2025-02-18 00:26:44 +01:00
await utils . scan ( admin . accessToken , library . id ) ;
2024-12-22 03:50:07 +01:00
const { assets : newAssets } = await utils . searchAssets ( admin . accessToken , { libraryId : library.id } ) ;
expect ( newAssets . items ) . toEqual ( [
expect . objectContaining ( {
originalFileName : 'glarus.nef' ,
fileCreatedAt : '2010-09-27T12:35:33.000Z' ,
} ) ,
] ) ;
rmSync ( ` ${ testAssetDir } /temp/xmp ` , { recursive : true , force : true } ) ;
} ) ;
it ( 'should switch from using file.ext.xmp to file metadata' , async ( ) = > {
const library = await utils . createLibrary ( admin . accessToken , {
ownerId : admin.userId ,
importPaths : [ ` ${ testAssetDirInternal } /temp/xmp ` ] ,
} ) ;
cpSync ( ` ${ testAssetDir } /metadata/xmp/dates/2000.xmp ` , ` ${ testAssetDir } /temp/xmp/glarus.nef.xmp ` ) ;
cpSync ( ` ${ testAssetDir } /formats/raw/Nikon/D80/glarus.nef ` , ` ${ testAssetDir } /temp/xmp/glarus.nef ` ) ;
await utimes ( ` ${ testAssetDir } /temp/xmp/glarus.nef ` , 447 _775_200_000 ) ;
2025-02-18 00:26:44 +01:00
await utils . scan ( admin . accessToken , library . id ) ;
2024-12-22 03:50:07 +01:00
unlinkSync ( ` ${ testAssetDir } /temp/xmp/glarus.nef.xmp ` ) ;
await utimes ( ` ${ testAssetDir } /temp/xmp/glarus.nef ` , 447 _775_200_001 ) ;
2025-02-18 00:26:44 +01:00
await utils . scan ( admin . accessToken , library . id ) ;
2024-12-22 03:50:07 +01:00
const { assets : newAssets } = await utils . searchAssets ( admin . accessToken , { libraryId : library.id } ) ;
expect ( newAssets . items ) . toEqual ( [
expect . objectContaining ( {
originalFileName : 'glarus.nef' ,
fileCreatedAt : '2010-07-20T17:27:12.000Z' ,
} ) ,
] ) ;
rmSync ( ` ${ testAssetDir } /temp/xmp ` , { recursive : true , force : true } ) ;
} ) ;
it ( 'should switch from using file.xmp to file metadata' , async ( ) = > {
const library = await utils . createLibrary ( admin . accessToken , {
ownerId : admin.userId ,
importPaths : [ ` ${ testAssetDirInternal } /temp/xmp ` ] ,
} ) ;
cpSync ( ` ${ testAssetDir } /metadata/xmp/dates/2000.xmp ` , ` ${ testAssetDir } /temp/xmp/glarus.xmp ` ) ;
cpSync ( ` ${ testAssetDir } /formats/raw/Nikon/D80/glarus.nef ` , ` ${ testAssetDir } /temp/xmp/glarus.nef ` ) ;
await utimes ( ` ${ testAssetDir } /temp/xmp/glarus.nef ` , 447 _775_200_000 ) ;
2025-02-18 00:26:44 +01:00
await utils . scan ( admin . accessToken , library . id ) ;
2024-12-22 03:50:07 +01:00
unlinkSync ( ` ${ testAssetDir } /temp/xmp/glarus.xmp ` ) ;
await utimes ( ` ${ testAssetDir } /temp/xmp/glarus.nef ` , 447 _775_200_001 ) ;
2025-02-18 00:26:44 +01:00
await utils . scan ( admin . accessToken , library . id ) ;
2024-12-22 03:50:07 +01:00
const { assets : newAssets } = await utils . searchAssets ( admin . accessToken , { libraryId : library.id } ) ;
expect ( newAssets . items ) . toEqual ( [
expect . objectContaining ( {
originalFileName : 'glarus.nef' ,
fileCreatedAt : '2010-07-20T17:27:12.000Z' ,
} ) ,
] ) ;
rmSync ( ` ${ testAssetDir } /temp/xmp ` , { recursive : true , force : true } ) ;
} ) ;
} ) ;
2025-01-07 19:25:43 +01:00
it ( 'should set an offline asset to online if its file exists, is in an import path, and not covered by an exclusion pattern' , async ( ) = > {
utils . createImageFile ( ` ${ testAssetDir } /temp/offline/offline.png ` ) ;
const library = await utils . createLibrary ( admin . accessToken , {
ownerId : admin.userId ,
importPaths : [ ` ${ testAssetDirInternal } /temp/offline ` ] ,
} ) ;
2025-02-18 00:26:44 +01:00
await utils . scan ( admin . accessToken , library . id ) ;
2025-01-07 19:25:43 +01:00
const { assets } = await utils . searchAssets ( admin . accessToken , { libraryId : library.id } ) ;
utils . renameImageFile ( ` ${ testAssetDir } /temp/offline/offline.png ` , ` ${ testAssetDir } /temp/offline.png ` ) ;
2025-02-18 01:04:38 +01:00
await utils . scan ( admin . accessToken , library . id ) ;
2025-01-07 19:25:43 +01:00
const offlineAsset = await utils . getAssetInfo ( admin . accessToken , assets . items [ 0 ] . id ) ;
expect ( offlineAsset . isTrashed ) . toBe ( true ) ;
expect ( offlineAsset . originalPath ) . toBe ( ` ${ testAssetDirInternal } /temp/offline/offline.png ` ) ;
expect ( offlineAsset . isOffline ) . toBe ( true ) ;
{
const { assets } = await utils . searchAssets ( admin . accessToken , { libraryId : library.id , withDeleted : true } ) ;
expect ( assets . count ) . toBe ( 1 ) ;
}
utils . renameImageFile ( ` ${ testAssetDir } /temp/offline.png ` , ` ${ testAssetDir } /temp/offline/offline.png ` ) ;
2025-02-18 01:04:38 +01:00
await utils . scan ( admin . accessToken , library . id ) ;
2025-01-07 19:25:43 +01:00
const backOnlineAsset = await utils . getAssetInfo ( admin . accessToken , assets . items [ 0 ] . id ) ;
expect ( backOnlineAsset . isTrashed ) . toBe ( false ) ;
expect ( backOnlineAsset . originalPath ) . toBe ( ` ${ testAssetDirInternal } /temp/offline/offline.png ` ) ;
expect ( backOnlineAsset . isOffline ) . toBe ( false ) ;
{
const { assets } = await utils . searchAssets ( admin . accessToken , { libraryId : library.id } ) ;
expect ( assets . count ) . toBe ( 1 ) ;
}
} ) ;
it ( 'should not set an offline asset to online if its file exists, is not covered by an exclusion pattern, but is outside of all import paths' , async ( ) = > {
utils . createImageFile ( ` ${ testAssetDir } /temp/offline/offline.png ` ) ;
const library = await utils . createLibrary ( admin . accessToken , {
ownerId : admin.userId ,
importPaths : [ ` ${ testAssetDirInternal } /temp/offline ` ] ,
} ) ;
2025-02-18 00:26:44 +01:00
await utils . scan ( admin . accessToken , library . id ) ;
2025-01-07 19:25:43 +01:00
const { assets } = await utils . searchAssets ( admin . accessToken , { libraryId : library.id } ) ;
utils . renameImageFile ( ` ${ testAssetDir } /temp/offline/offline.png ` , ` ${ testAssetDir } /temp/offline.png ` ) ;
2025-02-18 01:04:38 +01:00
await utils . scan ( admin . accessToken , library . id ) ;
2025-01-07 19:25:43 +01:00
{
const { assets } = await utils . searchAssets ( admin . accessToken , { libraryId : library.id , withDeleted : true } ) ;
expect ( assets . count ) . toBe ( 1 ) ;
}
const offlineAsset = await utils . getAssetInfo ( admin . accessToken , assets . items [ 0 ] . id ) ;
expect ( offlineAsset . isTrashed ) . toBe ( true ) ;
expect ( offlineAsset . originalPath ) . toBe ( ` ${ testAssetDirInternal } /temp/offline/offline.png ` ) ;
expect ( offlineAsset . isOffline ) . toBe ( true ) ;
utils . renameImageFile ( ` ${ testAssetDir } /temp/offline.png ` , ` ${ testAssetDir } /temp/offline/offline.png ` ) ;
utils . createDirectory ( ` ${ testAssetDir } /temp/another-path/ ` ) ;
await utils . updateLibrary ( admin . accessToken , library . id , {
importPaths : [ ` ${ testAssetDirInternal } /temp/another-path ` ] ,
} ) ;
2025-02-18 01:04:38 +01:00
await utils . scan ( admin . accessToken , library . id ) ;
2025-01-07 19:25:43 +01:00
const stillOfflineAsset = await utils . getAssetInfo ( admin . accessToken , assets . items [ 0 ] . id ) ;
expect ( stillOfflineAsset . isTrashed ) . toBe ( true ) ;
expect ( stillOfflineAsset . originalPath ) . toBe ( ` ${ testAssetDirInternal } /temp/offline/offline.png ` ) ;
expect ( stillOfflineAsset . isOffline ) . toBe ( true ) ;
{
const { assets } = await utils . searchAssets ( admin . accessToken , { libraryId : library.id , withDeleted : true } ) ;
expect ( assets . count ) . toBe ( 1 ) ;
}
utils . removeDirectory ( ` ${ testAssetDir } /temp/another-path/ ` ) ;
} ) ;
it ( 'should not set an offline asset to online if its file exists, is in an import path, but is covered by an exclusion pattern' , async ( ) = > {
utils . createImageFile ( ` ${ testAssetDir } /temp/offline/offline.png ` ) ;
const library = await utils . createLibrary ( admin . accessToken , {
ownerId : admin.userId ,
importPaths : [ ` ${ testAssetDirInternal } /temp/offline ` ] ,
} ) ;
2025-02-18 00:26:44 +01:00
await utils . scan ( admin . accessToken , library . id ) ;
2025-01-07 19:25:43 +01:00
const { assets } = await utils . searchAssets ( admin . accessToken , { libraryId : library.id } ) ;
utils . renameImageFile ( ` ${ testAssetDir } /temp/offline/offline.png ` , ` ${ testAssetDir } /temp/offline.png ` ) ;
2025-02-18 01:04:38 +01:00
await utils . scan ( admin . accessToken , library . id ) ;
2025-01-07 19:25:43 +01:00
{
const { assets } = await utils . searchAssets ( admin . accessToken , { libraryId : library.id , withDeleted : true } ) ;
expect ( assets . count ) . toBe ( 1 ) ;
}
const offlineAsset = await utils . getAssetInfo ( admin . accessToken , assets . items [ 0 ] . id ) ;
expect ( offlineAsset . isTrashed ) . toBe ( true ) ;
expect ( offlineAsset . originalPath ) . toBe ( ` ${ testAssetDirInternal } /temp/offline/offline.png ` ) ;
expect ( offlineAsset . isOffline ) . toBe ( true ) ;
utils . renameImageFile ( ` ${ testAssetDir } /temp/offline.png ` , ` ${ testAssetDir } /temp/offline/offline.png ` ) ;
await utils . updateLibrary ( admin . accessToken , library . id , { exclusionPatterns : [ '**/offline/**' ] } ) ;
2025-02-18 01:04:38 +01:00
await utils . scan ( admin . accessToken , library . id ) ;
2025-01-07 19:25:43 +01:00
const stillOfflineAsset = await utils . getAssetInfo ( admin . accessToken , assets . items [ 0 ] . id ) ;
expect ( stillOfflineAsset . isTrashed ) . toBe ( true ) ;
expect ( stillOfflineAsset . originalPath ) . toBe ( ` ${ testAssetDirInternal } /temp/offline/offline.png ` ) ;
expect ( stillOfflineAsset . isOffline ) . toBe ( true ) ;
{
const { assets } = await utils . searchAssets ( admin . accessToken , { libraryId : library.id , withDeleted : true } ) ;
expect ( assets . count ) . toBe ( 1 ) ;
}
} ) ;
2024-02-29 15:10:08 -05:00
} ) ;
2024-05-22 13:24:57 -04:00
describe ( 'POST /libraries/:id/validate' , ( ) = > {
2024-02-29 15:10:08 -05:00
it ( 'should require authentication' , async ( ) = > {
2024-05-22 13:24:57 -04:00
const { status , body } = await request ( app ) . post ( ` /libraries/ ${ uuidDto . notFound } /validate ` ) . send ( { } ) ;
2024-02-29 15:10:08 -05:00
expect ( status ) . toBe ( 401 ) ;
expect ( body ) . toEqual ( errorDto . unauthorized ) ;
} ) ;
it ( 'should pass with no import paths' , async ( ) = > {
2024-03-07 10:14:36 -05:00
const response = await utils . validateLibrary ( admin . accessToken , library . id , { importPaths : [ ] } ) ;
2024-02-29 15:10:08 -05:00
expect ( response . importPaths ) . toEqual ( [ ] ) ;
} ) ;
it ( 'should fail if path does not exist' , async ( ) = > {
const pathToTest = ` ${ testAssetDirInternal } /does/not/exist ` ;
2024-03-07 10:14:36 -05:00
const response = await utils . validateLibrary ( admin . accessToken , library . id , {
2024-02-29 15:10:08 -05:00
importPaths : [ pathToTest ] ,
} ) ;
expect ( response . importPaths ? . length ) . toEqual ( 1 ) ;
const pathResponse = response ? . importPaths ? . at ( 0 ) ;
expect ( pathResponse ) . toEqual ( {
importPath : pathToTest ,
isValid : false ,
message : ` Path does not exist (ENOENT) ` ,
} ) ;
} ) ;
2024-10-21 16:12:12 +02:00
it ( "should fail if path isn't absolute" , async ( ) = > {
const pathToTest = ` relative/path ` ;
const cwd = process . cwd ( ) ;
// Create directory in cwd
utils . createDirectory ( ` ${ cwd } / ${ pathToTest } ` ) ;
const response = await utils . validateLibrary ( admin . accessToken , library . id , {
importPaths : [ pathToTest ] ,
} ) ;
utils . removeDirectory ( ` ${ cwd } / ${ pathToTest } ` ) ;
expect ( response . importPaths ? . length ) . toEqual ( 1 ) ;
const pathResponse = response ? . importPaths ? . at ( 0 ) ;
expect ( pathResponse ) . toEqual ( {
importPath : pathToTest ,
isValid : false ,
message : expect.stringMatching ( 'Import path must be absolute, try /usr/src/app/relative/path' ) ,
} ) ;
} ) ;
2024-02-29 15:10:08 -05:00
it ( 'should fail if path is a file' , async ( ) = > {
const pathToTest = ` ${ testAssetDirInternal } /albums/nature/el_torcal_rocks.jpg ` ;
2024-03-07 10:14:36 -05:00
const response = await utils . validateLibrary ( admin . accessToken , library . id , {
2024-02-29 15:10:08 -05:00
importPaths : [ pathToTest ] ,
} ) ;
expect ( response . importPaths ? . length ) . toEqual ( 1 ) ;
const pathResponse = response ? . importPaths ? . at ( 0 ) ;
expect ( pathResponse ) . toEqual ( {
importPath : pathToTest ,
isValid : false ,
message : ` Not a directory ` ,
} ) ;
} ) ;
} ) ;
2024-04-12 15:15:41 -04:00
2024-05-22 13:24:57 -04:00
describe ( 'DELETE /libraries/:id' , ( ) = > {
2024-04-12 15:15:41 -04:00
it ( 'should require authentication' , async ( ) = > {
2024-05-22 13:24:57 -04:00
const { status , body } = await request ( app ) . delete ( ` /libraries/ ${ uuidDto . notFound } ` ) ;
2024-04-12 15:15:41 -04:00
expect ( status ) . toBe ( 401 ) ;
expect ( body ) . toEqual ( errorDto . unauthorized ) ;
} ) ;
it ( 'should delete an external library' , async ( ) = > {
2024-05-20 18:09:10 -04:00
const library = await utils . createLibrary ( admin . accessToken , { ownerId : admin.userId } ) ;
2024-04-12 15:15:41 -04:00
const { status , body } = await request ( app )
2024-05-22 13:24:57 -04:00
. delete ( ` /libraries/ ${ library . id } ` )
2024-04-12 15:15:41 -04:00
. set ( 'Authorization' , ` Bearer ${ admin . accessToken } ` ) ;
expect ( status ) . toBe ( 204 ) ;
expect ( body ) . toEqual ( { } ) ;
2024-05-20 18:09:10 -04:00
const libraries = await getAllLibraries ( { headers : asBearerAuth ( admin . accessToken ) } ) ;
2024-04-12 15:15:41 -04:00
expect ( libraries ) . not . toEqual (
expect . arrayContaining ( [
expect . objectContaining ( {
id : library.id ,
} ) ,
] ) ,
) ;
} ) ;
it ( 'should delete an external library with assets' , async ( ) = > {
const library = await utils . createLibrary ( admin . accessToken , {
ownerId : admin.userId ,
importPaths : [ ` ${ testAssetDirInternal } /temp ` ] ,
} ) ;
2025-02-18 00:26:44 +01:00
await utils . scan ( admin . accessToken , library . id ) ;
2024-04-12 15:15:41 -04:00
const { status , body } = await request ( app )
2024-05-22 13:24:57 -04:00
. delete ( ` /libraries/ ${ library . id } ` )
2024-04-12 15:15:41 -04:00
. set ( 'Authorization' , ` Bearer ${ admin . accessToken } ` ) ;
expect ( status ) . toBe ( 204 ) ;
expect ( body ) . toEqual ( { } ) ;
2024-05-20 18:09:10 -04:00
const libraries = await getAllLibraries ( { headers : asBearerAuth ( admin . accessToken ) } ) ;
2024-04-12 15:15:41 -04:00
expect ( libraries ) . not . toEqual (
expect . arrayContaining ( [
expect . objectContaining ( {
id : library.id ,
} ) ,
] ) ,
) ;
// ensure no files get deleted
expect ( existsSync ( ` ${ testAssetDir } /temp/directoryA/assetA.png ` ) ) . toBe ( true ) ;
expect ( existsSync ( ` ${ testAssetDir } /temp/directoryB/assetB.png ` ) ) . toBe ( true ) ;
} ) ;
} ) ;
2024-02-29 15:10:08 -05:00
} ) ;