diff --git a/angular.json b/angular.json index 02d91fb31..33401d953 100644 --- a/angular.json +++ b/angular.json @@ -222,7 +222,7 @@ "preserveSymlinks": true, "karmaConfig": "src/karma.conf.js", "styles": ["src/styles.scss"], - "scripts": ["src/test-helpers/jasmine-spec-reporter-hook.js"], + "scripts": [], "assets": ["src/favicon.ico", "src/assets", "src/manifest.json"] } }, diff --git a/src/app/pfapi/api/sync/providers/webdav/webdav-api-upload.spec.ts b/src/app/pfapi/api/sync/providers/webdav/webdav-api-upload.spec.ts index 4cd178252..51824031a 100644 --- a/src/app/pfapi/api/sync/providers/webdav/webdav-api-upload.spec.ts +++ b/src/app/pfapi/api/sync/providers/webdav/webdav-api-upload.spec.ts @@ -244,18 +244,32 @@ describe('WebdavApi - Upload Operations', () => { }); it('should handle 409 conflict by creating parent directories', async () => { - // The upload method doesn't handle 409 specially - it just throws NoEtagAPIError - const mockResponse = createMockResponse(409); - mockFetch.and.returnValue(Promise.resolve(mockResponse)); + // First upload returns 409, then after creating directories, second upload succeeds + const firstResponse = createMockResponse(409); + const secondResponse = createMockResponse(201, { etag: '"after-mkdir-etag"' }); - await expectAsync( - api.upload({ - data: 'test data', - path: 'folder/test.txt', - isOverwrite: false, - expectedEtag: null, - }), - ).toBeRejectedWith(jasmine.any(NoEtagAPIError)); + let callCount = 0; + mockFetch.and.callFake((url: string, options: any) => { + if (options.method === 'PUT') { + callCount++; + return Promise.resolve(callCount === 1 ? firstResponse : secondResponse); + } else if (options.method === 'MKCOL') { + // Directory creation succeeds + return Promise.resolve(createMockResponse(201)); + } + // Other requests + return Promise.resolve(createMockResponse(404)); + }); + + const result = await api.upload({ + data: 'test data', + path: 'folder/test.txt', + isOverwrite: false, + expectedEtag: null, + }); + + expect(result).toBe('after-mkdir-etag'); + expect(callCount).toBe(2); // Initial attempt + retry after creating directories }); it('should throw NoEtagAPIError after all retry attempts fail', async () => { diff --git a/src/app/pfapi/api/sync/providers/webdav/webdav-api.ts b/src/app/pfapi/api/sync/providers/webdav/webdav-api.ts index 2bdd975e3..3f529ec0d 100644 --- a/src/app/pfapi/api/sync/providers/webdav/webdav-api.ts +++ b/src/app/pfapi/api/sync/providers/webdav/webdav-api.ts @@ -138,8 +138,7 @@ export class WebdavApi { data: string, headers: Record, errorCode: number, - ): Promise { - const context = errorCode === 404 ? 'upload-404-retry' : 'upload-409-retry'; + ): Promise { PFLog.error( `${WebdavApi.L}.upload() ${errorCode} ${errorCode === 404 ? 'not found' : 'conflict'}, attempting to create parent directories`, { path }, @@ -148,25 +147,14 @@ export class WebdavApi { try { await this._ensureParentDirectoryExists(path); - // Retry the upload - const retryResponse = await this._makeRequest({ - method: 'PUT', + // Retry the upload with _retryAttempted flag to prevent infinite loops + return await this.upload({ + data, path, - body: data, - headers, + isOverwrite: true, // After creating directories, we can overwrite + expectedEtag: null, + _retryAttempted: true, // Critical: prevent infinite retry loops }); - - const retryHeaderObj = this._responseHeadersToObject(retryResponse); - const validators = this._extractValidators(retryHeaderObj); - - if (!validators.validator) { - return await this._retrieveEtagWithFallback(path, retryHeaderObj, context); - } - - // Clean ETag if it's an ETag validator - return validators.validatorType === 'etag' - ? this._cleanRev(validators.validator) - : validators.validator; } catch (retryError: any) { PFLog.err(`${WebdavApi.L}.upload() retry after ${errorCode} failed`, retryError); @@ -174,7 +162,8 @@ export class WebdavApi { if ( retryError instanceof NoEtagAPIError || retryError instanceof NoRevAPIError || - retryError instanceof RemoteFileNotFoundAPIError + retryError instanceof RemoteFileNotFoundAPIError || + retryError instanceof HttpNotOkAPIError ) { throw retryError; } @@ -547,12 +536,18 @@ export class WebdavApi { throw new Error(`Upload failed: Resource is locked`); } else if (response.status === 409) { // Conflict - parent directory doesn't exist - return await this._retryUploadWithDirectoryCreation( - path, - data, - headers || {}, - 409, - ); + if (!_retryAttempted) { + return await this._retryUploadWithDirectoryCreation( + path, + data, + headers || {}, + 409, + ); + } + // If retry was already attempted, throw error with retry flag + const error = new HttpNotOkAPIError(response); + (error as any)._retryAttempted = true; + throw error; } // Extract validator from response headers @@ -610,12 +605,16 @@ export class WebdavApi { switch (errorStatus) { case 409: // Conflict - parent directory doesn't exist - return await this._retryUploadWithDirectoryCreation( - path, - data, - headers || {}, - 409, - ); + // Check both the method parameter and the error flag to prevent infinite loops + if (!_retryAttempted && !(e as any)?._retryAttempted) { + return await this._retryUploadWithDirectoryCreation( + path, + data, + headers || {}, + 409, + ); + } + throw e; case 412: // Precondition Failed - conditional header failed if (expectedEtag) { @@ -1569,8 +1568,8 @@ export class WebdavApi { // Construct the URL const url = new URL(normalizedPath, baseUrl).toString(); - // Log for debugging - increased log level for better visibility - PFLog.error(`${WebdavApi.L}._getUrl() constructed URL`, { + // Log for debugging + PFLog.debug(`${WebdavApi.L}._getUrl() constructed URL`, { baseUrl, path, normalizedPath, diff --git a/webdav.yaml b/webdav.yaml index e28b58666..d8a99395e 100644 --- a/webdav.yaml +++ b/webdav.yaml @@ -31,8 +31,11 @@ cors: users: - username: alice - password: alicepassword + password: alice directory: /data/alice - username: bob - password: bobpassword + password: bob directory: /data/bob + - username: admin + password: admin + directory: /data/admin