diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,5 @@
+# Revision history for file-io
+
+## 0.1.0.0 -- YYYY-mm-dd
+
+* First version. Released on an unsuspecting world.
diff --git a/System/File/Common.hs b/System/File/Common.hs
new file mode 100644
--- /dev/null
+++ b/System/File/Common.hs
@@ -0,0 +1,82 @@
+
+-- | Like 'openFile', but open the file in binary mode.
+-- On Windows, reading a file in text mode (which is the default)
+-- will translate CRLF to LF, and writing will translate LF to CRLF.
+-- This is usually what you want with text files.  With binary files
+-- this is undesirable; also, as usual under Microsoft operating systems,
+-- text mode treats control-Z as EOF.  Binary mode turns off all special
+-- treatment of end-of-line and end-of-file characters.
+-- (See also 'System.IO.hSetBinaryMode'.)
+
+-- On POSIX systems, 'openBinaryFile' is an /interruptible operation/ as
+-- described in "Control.Exception".
+openBinaryFile :: FILE_PATH -> IOMode -> IO Handle
+openBinaryFile fp iomode = do
+  h <- openFile fp iomode
+  hSetBinaryMode h True
+  pure h
+
+-- | Run an action on a file.
+--
+-- The 'Handle' is automatically closed afther the action.
+withFile :: FILE_PATH -> IOMode -> (Handle -> IO r) -> IO r
+withFile fp iomode action = bracket
+  (openFile fp iomode)
+  hClose
+  action
+
+withBinaryFile :: FILE_PATH -> IOMode -> (Handle -> IO r) -> IO r
+withBinaryFile fp iomode action = bracket
+  (openBinaryFile fp iomode)
+  hClose
+  action
+
+-- | Run an action on a file.
+--
+-- The 'Handle' is not automatically closed to allow lazy IO. Use this
+-- with caution.
+withFile'
+  :: FILE_PATH -> IOMode -> (Handle -> IO r) -> IO r
+withFile' fp iomode action = do
+  h <- openFile fp iomode
+  action h
+
+withBinaryFile'
+  :: FILE_PATH -> IOMode -> (Handle -> IO r) -> IO r
+withBinaryFile' fp iomode action = do
+  h <- openBinaryFile fp iomode
+  action h
+
+-- | The 'readFile' function reads a file and returns the contents of the file
+-- as a 'ByteString'. The file is read lazily, on demand.
+readFile :: FILE_PATH -> IO BSL.ByteString
+readFile fp = withFile' fp ReadMode BSL.hGetContents
+
+-- | The 'readFile'' function reads a file and returns the contents of the file
+-- as a 'ByteString'. The file is fully read before being returned.
+readFile'
+  :: FILE_PATH -> IO BS.ByteString
+readFile' fp = withFile fp ReadMode BS.hGetContents
+
+-- | The computation 'writeFile' @file str@ function writes the lazy 'ByteString' @str@,
+-- to the file @file@.
+writeFile :: FILE_PATH -> BSL.ByteString -> IO ()
+writeFile fp contents = withFile fp WriteMode (`BSL.hPut` contents)
+
+-- | The computation 'writeFile' @file str@ function writes the strict 'ByteString' @str@,
+-- to the file @file@.
+writeFile'
+  :: FILE_PATH -> BS.ByteString -> IO ()
+writeFile' fp contents = withFile fp WriteMode (`BS.hPut` contents)
+
+-- | The computation 'appendFile' @file str@ function appends the lazy 'ByteString' @str@,
+-- to the file @file@.
+appendFile :: FILE_PATH -> BSL.ByteString -> IO ()
+appendFile fp contents = withFile fp AppendMode (`BSL.hPut` contents)
+
+-- | The computation 'appendFile' @file str@ function appends the strict 'ByteString' @str@,
+-- to the file @file@.
+appendFile'
+  :: FILE_PATH -> BS.ByteString -> IO ()
+appendFile' fp contents = withFile fp AppendMode (`BS.hPut` contents)
+
diff --git a/System/File/OsPath.hs b/System/File/OsPath.hs
new file mode 100644
--- /dev/null
+++ b/System/File/OsPath.hs
@@ -0,0 +1,30 @@
+{-# LANGUAGE CPP #-}
+
+module System.File.OsPath where
+
+#if defined(mingw32_HOST_OS) || defined(__MINGW32__)
+#define CTOR WS
+import qualified System.File.Windows as P
+#else
+#define CTOR PS
+import qualified System.File.Posix as P
+#endif
+
+import Control.Exception (bracket)
+import System.IO (IOMode(..), Handle, hSetBinaryMode, hClose)
+import System.OsPath
+import System.OsString.Internal.Types
+
+import qualified Data.ByteString as BS
+import qualified Data.ByteString.Lazy as BSL
+
+#define FILE_PATH OsPath
+#include "Common.hs"
+
+-- | Open a file and return the 'Handle'.
+openFile :: OsPath -> IOMode -> IO Handle
+openFile (OsString fp) = P.openFile fp
+
+-- | Open an existing file and return the 'Handle'.
+openExistingFile :: OsPath -> IOMode -> IO Handle
+openExistingFile (OsString fp) = P.openExistingFile fp
diff --git a/System/File/PlatformPath.hs b/System/File/PlatformPath.hs
new file mode 100644
--- /dev/null
+++ b/System/File/PlatformPath.hs
@@ -0,0 +1,28 @@
+{-# LANGUAGE CPP #-}
+
+module System.File.PlatformPath where
+
+#if defined(mingw32_HOST_OS) || defined(__MINGW32__)
+import qualified System.File.Windows as P
+#else
+import qualified System.File.Posix as P
+#endif
+
+import Control.Exception (bracket)
+import System.IO (IOMode(..), Handle, hSetBinaryMode, hClose)
+import System.OsPath.Types
+
+import qualified Data.ByteString as BS
+import qualified Data.ByteString.Lazy as BSL
+
+#define FILE_PATH PlatformPath
+#include "Common.hs"
+
+-- | Open a file and return the 'Handle'.
+openFile :: PlatformPath -> IOMode -> IO Handle
+openFile fp = P.openFile fp
+
+-- | Open an existing file and return the 'Handle'.
+openExistingFile :: PlatformPath -> IOMode -> IO Handle
+openExistingFile fp = P.openExistingFile fp
+
diff --git a/System/File/Posix.hs b/System/File/Posix.hs
new file mode 100644
--- /dev/null
+++ b/System/File/Posix.hs
@@ -0,0 +1,32 @@
+module System.File.Posix where
+
+import System.IO (IOMode(..), Handle)
+import System.Posix.IO.PosixString
+    ( defaultFileFlags,
+      fdToHandle,
+      openFd,
+      OpenFileFlags(noctty, nonBlock, creat, append, trunc),
+      OpenMode(ReadWrite, ReadOnly, WriteOnly) )
+import System.OsPath.Posix ( PosixPath )
+
+-- | Open a file and return the 'Handle'.
+openFile :: PosixPath -> IOMode -> IO Handle
+openFile fp iomode = fdToHandle =<< case iomode of
+  ReadMode      -> open ReadOnly  df
+  WriteMode     -> open WriteOnly df { trunc = True }
+  AppendMode    -> open WriteOnly df { append = True }
+  ReadWriteMode -> open ReadWrite df
+ where
+  open = openFd fp
+  df = defaultFileFlags { noctty = True, nonBlock = True, creat = Just 0o666 }
+
+-- | Open an existing file and return the 'Handle'.
+openExistingFile :: PosixPath -> IOMode -> IO Handle
+openExistingFile fp iomode = fdToHandle =<< case iomode of
+  ReadMode      -> open ReadOnly  df
+  WriteMode     -> open WriteOnly df { trunc = True }
+  AppendMode    -> open WriteOnly df { append = True }
+  ReadWriteMode -> open ReadWrite df
+ where
+  open = openFd fp
+  df = defaultFileFlags { noctty = True, nonBlock = True, creat = Nothing }
diff --git a/System/File/Windows.hs b/System/File/Windows.hs
new file mode 100644
--- /dev/null
+++ b/System/File/Windows.hs
@@ -0,0 +1,111 @@
+{-# LANGUAGE CPP #-}
+
+module System.File.Windows where
+
+import Control.Exception (bracketOnError)
+import Data.Bits
+import System.IO (IOMode(..), Handle)
+import System.OsPath.Windows ( WindowsPath )
+
+import qualified System.Win32 as Win32
+import qualified System.Win32.WindowsString.File as WS
+import Control.Monad (when, void)
+#if defined(__IO_MANAGER_WINIO__)
+import GHC.IO.SubSystem
+#endif
+
+-- | Open a file and return the 'Handle'.
+openFile :: WindowsPath -> IOMode -> IO Handle
+openFile fp iomode = bracketOnError
+    (WS.createFile
+      fp
+      accessMode
+      shareMode
+      Nothing
+      createMode
+#if defined(__IO_MANAGER_WINIO__)
+      (case ioSubSystem of
+        IoPOSIX -> Win32.fILE_ATTRIBUTE_NORMAL
+        IoNative -> Win32.fILE_ATTRIBUTE_NORMAL .|. Win32.fILE_FLAG_OVERLAPPED
+      )
+#else
+      Win32.fILE_ATTRIBUTE_NORMAL
+#endif
+      Nothing)
+    Win32.closeHandle
+    toHandle
+ where
+  toHandle h = do
+    when (iomode == AppendMode ) $ void $ Win32.setFilePointerEx h 0 Win32.fILE_END
+    Win32.hANDLEToHandle h
+  accessMode = case iomode of
+    ReadMode      -> Win32.gENERIC_READ
+    WriteMode     -> Win32.gENERIC_WRITE
+    AppendMode    -> Win32.gENERIC_WRITE .|. Win32.fILE_APPEND_DATA
+    ReadWriteMode -> Win32.gENERIC_READ .|. Win32.gENERIC_WRITE
+
+  createMode = case iomode of
+    ReadMode      -> Win32.oPEN_ALWAYS
+    WriteMode     -> Win32.cREATE_ALWAYS
+    AppendMode    -> Win32.oPEN_ALWAYS
+    ReadWriteMode -> Win32.oPEN_ALWAYS
+
+  shareMode = case iomode of
+    ReadMode      -> maxShareMode
+    WriteMode     -> writeShareMode
+    AppendMode    -> writeShareMode
+    ReadWriteMode -> maxShareMode
+
+maxShareMode :: Win32.ShareMode
+maxShareMode =
+  Win32.fILE_SHARE_DELETE .|.
+  Win32.fILE_SHARE_READ   .|.
+  Win32.fILE_SHARE_WRITE
+
+writeShareMode :: Win32.ShareMode
+writeShareMode =
+  Win32.fILE_SHARE_DELETE .|.
+  Win32.fILE_SHARE_READ
+
+  -- | Open an existing file and return the 'Handle'.
+openExistingFile :: WindowsPath -> IOMode -> IO Handle
+openExistingFile fp iomode = bracketOnError
+    (WS.createFile
+      fp
+      accessMode
+      shareMode
+      Nothing
+      createMode
+#if defined(__IO_MANAGER_WINIO__)
+      (case ioSubSystem of
+        IoPOSIX -> Win32.fILE_ATTRIBUTE_NORMAL
+        IoNative -> Win32.fILE_ATTRIBUTE_NORMAL .|. Win32.fILE_FLAG_OVERLAPPED
+      )
+#else
+      Win32.fILE_ATTRIBUTE_NORMAL
+#endif
+      Nothing)
+    Win32.closeHandle
+    toHandle
+ where
+  toHandle h = do
+    when (iomode == AppendMode ) $ void $ Win32.setFilePointerEx h 0 Win32.fILE_END
+    Win32.hANDLEToHandle h
+  accessMode = case iomode of
+    ReadMode      -> Win32.gENERIC_READ
+    WriteMode     -> Win32.gENERIC_WRITE
+    AppendMode    -> Win32.gENERIC_WRITE .|. Win32.fILE_APPEND_DATA
+    ReadWriteMode -> Win32.gENERIC_READ .|. Win32.gENERIC_WRITE
+
+  createMode = case iomode of
+    ReadMode      -> Win32.oPEN_EXISTING
+    WriteMode     -> Win32.tRUNCATE_EXISTING
+    AppendMode    -> Win32.oPEN_EXISTING
+    ReadWriteMode -> Win32.oPEN_EXISTING
+
+  shareMode = case iomode of
+    ReadMode      -> maxShareMode
+    WriteMode     -> writeShareMode
+    AppendMode    -> writeShareMode
+    ReadWriteMode -> maxShareMode
+
diff --git a/file-io.cabal b/file-io.cabal
new file mode 100644
--- /dev/null
+++ b/file-io.cabal
@@ -0,0 +1,43 @@
+cabal-version:      2.4
+name:               file-io
+version:            0.1.0.0
+
+synopsis: Basic file IO operations via 'OsPath'
+
+description: Basic file IO operations like Prelude, but for 'OsPath'.
+
+-- A URL where users can report bugs.
+-- bug-reports:
+
+-- The license under which the package is released.
+license:            BSD-3-Clause
+author:             Julian Ospald
+maintainer:         hasufell@posteo.de
+
+copyright:          Julian Ospald 2022
+category:           System
+extra-source-files: CHANGELOG.md
+                    System/File/Common.hs
+
+library
+    default-language: Haskell2010
+
+    if os(windows)
+        other-modules:
+            System.File.Windows
+        build-depends: Win32 >= 2.13.3.0
+    else
+        other-modules:
+            System.File.Posix
+        build-depends: unix >= 2.8.0.0
+
+    build-depends:
+        base        >= 4.10    && < 4.17,
+        bytestring  >=0.11.3.0,
+        filepath    >= 1.4.100.0
+
+    exposed-modules:
+        System.File.OsPath
+        System.File.PlatformPath
+
+    ghc-options: -Wall
