Provider Plugin implementieren
Basis Dokumente: Read Me, TransferX Architektur, TransferX Abstractions, TransferX Core
Dieser Leitfaden erklärt Schritt für Schritt, wie ein neues Provider-Plugin für TransferX erstellt wird.
Ein Provider kapselt den Zugriff auf ein Dateisystem oder einen Datendienst (z.B. WebDAV, FTP, lokales Dateisystem, SFTP, S3).
Inhaltsverzeichnis
- Überblick und Architektur
- Voraussetzungen und Abhängigkeiten
- Projektstruktur
- Schritt-für-Schritt-Anleitung
- Requests & Responses Referenz
- Models Referenz
- ProviderResult und Fehlerbehandlung
- Plugin-Discovery durch den ProviderLoader
- Vollständiges Beispiel: MyFtpProvider
- Checkliste
1. Überblick und Architektur
Provider-Plugins sind eigenständige .NET-Assemblies (.dll), die vom ProviderLoader im Core Layer dynamisch geladen werden.
Ein Plugin implementiert das IProvider-Interface aus TransferX.Provider.Abstractions und wird über das [ProviderMetadata]-Attribut für die automatische Discovery markiert.
graph TD
subgraph Core["⚙️ Core"]
PL["ProviderLoader"]
PE["ProviderEngine"]
end
subgraph Abstractions["📦 TransferX.Provider.Abstractions"]
IProvider["IProvider"]
IPCmd["IProviderCommand<TRequest,TResponse>"]
IPQry["IProviderQuery<TRequest,TResponse>"]
META["ProviderMetadataAttribute"]
end
subgraph Plugin["🔌 TransferX.Provider.MyFtp (Neues Plugin)"]
MyFtp["MyFtpProvider"]
UpCmd["UploadFileCommand"]
DlQry["DownloadFileQuery"]
end
PL -->|"scannt Assembly"| META
PL -->|"instanziiert"| MyFtp
PE -->|"ruft auf"| IProvider
MyFtp -->|"implementiert"| IProvider
UpCmd -->|"implementiert"| IPCmd
DlQry -->|"implementiert"| IPQry
MyFtp -->|"delegiert an"| UpCmd
MyFtp -->|"delegiert an"| DlQry
Ablauf: Provider Aufruf
sequenceDiagram
participant PE as ProviderEngine
participant PL as ProviderLoader
participant MP as MyFtpProvider
participant CMD as UploadFileCommand
PE->>PL: CreateProvider(assemblyPath, "MyFtp")
PL-->>PE: IProvider (MyFtpProvider)
PE->>MP: InitializeAsync(config, ct)
PE->>MP: ExecuteAsync(UploadFileRequest, progress, ct)
MP->>CMD: ExecuteAsync(request, progress, ct)
CMD-->>MP: UploadFileResponse
MP-->>PE: UploadFileResponse
2. Voraussetzungen und Abhängigkeiten
Das neue Plugin-Projekt benötigt ausschliesslich eine Referenz auf TransferX.Provider.Abstractions.
Keine Referenz auf Core, Application, Infrastructure oder Domain ist erlaubt oder nötig.
Die TransferX.Provider.Abstractions und TransferX.Provider.Domain gibt es als NuGet Package.
graph LR
Plugin["🔌 TransferX.Provider.MyFtp"]
PA["📦 TransferX.Provider.Abstractions"]
Domain["💎 TransferX.Domain"]
ExtLib["externe Bibliothek (z.B. FluentFTP)"]
Plugin -->|"Pflicht"| PA
Plugin -->|"transitiv"| Domain
Plugin -->|"optional"| ExtLib
.csproj Minimal-Konfiguration:
Beispiel: TransferX.Provider.MyFtp\TransferX.Provider.MyFtp.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<AssemblyName>TransferX.Provider.MyFtp</AssemblyName>
<RootNamespace>TransferX.Provider.MyFtp</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="TransferX.Provider.Abstractions" Version="2.0.0" />
</ItemGroup>
</Project>
3. Projektstruktur
Empfohlene Ordnerstruktur für ein neues Provider-Plugin:
TransferX.Provider.MyFtp
│ MyFtpProvider.cs ← IProvider Implementierung (Haupt-Einstiegspunkt)
│ TransferX.Provider.MyFtp.csproj
│
├───Commands
│ CreateFolderCommand.cs ← IProviderCommand (Ordner erstellen)
│ DeleteFileCommand.cs ← IProviderCommand (Datei löschen)
│ UploadFileCommand.cs ← IProviderCommand (Datei hochladen)
│
└───Queries
DownloadFileQuery.cs ← IProviderQuery (Datei herunterladen)
ListFilesQuery.cs ← IProviderQuery (Dateien auflisten)
ListFoldersQuery.cs ← IProviderQuery (Ordner auflisten)
4. Schritt-für-Schritt-Anleitung
4.1 Projekt erstellen
- Neues Class Library (.NET 8) Projekt erstellen:
TransferX.Provider.MyFtp - Referenz auf
TransferX.Provider.Abstractionshinzufügen (siehe Abschnitt 2)
4.2 ProviderMetadataAttribute setzen
Das [ProviderMetadata]-Attribut markiert die Klasse für die automatische Plugin-Discovery durch den ProviderLoader.
Ohne dieses Attribut wird das Plugin nicht gefunden und nicht geladen.
TransferX.Provider.MyFtp\MyFtpProvider.cs
// SOWI Informatik, www.sowi.ch
// Franz Schönbächler
using TransferX.Domain.ValueObjects.Progress;
using TransferX.Provider.Abstractions;
using TransferX.Provider.Abstractions.Contracts;
using TransferX.Provider.Abstractions.Metadata;
using TransferX.Provider.Abstractions.Models;
using TransferX.Provider.Abstractions.Requests;
using TransferX.Provider.MyFtp.Commands;
using TransferX.Provider.MyFtp.Queries;
namespace TransferX.Provider.MyFtp;
/// <summary>
/// FTP-Provider-Implementierung für TransferX.<br/>
/// Unterstützt die Standard-Provider-Operationen: Auflisten, Hoch- und Herunterladen, Ordner verwalten, Löschen.
/// </summary>
[ProviderMetadata]
public sealed class MyFtpProvider : IProvider
{
// ...
}
4.3 IProvider implementieren
IProvider schreibt drei Properties und zwei Methoden vor:
| Member | Pflicht | Beschreibung |
|---|---|---|
string Name |
✅ | Anzeigename, z.B. "My FTP Provider" |
string Version |
✅ | Versionsnummer, z.B. "1.0.0" |
string ProviderType |
✅ | Technischer Bezeichner, z.B. "MyFtp" – muss eindeutig sein |
InitializeAsync(config, ct) |
✅ | Verbindung herstellen, Credentials einlesen |
ExecuteAsync(request, progress, ct) |
✅ | Request entgegennehmen und an Command/Query delegieren |
Wichtig:
ProviderTypemuss systemweit eindeutig sein. DerProviderLoadersucht anhand dieses Strings das richtige Plugin.
4.4 Commands und Queries implementieren
Operations nach dem CQS-Muster (Command/Query Separation):
| Typ | Interface | Ändert Zustand? | Beispiele |
|---|---|---|---|
| Command | IProviderCommand<TRequest, TResponse> |
✅ Ja | Upload, Ordner erstellen, Löschen |
| Query | IProviderQuery<TRequest, TResponse> |
❌ Nein | Download, Ordner auflisten, Dateien auflisten |
graph LR
CMD["IProviderCommand<TRequest, TResponse>"]
QRY["IProviderQuery<TRequest, TResponse>"]
REQ["ProviderRequest (abstrakt)"]
RES["ProviderResponse (abstrakt)"]
CMD -->|"TRequest erbt von"| REQ
CMD -->|"TResponse erbt von"| RES
QRY -->|"TRequest erbt von"| REQ
QRY -->|"TResponse erbt von"| RES
Beispiel – Upload Command:
TransferX.Provider.MyFtp\Commands\UploadFileCommand.cs
// SOWI Informatik, www.sowi.ch
// Franz Schönbächler
using TransferX.Domain.ValueObjects.Progress;
using TransferX.Provider.Abstractions;
using TransferX.Provider.Abstractions.Requests;
using TransferX.Provider.Abstractions.Responses;
namespace TransferX.Provider.MyFtp.Commands;
/// <summary>
/// Lädt eine Datei per FTP zum Provider hoch.
/// </summary>
internal sealed class UploadFileCommand : IProviderCommand<UploadFileRequest, UploadFileResponse>
{
private readonly string _basePath;
/// <summary>Erstellt eine neue Instanz von <see cref="UploadFileCommand"/>.</summary>
/// <param name="basePath">Basis-URL oder Pfad des FTP-Servers.</param>
public UploadFileCommand(string basePath)
{
this._basePath = basePath;
}
/// <inheritdoc/>
public async Task<UploadFileResponse> ExecuteAsync(
UploadFileRequest request,
IProgress<FileProgress>? progress = null,
CancellationToken cancellationToken = default)
{
// FTP-spezifische Upload-Logik hier implementieren
await using var stream = await request.ContentFactory(cancellationToken);
// ... Datei hochladen, Fortschritt melden ...
return new UploadFileResponse
{
Success = true,
Path = request.TargetPath,
BytesTransferred = request.FileSize
};
}
}
Beispiel – List Files Query:
TransferX.Provider.MyFtp\Queries\ListFilesQuery.cs
// SOWI Informatik, www.sowi.ch
// Franz Schönbächler
using TransferX.Domain.ValueObjects.Progress;
using TransferX.Provider.Abstractions;
using TransferX.Provider.Abstractions.Models;
using TransferX.Provider.Abstractions.Requests;
using TransferX.Provider.Abstractions.Responses;
namespace TransferX.Provider.MyFtp.Queries;
/// <summary>
/// Listet Dateien in einem FTP-Verzeichnis auf.
/// </summary>
internal sealed class ListFilesQuery : IProviderQuery<ListFilesRequest, ListFilesResponse>
{
private readonly string _basePath;
/// <summary>Erstellt eine neue Instanz von <see cref="ListFilesQuery"/>.</summary>
/// <param name="basePath">Basis-URL oder Pfad des FTP-Servers.</param>
public ListFilesQuery(string basePath)
{
this._basePath = basePath;
}
/// <inheritdoc/>
public async Task<ListFilesResponse> ExecuteAsync(
ListFilesRequest request,
IProgress<FileProgress>? progress = null,
CancellationToken cancellationToken = default)
{
// FTP-spezifische Logik zum Auflisten von Dateien hier implementieren
var items = new List<FileItem>();
// ... FTP-Verzeichnis lesen, items befüllen ...
return new ListFilesResponse
{
Items = items,
SearchedPath = request.Path,
WasRecursive = request.Recursive
};
}
}
4.5 ExecuteAsync – Request-Dispatch
IProvider.ExecuteAsync empfängt alle Requests als ProviderRequest-Basistyp.
Die Implementierung muss via Pattern-Matching auf den konkreten Typ prüfen und an das zuständige Command oder Query delegieren.
Nicht unterstützte Request-Typen sollen eine
NotSupportedExceptionwerfen, damit der Aufrufer klar informiert wird.
/// <inheritdoc/>
public async Task<ProviderResponse> ExecuteAsync(
ProviderRequest request,
IProgress<FileProgress>? progress = null,
CancellationToken cancellationToken = default)
{
return request switch
{
ListFoldersRequest listFolders =>
await new ListFoldersQuery(this._basePath)
.ExecuteAsync(listFolders, progress, cancellationToken),
ListFilesRequest listFiles =>
await new ListFilesQuery(this._basePath)
.ExecuteAsync(listFiles, progress, cancellationToken),
DownloadFileRequest download =>
await new DownloadFileQuery(this._basePath)
.ExecuteAsync(download, progress, cancellationToken),
UploadFileRequest upload =>
await new UploadFileCommand(this._basePath)
.ExecuteAsync(upload, progress, cancellationToken),
CreateFolderRequest createFolder =>
await new CreateFolderCommand(this._basePath)
.ExecuteAsync(createFolder, progress, cancellationToken),
DeleteFileRequest deleteFile =>
await new DeleteFileCommand(this._basePath)
.ExecuteAsync(deleteFile, progress, cancellationToken),
_ => throw new NotSupportedException(
$"Request-Typ '{request.GetType().Name}' wird von {this.ProviderType} nicht unterstützt.")
};
}
4.6 Fortschritts-Tracking (IProgress)
IProgress<FileProgress> wird bei Operationen mit Datenübertragung genutzt (Upload, Download).
Der progress-Parameter kann null sein – immer prüfen vor dem Aufruf.
| Property | Typ | Beschreibung |
|---|---|---|
FileName |
string |
Name der aktuellen Datei |
FilePath |
string |
Vollständiger Pfad der Datei |
BytesTransferred |
long |
Bereits übertragene Bytes |
TotalBytes |
long |
Gesamtgrösse der Datei in Bytes |
ProgressPercent |
double |
Fortschritt in Prozent (0–100) |
Fortschritt im Upload melden:
// Fortschritt periodisch melden (z.B. bei jedem übertragenen Chunk)
progress?.Report(new FileProgress
{
FileName = Path.GetFileName(request.TargetPath),
FilePath = request.TargetPath,
BytesTransferred = bytesTransferred,
TotalBytes = request.FileSize
});
5. Requests & Responses Referenz
Alle Requests erben von ProviderRequest, alle Responses von ProviderResponse.
graph TD
PR["ProviderRequest (abstract record)"]
LFReq["ListFoldersRequest"]
LFiReq["ListFilesRequest"]
DLReq["DownloadFileRequest"]
ULReq["UploadFileRequest"]
CFReq["CreateFolderRequest"]
DFReq["DeleteFileRequest"]
PR --> LFReq
PR --> LFiReq
PR --> DLReq
PR --> ULReq
PR --> CFReq
PR --> DFReq
graph TD
PRS["ProviderResponse (abstract record)"]
LFRes["ListFoldersResponse"]
LFiRes["ListFilesResponse"]
DLRes["DownloadFileResponse"]
ULRes["UploadFileResponse"]
CFRes["CreateFolderResponse"]
DFRes["DeleteFileResponse"]
PRS --> LFRes
PRS --> LFiRes
PRS --> DLRes
PRS --> ULRes
PRS --> CFRes
PRS --> DFRes
Request/Response Übersicht
| Request | Response | CQS-Typ | Beschreibung |
|---|---|---|---|
ListFoldersRequest |
ListFoldersResponse |
Query | Ordner unter einem Pfad auflisten |
ListFilesRequest |
ListFilesResponse |
Query | Dateien unter einem Pfad auflisten |
DownloadFileRequest |
DownloadFileResponse |
Query | Datei herunterladen |
UploadFileRequest |
UploadFileResponse |
Command | Datei hochladen |
CreateFolderRequest |
CreateFolderResponse |
Command | Ordner erstellen |
DeleteFileRequest |
DeleteFileResponse |
Command | Datei löschen |
Request-Properties im Detail
ListFoldersRequest
| Property | Typ | Default | Beschreibung |
|---|---|---|---|
Path |
string |
– | Pfad, dessen Unterordner aufgelistet werden |
Recursive |
bool |
false |
Rekursive Auflistung aller Unterebenen |
ListFilesRequest
| Property | Typ | Default | Beschreibung |
|---|---|---|---|
Path |
string |
– | Pfad, dessen Dateien aufgelistet werden |
Recursive |
bool |
false |
Rekursive Auflistung aller Unterebenen |
DownloadFileRequest
| Property | Typ | Beschreibung |
|---|---|---|
Path |
string |
Vollständiger Pfad der herunterzuladenden Datei |
UploadFileRequest
| Property | Typ | Beschreibung |
|---|---|---|
TargetPath |
string |
Zielpfad inkl. Dateiname beim Provider |
ContentFactory |
Func<CancellationToken, Task<Stream>> |
Factory für den Upload-Stream (Retry-fähig) |
FileSize |
long |
Dateigrösse in Bytes (für Fortschritts-Tracking) |
ContentType |
string? |
Optionaler MIME-Typ, z.B. "application/pdf" |
Hinweis
ContentFactory: Den Stream immer über die Factory-Funktion beziehen – nicht einmalig speichern. Bei Retries wird die Factory erneut aufgerufen, um einen frischen Stream zu erhalten.
CreateFolderRequest
| Property | Typ | Default | Beschreibung |
|---|---|---|---|
Path |
string |
– | Vollständiger Pfad des zu erstellenden Ordners |
CreateParentFolders |
bool |
false |
Fehlende Elternordner automatisch erstellen |
IgnoreIfExists |
bool |
true |
Kein Fehler wenn Ordner bereits existiert |
DeleteFileRequest
| Property | Typ | Default | Beschreibung |
|---|---|---|---|
Path |
string |
– | Vollständiger Pfad der zu löschenden Datei |
IgnoreIfNotExists |
bool |
true |
Kein Fehler wenn Datei nicht gefunden |
Response-Properties im Detail
ListFoldersResponse
| Property | Typ | Beschreibung |
|---|---|---|
Items |
IReadOnlyList<FolderItem> |
Liste der gefundenen Ordner |
SearchedPath |
string |
Abgefragter Pfad |
TotalFolders |
int |
Anzahl gefundener Ordner (berechnet) |
WasRecursive |
bool |
Gibt an, ob rekursiv abgefragt wurde |
ListFilesResponse
| Property | Typ | Beschreibung |
|---|---|---|
Items |
IReadOnlyList<FileItem> |
Liste der gefundenen Dateien |
SearchedPath |
string |
Abgefragter Pfad |
TotalFiles |
int |
Anzahl gefundener Dateien (berechnet) |
TotalSizeBytes |
long |
Gesamtgrösse aller Dateien (berechnet) |
WasRecursive |
bool |
Gibt an, ob rekursiv abgefragt wurde |
DownloadFileResponse
| Property | Typ | Beschreibung |
|---|---|---|
Stream |
Stream |
Datei-Stream – der Aufrufer ist für das Schliessen verantwortlich |
FileSize |
long |
Dateigrösse in Bytes |
ContentType |
string? |
MIME-Typ der Datei, null wenn nicht verfügbar |
UploadFileResponse
| Property | Typ | Beschreibung |
|---|---|---|
Success |
bool |
Upload erfolgreich |
Path |
string |
Vollständiger Pfad beim Provider |
BytesTransferred |
long |
Tatsächlich übertragene Bytes |
CreateFolderResponse
| Property | Typ | Beschreibung |
|---|---|---|
Success |
bool |
Operation erfolgreich |
Path |
string |
Vollständiger Pfad des Ordners |
WasCreated |
bool |
true = neu erstellt, false = bereits vorhanden |
ParentFoldersCreated |
int |
Anzahl automatisch erstellter Elternordner |
DeleteFileResponse
| Property | Typ | Beschreibung |
|---|---|---|
Success |
bool |
Operation erfolgreich |
Path |
string |
Vollständiger Pfad der betroffenen Datei |
WasDeleted |
bool |
true = tatsächlich gelöscht |
WasNotFound |
bool |
true = Datei war nicht vorhanden |
6. Models Referenz
graph TD
CI["ContentItem (abstract record)"]
FI["FileItem (sealed record)"]
FoI["FolderItem (sealed record)"]
SI["StorageItem (sealed record)"]
PCI["ProviderConfigItem (sealed record)"]
PC["ProviderCredentials (sealed record)"]
CI --> FI
CI --> FoI
PCI -->|"enthält optional"| PC
ContentItem – Basisklasse für Dateisystem-Einträge
| Property | Typ | Beschreibung |
|---|---|---|
Name |
string |
Name des Eintrags (ohne Pfad) |
Path |
string |
Vollständiger Pfad beim Provider |
LastModified |
DateTime? |
Letzte Änderung (UTC), null wenn Provider keine Zeitinfo liefert |
FileItem – Datei
| Property | Typ | Beschreibung |
|---|---|---|
Size |
long |
Dateigrösse in Bytes |
ContentType |
string? |
MIME-Typ, null wenn nicht bekannt |
FolderItem – Ordner
| Property | Typ | Beschreibung |
|---|---|---|
HasChildren |
bool |
Gibt an, ob Unterordner vorhanden sind |
StorageItem – Speicherinformationen
| Property | Typ | Beschreibung |
|---|---|---|
TotalBytes |
long |
Gesamtkapazität in Bytes |
FreeBytes |
long |
Freier Speicher in Bytes |
UsedBytes |
long |
Verwendeter Speicher (berechnet: Total - Free) |
ProviderConfigItem – Konfiguration
Wird in InitializeAsync übergeben. Enthält alle nötigen Verbindungsparameter.
| Property | Typ | Beschreibung |
|---|---|---|
Id |
Guid |
Eindeutige ID der Konfiguration |
Name |
string |
Anzeigename |
ProviderType |
string |
Technischer Typ, z.B. "MyFtp" |
BasePath |
string |
Basis-URL oder Pfad, z.B. "ftp://meinserver.ch" |
Credentials |
ProviderCredentials? |
Zugangsdaten, null wenn keine Authentifizierung |
ProviderCredentials – Zugangsdaten
| Property | Typ | Beschreibung |
|---|---|---|
Username |
string |
Benutzername |
Password |
string |
Passwort (in ToString() maskiert als Username:***) |
7. ProviderResult und Fehlerbehandlung
ProviderResult kapselt den Erfolg oder Misserfolg einer Operation. Factory-Methoden vereinfachen die Erstellung:
// Erfolgreich
var ok = ProviderResult.Ok();
// Fehler mit Meldung
var fail = ProviderResult.Fail("Verbindung zum FTP-Server fehlgeschlagen.");
// Fehler mit Ausnahme
var failEx = ProviderResult.Fail("Verbindung fehlgeschlagen.", exception);
Empfohlene Fehlerbehandlung in Commands/Queries
- Bekannte Fehler (z.B. Datei nicht gefunden, Verbindungsfehler):
ProviderResult.Fail(...)verwenden, keine Exception werfen. - Unbekannte Fehler / Programmierfehler: Exception propagieren lassen.
- Abbruch:
CancellationTokenprüfen undOperationCanceledExceptionpropagieren lassen. - Nicht unterstützte Requests:
NotSupportedExceptionwerfen.
public async Task<UploadFileResponse> ExecuteAsync(
UploadFileRequest request,
IProgress<FileProgress>? progress = null,
CancellationToken cancellationToken = default)
{
try
{
cancellationToken.ThrowIfCancellationRequested();
// ... Upload-Logik ...
return new UploadFileResponse { Success = true, Path = request.TargetPath, BytesTransferred = request.FileSize };
}
catch (OperationCanceledException)
{
throw; // Abbruch propagieren
}
catch (Exception ex)
{
// Fehler als Response zurückgeben (kein unbehandelter Crash)
return new UploadFileResponse { Success = false, Path = request.TargetPath, BytesTransferred = 0 };
// Alternativ: Exception für den Core loggen lassen
}
}
8. Plugin-Discovery durch den ProviderLoader
Der ProviderLoader im Core Layer scannt Plugin-Assemblies automatisch nach Klassen, die:
- Das
[ProviderMetadata]-Attribut tragen IProviderimplementieren- Einen parameterlosen Konstruktor besitzen
graph LR
PL["ProviderLoader"]
PLCtx["PluginLoadContext (isoliert)"]
DLL["TransferX.Provider.MyFtp.dll"]
REFL["Reflection (GetExportedTypes)"]
META["ProviderMetadataAttribute"]
IPROV["IProvider"]
INFO["ProviderPluginInfo"]
PL -->|"erstellt"| PLCtx
PLCtx -->|"lädt"| DLL
DLL -->|"scannt"| REFL
REFL -->|"prüft auf"| META
REFL -->|"prüft auf"| IPROV
REFL -->|"erzeugt"| INFO
INFO -->|"zurück an"| PL
Deployment
Die fertig kompilierte Plugin-Assembly (.dll) inkl. aller transitiven Abhängigkeiten muss im Plugin-Verzeichnis des Hosts abgelegt werden:
./plugins/providers/
TransferX.Provider.MyFtp.dll
FluentFTP.dll ← transitive Abhängigkeiten des Plugins
...
Wichtig:
TransferX.Provider.Abstractions.dllundTransferX.Domain.dllwerden nicht mitgeliefert – diese werden vom Host (Core) bereitgestellt und geteilt.
9. Vollständiges Beispiel: MyFtp Provider
Das folgende Beispiel zeigt eine vollständige, minimale IProvider-Implementierung.
TransferX.Provider.MyFtp\MyFtpProvider.cs
// SOWI Informatik, www.sowi.ch
// Franz Schönbächler
using TransferX.Domain.ValueObjects.Progress;
using TransferX.Provider.Abstractions;
using TransferX.Provider.Abstractions.Contracts;
using TransferX.Provider.Abstractions.Metadata;
using TransferX.Provider.Abstractions.Models;
using TransferX.Provider.Abstractions.Requests;
using TransferX.Provider.MyFtp.Commands;
using TransferX.Provider.MyFtp.Queries;
namespace TransferX.Provider.MyFtp;
/// <summary>
/// FTP-Provider-Implementierung für TransferX.<br/>
/// Unterstützt: Auflisten von Ordnern und Dateien, Hoch- und Herunterladen,<br/>
/// Erstellen und Löschen von Ordnern und Dateien.
/// </summary>
[ProviderMetadata]
public sealed class MyFtpProvider : IProvider
{
private string _basePath = string.Empty;
private ProviderCredentials? _credentials;
/// <inheritdoc/>
public string Name => "My FTP Provider";
/// <inheritdoc/>
public string Version => "1.0.0";
/// <inheritdoc/>
public string ProviderType => "MyFtp";
/// <inheritdoc/>
public Task InitializeAsync(ProviderConfigItem config, CancellationToken cancellationToken = default)
{
this._basePath = config.BasePath;
this._credentials = config.Credentials;
// Verbindungsaufbau / Validierung hier implementieren
return Task.CompletedTask;
}
/// <inheritdoc/>
public async Task<ProviderResponse> ExecuteAsync(
ProviderRequest request,
IProgress<FileProgress>? progress = null,
CancellationToken cancellationToken = default)
{
return request switch
{
ListFoldersRequest listFolders =>
await new ListFoldersQuery(this._basePath, this._credentials)
.ExecuteAsync(listFolders, progress, cancellationToken),
ListFilesRequest listFiles =>
await new ListFilesQuery(this._basePath, this._credentials)
.ExecuteAsync(listFiles, progress, cancellationToken),
DownloadFileRequest download =>
await new DownloadFileQuery(this._basePath, this._credentials)
.ExecuteAsync(download, progress, cancellationToken),
UploadFileRequest upload =>
await new UploadFileCommand(this._basePath, this._credentials)
.ExecuteAsync(upload, progress, cancellationToken),
CreateFolderRequest createFolder =>
await new CreateFolderCommand(this._basePath, this._credentials)
.ExecuteAsync(createFolder, progress, cancellationToken),
DeleteFileRequest deleteFile =>
await new DeleteFileCommand(this._basePath, this._credentials)
.ExecuteAsync(deleteFile, progress, cancellationToken),
_ => throw new NotSupportedException(
$"Request-Typ '{request.GetType().Name}' wird von {this.ProviderType} nicht unterstützt.")
};
}
}
10. Checkliste
Vor dem Deployment des neuen Providers alle Punkte prüfen:
| # | Aufgabe | Erledigt |
|---|---|---|
| 1 | [ProviderMetadata]-Attribut an der Provider-Klasse gesetzt |
☐ |
| 2 | IProvider vollständig implementiert (Name, Version, ProviderType, InitializeAsync, ExecuteAsync) |
☐ |
| 3 | ProviderType ist systemweit eindeutig (z.B. "MyFtp", "MySftp") |
☐ |
| 4 | Alle 6 Standard-Operationen im ExecuteAsync-Switch behandelt |
☐ |
| 5 | default-Fall im Switch wirft NotSupportedException |
☐ |
| 6 | IProgress<FileProgress> wird bei Upload und Download korrekt gemeldet |
☐ |
| 7 | progress-Parameter wird vor dem Aufruf auf null geprüft (progress?.Report(...)) |
☐ |
| 8 | CancellationToken wird in allen asynchronen Methoden weitergereicht |
☐ |
| 9 | UploadFileRequest.ContentFactory wird bei jedem Versuch neu aufgerufen (Retry-Unterstützung) |
☐ |
| 10 | DownloadFileResponse.Stream wird korrekt zurückgegeben (Aufrufer schliesst den Stream) |
☐ |
| 11 | ProviderCredentials werden sicher behandelt (kein Logging des Passworts) |
☐ |
| 12 | Datei-Header in jeder .cs-Datei vorhanden |
☐ |
| 13 | XML-Kommentare (///) für alle public Members vorhanden |
☐ |
| 14 | Plugin-Assembly inkl. eigener Abhängigkeiten im Plugin-Verzeichnis deployt | ☐ |
| 15 | TransferX.Provider.Abstractions.dll nicht im Plugin-Verzeichnis mitgeliefert |
☐ |