Repository: deepset-ai/haystack

Issue:
Add file abstraction
**Is your feature request related to a problem? Please describe.**
If you wanted to implement something like what's described in #5614, you would need to connect a `LinkContentFetcher` to some file converter like `TikaConverter`. `TikaConverter` accepts `list[Path]` whereas `LinkContentFetcher` returns `list[Document]`.
Document does not seem to be a good return type of `LinkContentFetcher` in the case of binary blobs because a `Document` is usually handled like something that's already processed. For example, you would not want to store this kind of document in a doc store nor use it with any other component accepting `Document`.

**Describe the solution you'd like**
There should be some kind of file abstraction. One possibility would be having a `File` class which looks something like this:

```py
class File:
    data: Optional[blob]
    path: Optional[Path]
    def get_blob(self) -> blob:
        if not self.data:
           self.data = load(self.path)
        return self.data
    def get_path(self) -> Path:
        if not path:
            self.path = store(self.data)
        return self.path
```


Discussion:

Resolving patch:
diff --git a/haystack/preview/dataclasses/__init__.py b/haystack/preview/dataclasses/__init__.py
--- a/haystack/preview/dataclasses/__init__.py
+++ b/haystack/preview/dataclasses/__init__.py
@@ -1,4 +1,5 @@
 from haystack.preview.dataclasses.document import Document
 from haystack.preview.dataclasses.answer import ExtractedAnswer, GeneratedAnswer, Answer
+from haystack.preview.dataclasses.byte_stream import ByteStream
 
-__all__ = ["Document", "ExtractedAnswer", "GeneratedAnswer", "Answer"]
+__all__ = ["Document", "ExtractedAnswer", "GeneratedAnswer", "Answer", "ByteStream"]
diff --git a/haystack/preview/dataclasses/byte_stream.py b/haystack/preview/dataclasses/byte_stream.py
new file mode 100644
--- /dev/null
+++ b/haystack/preview/dataclasses/byte_stream.py
@@ -0,0 +1,37 @@
+from dataclasses import dataclass, field
+from pathlib import Path
+from typing import Dict, Any
+
+
+@dataclass(frozen=True)
+class ByteStream:
+    """
+    Base data class representing a binary object in the Haystack API.
+    """
+
+    data: bytes
+    metadata: Dict[str, Any] = field(default_factory=dict, hash=False)
+
+    def to_file(self, destination_path: Path):
+        with open(destination_path, "wb") as fd:
+            fd.write(self.data)
+
+    @classmethod
+    def from_file_path(cls, filepath: Path) -> "ByteStream":
+        """
+        Create a ByteStream from the contents read from a file.
+
+        :param filepath: A valid path to a file.
+        """
+        with open(filepath, "rb") as fd:
+            return cls(data=fd.read())
+
+    @classmethod
+    def from_string(cls, text: str, encoding: str = "utf-8") -> "ByteStream":
+        """
+        Create a ByteStream encoding a string.
+
+        :param text: The string to encode
+        :param encoding: The encoding used to convert the string into bytes
+        """
+        return cls(data=text.encode(encoding))


Test patch:
diff --git a/test/preview/dataclasses/test_byte_stream.py b/test/preview/dataclasses/test_byte_stream.py
new file mode 100644
--- /dev/null
+++ b/test/preview/dataclasses/test_byte_stream.py
@@ -0,0 +1,33 @@
+import io
+
+from haystack.preview.dataclasses import ByteStream
+
+import pytest
+
+
+@pytest.mark.unit
+def test_from_file_path(tmp_path, request):
+    test_bytes = "Hello, world!\n".encode()
+    test_path = tmp_path / request.node.name
+    with open(test_path, "wb") as fd:
+        assert fd.write(test_bytes)
+
+    b = ByteStream.from_file_path(test_path)
+    assert b.data == test_bytes
+
+
+@pytest.mark.unit
+def test_from_string():
+    test_string = "Hello, world!"
+    b = ByteStream.from_string(test_string)
+    assert b.data.decode() == test_string
+
+
+@pytest.mark.unit
+def test_to_file(tmp_path, request):
+    test_str = "Hello, world!\n"
+    test_path = tmp_path / request.node.name
+
+    ByteStream(test_str.encode()).to_file(test_path)
+    with open(test_path, "rb") as fd:
+        assert fd.read().decode() == test_str