@@ -16,16 +16,100 @@ export class AddressBookCollectionMigrator extends CollectionMigrator {
1616 * Fetch all addressbooks from source
1717 */
1818 async fetchSourceCollections ( ) : Promise < DAVCollection [ ] > {
19- const addressBooks = await this . sourceClient . fetchAddressBooks ( ) ;
20- return addressBooks as DAVCollection [ ] ;
19+ // Temporarily override homeUrl to point to addressbook home for fetching
20+ const originalHomeUrl = this . sourceClient . account ?. homeUrl ;
21+
22+ try {
23+ // Determine the addressbook home URL (same logic as in createCollection)
24+ const addressbookHomeSet = ( this . sourceClient . account as any ) ?. addressbookHomeSet ;
25+
26+ if ( ! addressbookHomeSet ) {
27+ // Need to construct the addressbook home URL
28+ const serverUrl = this . sourceClient . account ?. serverUrl ?. replace ( / \/ + $ / , '' ) ;
29+ const username = this . sourceClient . account ?. credentials ?. username ;
30+
31+ if ( serverUrl && username ) {
32+ let addressbookHomeUrl : string ;
33+
34+ if ( this . config . source . provider === 'nextcloud' ) {
35+ addressbookHomeUrl = `${ serverUrl } /remote.php/dav/addressbooks/users/${ username } /` ;
36+ } else if ( this . config . source . provider === 'generic' ) {
37+ const principalUrl = this . sourceClient . account ?. principalUrl ;
38+ if ( principalUrl ) {
39+ addressbookHomeUrl = principalUrl . replace ( / p r i n c i p a l s \/ [ ^ / ] + \/ / , 'addressbooks/' ) ;
40+ if ( ! addressbookHomeUrl . endsWith ( '/' ) ) addressbookHomeUrl += '/' ;
41+ } else {
42+ addressbookHomeUrl = originalHomeUrl || '' ;
43+ }
44+ } else {
45+ addressbookHomeUrl = originalHomeUrl || '' ;
46+ }
47+
48+ // Temporarily override homeUrl
49+ if ( this . sourceClient . account ) {
50+ ( this . sourceClient . account as any ) . homeUrl = addressbookHomeUrl ;
51+ }
52+ }
53+ }
54+
55+ const addressBooks = await this . sourceClient . fetchAddressBooks ( ) ;
56+ return addressBooks as DAVCollection [ ] ;
57+ } finally {
58+ // Restore original homeUrl
59+ if ( this . sourceClient . account && originalHomeUrl ) {
60+ ( this . sourceClient . account as any ) . homeUrl = originalHomeUrl ;
61+ }
62+ }
2163 }
2264
2365 /**
2466 * Fetch all addressbooks from target
2567 */
2668 async fetchTargetCollections ( ) : Promise < DAVCollection [ ] > {
27- const addressBooks = await this . targetClient . fetchAddressBooks ( ) ;
28- return addressBooks as DAVCollection [ ] ;
69+ // Temporarily override homeUrl to point to addressbook home for fetching
70+ const originalHomeUrl = this . targetClient . account ?. homeUrl ;
71+
72+ try {
73+ // Determine the addressbook home URL (same logic as in createCollection)
74+ const addressbookHomeSet = ( this . targetClient . account as any ) ?. addressbookHomeSet ;
75+
76+ if ( ! addressbookHomeSet ) {
77+ // Need to construct the addressbook home URL
78+ const serverUrl = this . targetClient . account ?. serverUrl ?. replace ( / \/ + $ / , '' ) ;
79+ const username = this . targetClient . account ?. credentials ?. username ;
80+
81+ if ( serverUrl && username ) {
82+ let addressbookHomeUrl : string ;
83+
84+ if ( this . config . target . provider === 'nextcloud' ) {
85+ addressbookHomeUrl = `${ serverUrl } /remote.php/dav/addressbooks/users/${ username } /` ;
86+ } else if ( this . config . target . provider === 'generic' ) {
87+ const principalUrl = this . targetClient . account ?. principalUrl ;
88+ if ( principalUrl ) {
89+ addressbookHomeUrl = principalUrl . replace ( / p r i n c i p a l s \/ [ ^ / ] + \/ / , 'addressbooks/' ) ;
90+ if ( ! addressbookHomeUrl . endsWith ( '/' ) ) addressbookHomeUrl += '/' ;
91+ } else {
92+ addressbookHomeUrl = originalHomeUrl || '' ;
93+ }
94+ } else {
95+ addressbookHomeUrl = originalHomeUrl || '' ;
96+ }
97+
98+ // Temporarily override homeUrl
99+ if ( this . targetClient . account ) {
100+ ( this . targetClient . account as any ) . homeUrl = addressbookHomeUrl ;
101+ }
102+ }
103+ }
104+
105+ const addressBooks = await this . targetClient . fetchAddressBooks ( ) ;
106+ return addressBooks as DAVCollection [ ] ;
107+ } finally {
108+ // Restore original homeUrl
109+ if ( this . targetClient . account && originalHomeUrl ) {
110+ ( this . targetClient . account as any ) . homeUrl = originalHomeUrl ;
111+ }
112+ }
29113 }
30114
31115 /**
@@ -76,24 +160,90 @@ export class AddressBookCollectionMigrator extends CollectionMigrator {
76160 async createCollection ( displayName : string ) : Promise < DAVCollection > {
77161 const addressBookName = displayName . replace ( / [ ^ a - z A - Z 0 - 9 ] / g, '-' ) . toLowerCase ( ) ||
78162 `addressbook-${ Date . now ( ) } ` ;
79- const homeUrl = this . targetClient . account ?. homeUrl ?. replace ( / \/ + $ / , '' ) || '' ;
80- const newAddressBookUrl = `${ homeUrl } /${ addressBookName } /` ;
81-
82- await this . targetClient . makeCollection ( {
83- url : newAddressBookUrl ,
84- props : {
85- displayname : displayName ,
86- [ `${ DAVNamespaceShort . CARDDAV } :addressbook-description` ] : `Migrated from ${ this . config . source . provider } ` ,
87- } ,
88- } ) ;
163+
164+ // Determine the addressbook home URL
165+ let addressbookHomeUrl : string ;
166+
167+ // Try to get addressbookHomeSet first
168+ const addressbookHomeSet = ( this . targetClient . account as any ) ?. addressbookHomeSet ;
169+
170+ if ( addressbookHomeSet ) {
171+ // If addressbookHomeSet exists, use it
172+ addressbookHomeUrl = ( typeof addressbookHomeSet === 'string' ? addressbookHomeSet : addressbookHomeSet . href )
173+ . replace ( / \/ + $ / , '' ) ;
174+ } else {
175+ // Fallback: construct the URL based on provider and server URL
176+ const serverUrl = this . targetClient . account ?. serverUrl ?. replace ( / \/ + $ / , '' ) ;
177+ const username = this . targetClient . account ?. credentials ?. username ;
178+
179+ if ( ! serverUrl || ! username ) {
180+ throw new Error ( 'Could not determine server URL or username from account' ) ;
181+ }
182+
183+ // Provider-specific URL construction
184+ if ( this . config . target . provider === 'nextcloud' ) {
185+ // Nextcloud uses: /remote.php/dav/addressbooks/users/{username}/
186+ addressbookHomeUrl = `${ serverUrl } /remote.php/dav/addressbooks/users/${ username } ` ;
187+ } else if ( this . config . target . provider === 'generic' ) {
188+ // For generic CardDAV servers, try to construct from principalUrl
189+ const principalUrl = this . targetClient . account ?. principalUrl ;
190+ if ( principalUrl ) {
191+ // Replace 'principals' with 'addressbooks' in the path
192+ addressbookHomeUrl = principalUrl . replace ( / p r i n c i p a l s \/ [ ^ / ] + \/ / , 'addressbooks/' ) . replace ( / \/ + $ / , '' ) ;
193+ } else {
194+ // Last resort: use homeUrl but warn user
195+ addressbookHomeUrl = this . targetClient . account ?. homeUrl ?. replace ( / \/ + $ / , '' ) || '' ;
196+ console . warn ( ` Warning: Using homeUrl for addressbooks (may not work): ${ addressbookHomeUrl } ` ) ;
197+ }
198+ } else {
199+ // Default: try homeUrl
200+ addressbookHomeUrl = this . targetClient . account ?. homeUrl ?. replace ( / \/ + $ / , '' ) || '' ;
201+ console . warn ( ` Warning: Unknown provider '${ this . config . target . provider } ', using homeUrl: ${ addressbookHomeUrl } ` ) ;
202+ }
203+ }
204+
205+ if ( ! addressbookHomeUrl ) {
206+ throw new Error ( 'Could not determine addressbook home URL from account' ) ;
207+ }
208+
209+ const newAddressBookUrl = `${ addressbookHomeUrl } /${ addressBookName } /` ;
210+
211+ // Use makeAddressBook if available (tsdav v2.1.6+), otherwise fallback to makeCollection
212+ try {
213+ if ( typeof ( this . targetClient as any ) . makeAddressBook === 'function' ) {
214+ await ( this . targetClient as any ) . makeAddressBook ( {
215+ url : newAddressBookUrl ,
216+ props : {
217+ [ `${ DAVNamespaceShort . DAV } :displayname` ] : displayName ,
218+ [ `${ DAVNamespaceShort . CARDDAV } :addressbook-description` ] : `Migrated from ${ this . config . source . provider } ` ,
219+ [ `${ DAVNamespaceShort . DAV } :resourcetype` ] : {
220+ [ `${ DAVNamespaceShort . DAV } :collection` ] : { } ,
221+ [ `${ DAVNamespaceShort . CARDDAV } :addressbook` ] : { } ,
222+ } ,
223+ } ,
224+ } ) ;
225+ } else {
226+ // Fallback for older tsdav versions or providers that don't support RFC 5689
227+ await this . targetClient . makeCollection ( {
228+ url : newAddressBookUrl ,
229+ props : {
230+ displayname : displayName ,
231+ [ `${ DAVNamespaceShort . CARDDAV } :addressbook-description` ] : `Migrated from ${ this . config . source . provider } ` ,
232+ } ,
233+ } ) ;
234+ }
235+ } catch ( error ) {
236+ throw new Error ( `Failed to create addressbook at ${ newAddressBookUrl } : ${ ( error as Error ) . message } ` ) ;
237+ }
89238
90239 // Fetch addressbooks again to get the newly created one
91240 await this . targetRateLimiter . throttle ( ) ;
92- const updatedAddressBooks = await this . targetClient . fetchAddressBooks ( ) ;
241+ const updatedAddressBooks = await this . fetchTargetCollections ( ) ;
242+
93243 const newAddressBook = updatedAddressBooks . find ( ( ab ) => ab . url === newAddressBookUrl ) ;
94244
95245 if ( ! newAddressBook ) {
96- throw new Error ( `Failed to create addressbook: ${ displayName } ` ) ;
246+ throw new Error ( `Failed to create addressbook: ${ displayName } at ${ newAddressBookUrl } ` ) ;
97247 }
98248
99249 return newAddressBook as DAVCollection ;
0 commit comments