feat(sync): split up webdav model stuff into different files

This commit is contained in:
Johannes Millan 2025-07-18 14:09:22 +02:00
parent 6858ce3c91
commit 8201f745ba
26 changed files with 151 additions and 144 deletions

View file

@ -1,6 +1,6 @@
import { WebdavApi } from './src/app/pfapi/api/sync/providers/webdav/webdav-api';
import { WebdavPrivateCfg } from './src/app/pfapi/api/sync/providers/webdav/webdav';
import { createMockResponse } from './src/app/pfapi/api/sync/providers/webdav/webdav-api-test-utils';
import { WebdavPrivateCfg } from './src/app/pfapi/api/sync/providers/webdav/webdav.model';
describe('Debug Headers', () => {
let mockFetch: jasmine.Spy;

View file

@ -6,3 +6,7 @@ export * from './sync/providers/dropbox/dropbox';
export * from './sync/providers/webdav/webdav';
export * from './sync/sync-provider.interface';
export * from './errors/errors';
export { WebdavServerType } from './sync/providers/webdav/webdav.model';
export { WebdavPrivateCfg } from './sync/providers/webdav/webdav.model';
export { WebdavServerCapabilities } from './sync/providers/webdav/webdav.model';
export { getRecommendedServerCapabilities } from './sync/providers/webdav/getRecommendedServerCapabilities';

View file

@ -2,8 +2,8 @@ import { DatabaseAdapter } from './db/database-adapter.model';
import { ModelCtrl } from './model-ctrl/model-ctrl';
import { ConflictReason, SyncProviderId, SyncStatus } from './pfapi.const';
import { DropboxPrivateCfg } from './sync/providers/dropbox/dropbox';
import { WebdavPrivateCfg } from './sync/providers/webdav/webdav';
import { IValidation } from 'typia';
import { WebdavPrivateCfg } from './sync/providers/webdav/webdav.model';
type JSONPrimitive = string | number | boolean | null;
type Serializable = JSONPrimitive | SerializableObject | SerializableArray;

View file

@ -1,6 +1,6 @@
import { WebdavApi } from './webdav-api';
import { WebdavPrivateCfg } from './webdav';
import { createMockResponse } from './webdav-api-test-utils';
import { WebdavPrivateCfg } from './webdav.model';
describe('Debug Headers', () => {
let mockFetch: jasmine.Spy;

View file

@ -0,0 +1,53 @@
import { WebdavServerCapabilities, WebdavServerType } from './webdav.model';
/**
* Helper function to get recommended server capabilities for common WebDAV server types
*/
export const getRecommendedServerCapabilities = (
serverType: WebdavServerType,
): WebdavServerCapabilities => {
switch (serverType) {
case WebdavServerType.NEXTCLOUD:
case WebdavServerType.OWNCLOUD:
return {
supportsETags: true,
supportsIfHeader: true,
supportsLocking: true,
supportsLastModified: true,
};
case WebdavServerType.APACHE_MOD_DAV:
return {
supportsETags: true,
supportsIfHeader: false, // mod_dav has limited If header support
supportsLocking: true,
supportsLastModified: true,
};
case WebdavServerType.NGINX_DAV:
return {
supportsETags: false, // nginx dav module has limited ETag support
supportsIfHeader: false,
supportsLocking: false,
supportsLastModified: true,
};
case WebdavServerType.BASIC_WEBDAV:
return {
supportsETags: false,
supportsIfHeader: false,
supportsLocking: false,
supportsLastModified: true,
};
case WebdavServerType.CUSTOM:
default:
// Return null to trigger auto-detection
return {
supportsETags: false,
supportsIfHeader: false,
supportsLocking: false,
supportsLastModified: false,
};
}
};

View file

@ -1,11 +1,11 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { WebdavApi } from './webdav-api';
import { WebdavPrivateCfg } from './webdav';
import {
RemoteFileNotFoundAPIError,
NoEtagAPIError,
HttpNotOkAPIError,
} from '../../../errors/errors';
import { WebdavPrivateCfg } from './webdav.model';
describe('WebdavApi - Additional Coverage Tests', () => {
let api: WebdavApi;

View file

@ -1,9 +1,9 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { WebdavApi } from './webdav-api';
import { WebdavPrivateCfg } from './webdav';
// import { HttpNotOkAPIError, RemoteFileNotFoundAPIError } from '../../../errors/errors';
import { CapacitorHttp } from '@capacitor/core';
import { IS_ANDROID_WEB_VIEW } from '../../../../../util/is-android-web-view';
import { WebdavPrivateCfg } from './webdav.model';
describe('WebdavApi - Android WebView', () => {
let mockGetCfgOrError: jasmine.Spy;

View file

@ -1,6 +1,6 @@
import { WebdavApi } from './webdav-api';
import { WebdavPrivateCfg } from './webdav';
import { createMockResponse, createPropfindResponse } from './webdav-api-test-utils';
import { WebdavPrivateCfg } from './webdav.model';
describe('WebdavApi Capability Detection', () => {
let api: WebdavApi;

View file

@ -1,10 +1,7 @@
import { WebdavApi } from './webdav-api';
import {
WebdavPrivateCfg,
WebdavServerType,
getRecommendedServerCapabilities,
} from './webdav';
import { createMockResponse } from './webdav-api-test-utils';
import { WebdavPrivateCfg, WebdavServerType } from './webdav.model';
import { getRecommendedServerCapabilities } from './getRecommendedServerCapabilities';
/* eslint-disable @typescript-eslint/naming-convention */

View file

@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { WebdavApi } from './webdav-api';
import { WebdavPrivateCfg } from './webdav';
import { WebdavPrivateCfg } from './webdav.model';
describe('WebdavApi - Connection Testing', () => {
let api: WebdavApi;

View file

@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { WebdavApi } from './webdav-api';
import { WebdavPrivateCfg } from './webdav';
import { HttpNotOkAPIError, RemoteFileNotFoundAPIError } from '../../../errors/errors';
import { WebdavPrivateCfg } from './webdav.model';
describe('WebdavApi - Download Operations', () => {
let api: WebdavApi;

View file

@ -1,5 +1,4 @@
import { WebdavApi } from './webdav-api';
import { WebdavPrivateCfg } from './webdav';
import { createMockResponse, createMockResponseFactory } from './webdav-api-test-utils';
import {
NoRevAPIError,
@ -7,6 +6,7 @@ import {
FileExistsAPIError,
RemoteFileNotFoundAPIError,
} from '../../../errors/errors';
import { WebdavPrivateCfg } from './webdav.model';
/* eslint-disable @typescript-eslint/naming-convention */

View file

@ -1,11 +1,11 @@
import { WebdavApi } from './webdav-api';
import { WebdavPrivateCfg } from './webdav';
import {
createMockResponse,
createMockResponseFactory,
createPropfindResponse,
} from './webdav-api-test-utils';
import { RemoteFileNotFoundAPIError } from '../../../errors/errors';
import { WebdavPrivateCfg } from './webdav.model';
/* eslint-disable @typescript-eslint/naming-convention */

View file

@ -1,8 +1,8 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { WebdavApi } from './webdav-api';
import { WebdavPrivateCfg } from './webdav';
import { NoEtagAPIError, RemoteFileNotFoundAPIError } from '../../../errors/errors';
import { createMockResponseFactory } from './webdav-api-test-utils';
import { WebdavPrivateCfg } from './webdav.model';
describe('WebdavApi - Fallback Paths', () => {
let api: WebdavApi;

View file

@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { WebdavApi } from './webdav-api';
import { WebdavPrivateCfg } from './webdav';
import { HttpNotOkAPIError, RemoteFileNotFoundAPIError } from '../../../errors/errors';
import { WebdavPrivateCfg } from './webdav.model';
describe('WebdavApi - File Operations', () => {
let api: WebdavApi;

View file

@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { WebdavApi } from './webdav-api';
import { WebdavPrivateCfg } from './webdav';
import { AuthFailSPError } from '../../../errors/errors';
import { WebdavPrivateCfg } from './webdav.model';
describe('WebdavApi - Helper Methods', () => {
let api: WebdavApi;

View file

@ -1,7 +1,7 @@
import { WebdavApi } from './webdav-api';
import { WebdavPrivateCfg } from './webdav';
import { createMockResponse } from './webdav-api-test-utils';
import { RemoteFileNotFoundAPIError, NoEtagAPIError } from '../../../errors/errors';
import { WebdavPrivateCfg } from './webdav.model';
/* eslint-disable @typescript-eslint/naming-convention */

View file

@ -1,5 +1,6 @@
import { WebdavApi } from './webdav-api';
import { WebdavPrivateCfg } from './webdav';
import { WebdavPrivateCfg } from './webdav.model';
/* eslint-disable @typescript-eslint/naming-convention */

View file

@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { WebdavApi } from './webdav-api';
import { WebdavPrivateCfg } from './webdav';
import { RemoteFileNotFoundAPIError, NoEtagAPIError } from '../../../errors/errors';
import { WebdavPrivateCfg } from './webdav.model';
describe('WebdavApi - Metadata Operations', () => {
let api: WebdavApi;

View file

@ -1,5 +1,6 @@
import { WebdavApi } from './webdav-api';
import { WebdavPrivateCfg } from './webdav';
import { WebdavPrivateCfg } from './webdav.model';
describe('WebdavApi Safe Creation Simple Test', () => {
it('should be created', () => {

View file

@ -1,7 +1,7 @@
import { WebdavApi } from './webdav-api';
import { WebdavPrivateCfg } from './webdav';
import { createMockResponse } from './webdav-api-test-utils';
import { FileExistsAPIError } from '../../../errors/errors';
import { WebdavPrivateCfg } from './webdav.model';
/* eslint-disable @typescript-eslint/naming-convention */

View file

@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { WebdavPrivateCfg } from './webdav';
import { WebdavPrivateCfg } from './webdav.model';
export const createMockResponse = (
status: number,

View file

@ -1,12 +1,12 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { WebdavApi } from './webdav-api';
import { WebdavPrivateCfg } from './webdav';
import {
FileExistsAPIError,
HttpNotOkAPIError,
NoEtagAPIError,
RemoteFileNotFoundAPIError,
} from '../../../errors/errors';
import { WebdavPrivateCfg } from './webdav.model';
describe('WebdavApi - Upload Operations', () => {
let api: WebdavApi;

View file

@ -0,0 +1,63 @@
import { SyncProviderPrivateCfgBase } from '../../../pfapi.model';
export interface WebdavServerCapabilities {
/** Whether the server supports ETag headers for versioning */
supportsETags: boolean;
/** Whether the server supports WebDAV If headers (RFC 4918) */
supportsIfHeader: boolean;
/** Whether the server supports WebDAV LOCK/UNLOCK operations */
supportsLocking: boolean;
/** Whether the server supports Last-Modified headers for versioning */
supportsLastModified: boolean;
}
export interface WebdavPrivateCfg extends SyncProviderPrivateCfgBase {
baseUrl: string;
userName: string;
password: string;
syncFolderPath: string;
/**
* Server capabilities configuration. If not provided, capabilities will be
* detected automatically on first use. Providing this configuration can
* improve performance by skipping detection and ensure consistent behavior.
*
* Recommended settings for common servers:
* - Nextcloud/ownCloud: { supportsETags: true, supportsIfHeader: true, supportsLocking: true, supportsLastModified: true }
* - Apache mod_dav: { supportsETags: true, supportsIfHeader: false, supportsLocking: true, supportsLastModified: true }
* - Basic WebDAV: { supportsETags: false, supportsIfHeader: false, supportsLocking: false, supportsLastModified: true }
*/
serverCapabilities?: WebdavServerCapabilities;
/**
* Force the use of Last-Modified headers instead of ETags, even if ETags are available.
* This can be useful for testing fallback behavior or working with servers that have
* unreliable ETag implementations.
*/
preferLastModified?: boolean;
/**
* Compatibility mode for servers with limited WebDAV support.
* When enabled, disables conditional operations and safe creation mechanisms.
* Use only for very basic WebDAV servers that don't support any conditional headers.
*/
basicCompatibilityMode?: boolean;
/**
* Maximum number of retry attempts for capability detection and fallback operations.
* Default: 2
*/
maxRetries?: number;
}
/**
* Server type enum for common WebDAV implementations
*/
export enum WebdavServerType {
NEXTCLOUD = 'nextcloud',
OWNCLOUD = 'owncloud',
APACHE_MOD_DAV = 'apache_mod_dav',
NGINX_DAV = 'nginx_dav',
BASIC_WEBDAV = 'basic_webdav',
CUSTOM = 'custom',
}

View file

@ -1,4 +1,4 @@
import { Webdav, WebdavPrivateCfg } from './webdav';
import { Webdav } from './webdav';
import { WebdavApi } from './webdav-api';
import { SyncProviderId } from '../../../pfapi.const';
import { SyncProviderPrivateCfgStore } from '../../sync-provider-private-cfg-store';
@ -7,6 +7,7 @@ import {
MissingCredentialsSPError,
NoRevAPIError,
} from '../../../errors/errors';
import { WebdavPrivateCfg } from './webdav.model';
describe('Webdav', () => {
let webdav: Webdav;

View file

@ -7,121 +7,7 @@ import {
MissingCredentialsSPError,
NoRevAPIError,
} from '../../../errors/errors';
import { SyncProviderPrivateCfgBase } from '../../../pfapi.model';
export interface WebdavServerCapabilities {
/** Whether the server supports ETag headers for versioning */
supportsETags: boolean;
/** Whether the server supports WebDAV If headers (RFC 4918) */
supportsIfHeader: boolean;
/** Whether the server supports WebDAV LOCK/UNLOCK operations */
supportsLocking: boolean;
/** Whether the server supports Last-Modified headers for versioning */
supportsLastModified: boolean;
}
export interface WebdavPrivateCfg extends SyncProviderPrivateCfgBase {
baseUrl: string;
userName: string;
password: string;
syncFolderPath: string;
/**
* Server capabilities configuration. If not provided, capabilities will be
* detected automatically on first use. Providing this configuration can
* improve performance by skipping detection and ensure consistent behavior.
*
* Recommended settings for common servers:
* - Nextcloud/ownCloud: { supportsETags: true, supportsIfHeader: true, supportsLocking: true, supportsLastModified: true }
* - Apache mod_dav: { supportsETags: true, supportsIfHeader: false, supportsLocking: true, supportsLastModified: true }
* - Basic WebDAV: { supportsETags: false, supportsIfHeader: false, supportsLocking: false, supportsLastModified: true }
*/
serverCapabilities?: WebdavServerCapabilities;
/**
* Force the use of Last-Modified headers instead of ETags, even if ETags are available.
* This can be useful for testing fallback behavior or working with servers that have
* unreliable ETag implementations.
*/
preferLastModified?: boolean;
/**
* Compatibility mode for servers with limited WebDAV support.
* When enabled, disables conditional operations and safe creation mechanisms.
* Use only for very basic WebDAV servers that don't support any conditional headers.
*/
basicCompatibilityMode?: boolean;
/**
* Maximum number of retry attempts for capability detection and fallback operations.
* Default: 2
*/
maxRetries?: number;
}
/**
* Server type enum for common WebDAV implementations
*/
export enum WebdavServerType {
NEXTCLOUD = 'nextcloud',
OWNCLOUD = 'owncloud',
APACHE_MOD_DAV = 'apache_mod_dav',
NGINX_DAV = 'nginx_dav',
BASIC_WEBDAV = 'basic_webdav',
CUSTOM = 'custom',
}
/**
* Helper function to get recommended server capabilities for common WebDAV server types
*/
export const getRecommendedServerCapabilities = (
serverType: WebdavServerType,
): WebdavServerCapabilities => {
switch (serverType) {
case WebdavServerType.NEXTCLOUD:
case WebdavServerType.OWNCLOUD:
return {
supportsETags: true,
supportsIfHeader: true,
supportsLocking: true,
supportsLastModified: true,
};
case WebdavServerType.APACHE_MOD_DAV:
return {
supportsETags: true,
supportsIfHeader: false, // mod_dav has limited If header support
supportsLocking: true,
supportsLastModified: true,
};
case WebdavServerType.NGINX_DAV:
return {
supportsETags: false, // nginx dav module has limited ETag support
supportsIfHeader: false,
supportsLocking: false,
supportsLastModified: true,
};
case WebdavServerType.BASIC_WEBDAV:
return {
supportsETags: false,
supportsIfHeader: false,
supportsLocking: false,
supportsLastModified: true,
};
case WebdavServerType.CUSTOM:
default:
// Return null to trigger auto-detection
return {
supportsETags: false,
supportsIfHeader: false,
supportsLocking: false,
supportsLastModified: false,
};
}
};
import { WebdavPrivateCfg } from './webdav.model';
export class Webdav implements SyncProviderServiceInterface<SyncProviderId.WebDAV> {
private static readonly L = 'Webdav';
@ -205,14 +91,13 @@ export class Webdav implements SyncProviderServiceInterface<SyncProviderId.WebDA
}
return { rev, dataStr };
} catch (e: any) {
} catch (e: unknown) {
// Handle 304 Not Modified by retrying without localRev
if (e?.status === 304) {
if (e && typeof e === 'object' && 'status' in e && e.status === 304) {
const { rev, dataStr } = await this._api.download({
path: filePath,
localRev: null,
});
if (!dataStr && dataStr !== '') {
throw new InvalidDataSPError(targetPath);
}