# Dateien lesen und schreiben

> Anleitung: Auf Datei-Nodes eines Servers zugreifen: Inhalte lesen und anhängen, Dateien als Stream öffnen, Rechte prüfen und Fehler behandeln.

**Beispielanwendungen:**

- Server.CustomFile ([C#](https://github.com/Traeger-GmbH/opcuanet-samples/tree/master/cs/Scenarios/Server.CustomFile), [VB.NET](https://github.com/Traeger-GmbH/opcuanet-samples/tree/master/vb/Scenarios/Server.CustomFile)): Stellt über die Schnittstelle IOpcFileInfo eine Datei aus einer eigenen Quelle als Datei-Node bereit, hier von einem FTP-Server.

Beteiligte Typen: `OpcClient`, `OpcNodeId`, `OpcFile`, `OpcFileMode`, `OpcFileStream`, `OpcFileInfo`, `OpcFileMethods`, `SafeOpcFileHandle`

Nodes vom Typ FileType haben laut OPC-UA-Spezifikation festgelegte Eigenschaften (Property-Nodes) und Methoden (Methoden-Nodes). Über sie greift der Client auf einen Datenstrom so zu wie auf eine Datei im Dateisystem. Der Node stellt dabei nur Informationen über den Inhalt der logischen oder physischen Datei bereit. Einen eventuell vorhandenen Pfad zur Datei stellt er laut Spezifikation nicht bereit. Auf die Datei selbst greift der Client über die Methoden Open, Close, Read, Write, `GetPosition` und `SetPosition` zu. Diese Methoden verarbeiten die Daten immer binär. Wie auf jeder anderen Plattform geben Sie auch in OPC UA beim Aufruf von Open einen Modus an, der die Art des geplanten Dateizugriffs festlegt. Auch in OPC UA können Sie exklusiven Zugriff auf eine Datei anfordern. Nach dem Aufruf von Open erhält der Client einen numerischen Schlüssel für den weiteren Dateizugriff (engl. File Handle). Diesen Schlüssel übergeben Sie bei den Methoden Read, Write, `GetPosition` und `SetPosition` immer mit. Schließen Sie eine geöffnete Datei mit Close, sobald Sie sie nicht mehr benötigen.

Auf Nodes vom Typ FileType können Sie manuell über die Methoden `ReadNode` und `CallMethod` der Klasse `OpcClient` zugreifen. Alternativ bietet das SDK zahlreiche weitere Klassen, die den Zugriff auf solche Nodes unter anderem nach dem Vorbild von .NET umsetzen. Die NodeId legt fest, auf welchen Datei-Node der Client zugreift.

## Die passende Klasse wählen

| Klasse | Vorbild in .NET | Geeignet für |
|---|---|---|
| `OpcFile` | `System.IO.File` | ganze Inhalte in einem Aufruf lesen oder anhängen (`ReadAllText`, `ReadAllLines`, `ReadAllBytes`, `AppendAllText`, `AppendAllLines`) und Streams öffnen (`Open`, `OpenRead`, `OpenWrite`, `OpenText`, `AppendText`) |
| `OpcFileInfo` (Namespace `Opc.UaFx.Client`) | `System.IO.FileInfo` | Eigenschaften einer Datei abfragen (`Exists`, `Length`, `Name`, `MimeType`, `CanWrite`, `CanUserWrite`) und die Datei öffnen |
| `OpcFileStream` | `System.IO.FileStream` | Lesen, Schreiben und Positionieren über die Schnittstelle von `Stream`, auch zusammen mit `StreamReader`, `StreamWriter` oder `CopyTo` |
| `OpcFileMethods` | keines | die Methoden des Datei-Nodes (`Open`, `Read`, `Write`, `GetPosition`, `SetPosition`, `Close`) einzeln aufrufen |

Den Modus beim Öffnen legt `OpcFileMode` fest: `Read`, `Write`, `ReadWrite`, `EraseExisting` und `Truncate` (Schreiben nach Leeren der Datei). Methoden zum Überschreiben einer ganzen Datei in einem Aufruf (wie `File.WriteAllText`) bietet `OpcFile` nicht. Zum Schreiben verwenden Sie einen mit `OpenWrite` oder `Open` geöffneten Stream.

## Ganze Dateien lesen und schreiben

**Gesamten Inhalt einer Textdatei lesen**:

```csharp
string reportText = OpcFile.ReadAllText(client, "ns=2;s=Machine/Report");

```

```vbnet
Dim reportText As String = OpcFile.ReadAllText(client, "ns=2;s=Machine/Report")

```

**Weitere Textdaten an eine Textdatei anhängen**:

```csharp
OpcFile.AppendAllText(client, "ns=2;s=Machine/Report", "Lorem ipsum");

```

```vbnet
OpcFile.AppendAllText(client, "ns=2;s=Machine/Report", "Lorem ipsum")

```

**Datei mit `OpcFileStream` öffnen und lesen**:

```csharp
using (var stream = OpcFile.OpenRead(client, "ns=2;s=Machine/Report")) {
    var reader = new StreamReader(stream);
 
    while (!reader.EndOfStream)
        Console.WriteLine(reader.ReadLine());
}

```

```vbnet
Using stream = OpcFile.OpenRead(client, "ns=2;s=Machine/Report")
    Dim reader = New StreamReader(stream)

    While Not reader.EndOfStream
        Console.WriteLine(reader.ReadLine())
    End While
End Using
```

**Datei mit `OpcFileStream` öffnen und schreiben**:

```csharp
using (var stream = OpcFile.OpenWrite(client, "ns=2;s=Machine/Report")) {
    var writer = new StreamWriter(stream);
 
    writer.WriteLine("Lorem ipsum");
    writer.WriteLine("dolor sit");
    // ...
}
```

```vbnet
Using stream = OpcFile.OpenWrite(client, "ns=2;s=Machine/Report")
    Dim writer = New StreamWriter(stream)

    writer.WriteLine("Lorem ipsum")
    writer.WriteLine("dolor sit")
    '...
End Using
```

**Den gesamten Inhalt als Zeilen oder als Bytes lesen**:

```csharp
string[] lines = OpcFile.ReadAllLines(client, "ns=2;s=Machine/Recipe");
byte[] content = OpcFile.ReadAllBytes(client, "ns=2;s=Machine/Snapshot");
```

```vbnet
Dim lines As String() = OpcFile.ReadAllLines(client, "ns=2;s=Machine/Recipe")
Dim content As Byte() = OpcFile.ReadAllBytes(client, "ns=2;s=Machine/Snapshot")
```

## Dateiinformationen abfragen und öffnen

Die Klasse `OpcFileInfo` des Namespace `Opc.UaFx.Client` beschreibt eine einzelne Datei. Auch der Namespace `Opc.UaFx` enthält eine Klasse `OpcFileInfo`. Die folgenden Beispiele geben den Typnamen deshalb voll qualifiziert an:

**Instanz von `OpcFileInfo` erstellen**:

```csharp
// The type name is qualified, because the Opc.UaFx namespace declares an OpcFileInfo too.
var file = new Opc.UaFx.Client.OpcFileInfo(client, "ns=2;s=Machine/Report");
```

```vbnet
'The type name is qualified, because the Opc.UaFx namespace declares an OpcFileInfo too.
Dim file = New Opc.UaFx.Client.OpcFileInfo(client, "ns=2;s=Machine/Report")
```

**Mit der Instanz von `OpcFileInfo` arbeiten**:

```csharp
if (file.Exists) {
    Console.WriteLine("File Length: {0}", file.Length);
 
    if (file.CanUserWrite) {
        using (var stream = file.OpenWrite()) {
            // Your code to write via stream.
        }
    }
    else {
        using (var stream = file.OpenRead()) {
            // Your code to read via stream.
        }
    }
}
```

```vbnet
If file.Exists Then
    Console.WriteLine("File Length: {0}", file.Length)

    If file.CanUserWrite Then
        Using stream = file.OpenWrite()
            'Your code to write via stream.
        End Using
    Else
        Using stream = file.OpenRead()
            'Your code to read via stream.
        End Using
    End If
End If

```

**Eigenschaften abfragen und nur schreiben, wenn der Benutzer es darf**: `CanWrite` gibt an, ob die Datei grundsätzlich beschreibbar ist. `CanUserWrite` gibt an, ob der angemeldete Benutzer sie beschreiben darf. Ein zum Schreiben geöffneter `OpcFileStream` berücksichtigt beides. Seine Eigenschaft `CanWrite` ist nur dann `true`, wenn beides zutrifft:

```csharp
var file = new Opc.UaFx.Client.OpcFileInfo(client, "ns=2;s=Machine/Recipe");
 
Console.WriteLine(
        "{0}: {1} bytes, {2}",
        file.Name,
        file.Length,
        file.MimeType);
 
if (file.Exists && file.CanUserWrite) {
    using (var stream = file.OpenWrite())
    using (var writer = new StreamWriter(stream)) {
        writer.Write("Speed=1200");
    }
}

```

```vbnet
Dim file = New Opc.UaFx.Client.OpcFileInfo(client, "ns=2;s=Machine/Recipe")

Console.WriteLine( _
        "{0}: {1} bytes, {2}", _
        file.Name, _
        file.Length, _
        file.MimeType)

If file.Exists AndAlso file.CanUserWrite Then
    Using stream = file.OpenWrite()
        Using writer = New StreamWriter(stream)
            writer.Write("Speed=1200")
        End Using
    End Using
End If
```

## Große Dateien als Stream verarbeiten

**Große Dateien kopieren**: Da `OpcFileStream` von `Stream` abgeleitet ist, kopieren Sie eine Datei des Servers mit `CopyTo` in eine lokale Datei. Der Stream liest den Inhalt dabei abschnittsweise:

```csharp
using (var source = OpcFile.OpenRead(client, "ns=2;s=Machine/Snapshot"))
using (var target = File.Create("snapshot.bin")) {
    source.CopyTo(target);
}

```

```vbnet
Using source = OpcFile.OpenRead(client, "ns=2;s=Machine/Snapshot")
    Using target = IO.File.Create("snapshot.bin")
        source.CopyTo(target)
    End Using
End Using
```

**An eine bestimmte Position schreiben**: Mit `Seek` legen Sie die Position innerhalb der Datei fest, zum Beispiel ans Ende, um einen Eintrag anzuhängen:

```csharp
using (var stream = OpcFile.Open(client, "ns=2;s=Machine/Log", OpcFileMode.ReadWrite)) {
    stream.Seek(0, SeekOrigin.End);
 
    byte[] entry = Encoding.UTF8.GetBytes("Job started" + Environment.NewLine);
    stream.Write(entry, 0, entry.Length);
}
```

```vbnet
Using stream = OpcFile.Open(client, "ns=2;s=Machine/Log", OpcFileMode.ReadWrite)
    stream.Seek(0, SeekOrigin.End)

    Dim entry As Byte() = Encoding.UTF8.GetBytes("Job started" & Environment.NewLine)
    stream.Write(entry, 0, entry.Length)
End Using
```

## Dateimethoden direkt aufrufen

**Über das .NET-Konzept `SafeHandle` (Klasse `SafeOpcFileHandle`)**:

```csharp
using (var handle = OpcFileMethods.SecureOpen(client, "ns=2;s=Machine/Report", OpcFileMode.ReadWrite)) {
    byte[] data = OpcFileMethods.SecureRead(handle, 100);
 
    long position = OpcFileMethods.SecureGetPosition(handle);
    OpcFileMethods.SecureSetPosition(handle, position + data[data.Length - 1]);
 
    OpcFileMethods.SecureWrite(handle, new byte[] { 1, 2, 3 });
}

```

```vbnet
Using handle = OpcFileMethods.SecureOpen(client, "ns=2;s=Machine/Report", OpcFileMode.ReadWrite)
    Dim data As Byte() = OpcFileMethods.SecureRead(handle, 100)

    Dim position As Long = OpcFileMethods.SecureGetPosition(handle)
    OpcFileMethods.SecureSetPosition(handle, position + data(data.Length - 1))

    OpcFileMethods.SecureWrite(handle, New Byte() {1, 2, 3})
End Using

```

**Über einen numerischen File Handle**:

```csharp
uint handle = OpcFileMethods.Open(client, "ns=2;s=Machine/Report", OpcFileMode.ReadWrite);
 
try {
    byte[] data = OpcFileMethods.Read(client, "ns=2;s=Machine/Report", handle, 100);
 
    ulong position = OpcFileMethods.GetPosition(client, "ns=2;s=Machine/Report", handle);
    OpcFileMethods.SetPosition(client, "ns=2;s=Machine/Report", handle, position + data[data.Length - 1]);
 
    OpcFileMethods.Write(client, "ns=2;s=Machine/Report", handle, new byte[] { 1, 2, 3 });
}
finally {
    OpcFileMethods.Close(client, "ns=2;s=Machine/Report", handle);
}

```

```vbnet
Dim handle As UInteger = OpcFileMethods.Open(client, "ns=2;s=Machine/Report", OpcFileMode.ReadWrite)

Try
    Dim data As Byte() = OpcFileMethods.Read(client, "ns=2;s=Machine/Report", handle, 100)

    Dim position As ULong = OpcFileMethods.GetPosition(client, "ns=2;s=Machine/Report", handle)
    OpcFileMethods.SetPosition(client, "ns=2;s=Machine/Report", handle, position + data(data.Length - 1))

    OpcFileMethods.Write(client, "ns=2;s=Machine/Report", handle, New Byte() {1, 2, 3})
Finally
    OpcFileMethods.Close(client, "ns=2;s=Machine/Report", handle)
End Try

```

Nur Dateizugriffe über `OpcFile`, `OpcFileInfo`, `OpcFileStream` und `SafeOpcFileHandle` garantieren, dass eine geöffnete Datei auch dann freigegeben wird, wenn der Aufruf der Methode Close fehlt. Spätestens beim Schließen der Verbindung zum Server schließt der `OpcClient` dann alle geöffneten Dateien automatisch. Das gilt nicht, wenn Sie die Methoden der Klasse `OpcFileMethods` ohne das Präfix `Secure` verwenden.

Die OPC-UA-Spezifikation definiert keinen Weg, einen Node als Node vom Typ FileType und damit als Datei-Node zu erkennen. Das SDK kann einen Datei-Node deshalb anhand seiner Node-Struktur identifizieren:

```csharp
if (OpcFileMethods.IsFileNode(client, "ns=2;s=Machine/Report")) {
    // Your code to operate on the file node.
}

```

```vbnet
If OpcFileMethods.IsFileNode(client, "ns=2;s=Machine/Report") Then
    'Your code to operate on the file node.
End If

```

## Fehler behandeln

Wenn der angegebene Node nicht existiert oder kein Datei-Node ist, lösen die Methoden von `OpcFile`, `OpcFileInfo` und `OpcFileMethods` eine `InvalidOperationException` aus. Wenn der Server einen Methodenaufruf ablehnt, löst die Methode eine `OpcException` mit dem Status des Servers aus. Das ist zum Beispiel der Fall, wenn der Benutzer die Datei nicht beschreiben darf oder die Datei bereits exklusiv geöffnet ist. Operationen, die der Modus des Streams nicht zulässt (zum Beispiel Schreiben in einen zum Lesen geöffneten Stream), quittiert `OpcFileStream` mit einer `NotSupportedException`:

```csharp
try {
    string recipe = OpcFile.ReadAllText(client, "ns=2;s=Machine/Recipe");
}
catch (InvalidOperationException) {
    // The node does not exist or is not a file node.
}
catch (OpcException ex) {
    // The server rejected a file method, e.g. with BadUserAccessDenied.
    Console.WriteLine("{0}: {1}", ex.Code, ex.Message);
}
```

```vbnet
Try
    Dim recipe As String = OpcFile.ReadAllText(client, "ns=2;s=Machine/Recipe")
Catch ex As InvalidOperationException
    'The node does not exist or is not a file node.
Catch ex As OpcException
    'The server rejected a file method, e.g. with BadUserAccessDenied.
    Console.WriteLine("{0}: {1}", ex.Code, ex.Message)
End Try

```

## Sicherheit

Behandeln Sie Inhalte, die Sie von einem Server lesen, wie Eingaben von außen. Verwenden Sie zum Beispiel Dateinamen oder Pfade aus einer gelesenen Datei nicht ungeprüft für Zugriffe auf das lokale Dateisystem. Begrenzen Sie die Größe (Eigenschaft `Length`), bevor Sie eine Datei vollständig in den Speicher lesen. Prüfen Sie vor dem Schreiben die Eigenschaft `CanUserWrite`.

## Abgrenzung

- Große Binärwerte, die ein Server nicht als Datei, sondern als Wert einer Variablen bereitstellt, liest die Klasse `OpcNodeStream` (siehe [Große Binärwerte als Stream](https://www.traeger.de/docs/development/opcua/client/node-types#gro%C3%9Fe-binarwerte-als-stream)).
- Wie ein Server selbst Datei-Nodes bereitstellt, beschreibt die Seite [Node-Typen](https://www.traeger.de/docs/development/opcua/server/node-types) im Server-Kapitel.
