diff --git a/Changelog.md b/Changelog.md
new file mode 100644
--- /dev/null
+++ b/Changelog.md
@@ -0,0 +1,3 @@
+# 0.1
+
+Initial release
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright 2023 Sergey Vinokurov
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/Readme.md b/Readme.md
new file mode 100644
--- /dev/null
+++ b/Readme.md
@@ -0,0 +1,10 @@
+[![build](https://github.com/sergv/directory-ospath-streaming/actions/workflows/haskell-ci.yaml/badge.svg)](https://github.com/sergv/directory-ospath-streaming/actions/workflows/haskell-ci.yaml)
+
+# Synopsis
+
+Reading of directory contents in constant memory, i.e. in an iterative
+fashion without storing all directory elements in memory. From another
+perspective, this reading interface allows stopping at any point
+without loading every directory element.
+
+Both Windows and Unix systems are supported.
diff --git a/directory-ospath-streaming.cabal b/directory-ospath-streaming.cabal
new file mode 100644
--- /dev/null
+++ b/directory-ospath-streaming.cabal
@@ -0,0 +1,113 @@
+cabal-version: 3.0
+
+-- Created : 27 April 2023
+
+name:
+  directory-ospath-streaming
+version:
+  0.1
+synopsis:
+  Stream directory entries in constant memory in vanilla IO
+description:
+  Reading of directory contents in constant memory, i.e. in an iterative
+  fashion without storing all directory elements in memory. From another
+  perspective, this reading interface allows stopping at any point
+  without loading every directory element.
+
+  Both Windows and Unix systems are supported.
+copyright:
+  (c) Sergey Vinokurov 2023
+license:
+  Apache-2.0
+license-file:
+  LICENSE
+author:
+  Sergey Vinokurov
+maintainer:
+  Sergey Vinokurov <serg.foo@gmail.com>
+category:
+  File, Streaming
+
+build-type:
+  Simple
+
+extra-source-files:
+  Changelog.md
+  Readme.md
+
+homepage:
+  https://github.com/sergv/directory-ospath-streaming
+
+source-repository head
+  type: git
+  location: https://github.com/sergv/directory-ospath-streaming.git
+
+common ghc-options
+  default-language:
+    GHC2021
+
+  default-extensions:
+    LambdaCase
+
+  ghc-options:
+    -Weverything
+    -Wno-all-missed-specialisations
+    -Wno-implicit-prelude
+    -Wno-missed-specialisations
+    -Wno-missing-import-lists
+    -Wno-missing-local-signatures
+    -Wno-missing-safe-haskell-mode
+    -Wno-safe
+    -Wno-type-defaults
+    -Wno-unsafe
+
+  if impl(ghc >= 8.8)
+    ghc-options:
+      -Wno-missing-deriving-strategies
+
+  if impl(ghc >= 9.2)
+    ghc-options:
+      -Wno-missing-kind-signatures
+
+library
+  import: ghc-options
+  exposed-modules:
+    System.Directory.OsPath.FileType
+    System.Directory.OsPath.Streaming
+  hs-source-dirs:
+    src
+  build-depends:
+    , base >= 4.16 && < 5
+    , filepath >= 1.4.100
+  if os(windows)
+    build-depends:
+      , atomic-counter
+      , directory >= 1.3.8
+      , Win32 >= 2.13.3
+  else
+    build-depends:
+      , unix >= 2.8
+
+test-suite test
+  import: ghc-options
+  type:
+    exitcode-stdio-1.0
+  main-is:
+    test/TestMain.hs
+  hs-source-dirs:
+    .
+    test
+  build-depends:
+    , base >= 4.16
+    , directory
+    , directory-ospath-streaming
+    , filepath >= 1.4.100
+    , random
+    , tasty
+    , tasty-hunit
+  if !os(windows)
+    build-depends:
+      unix >= 2.8
+  ghc-options:
+    -rtsopts
+    -main-is TestMain
diff --git a/src/System/Directory/OsPath/FileType.hs b/src/System/Directory/OsPath/FileType.hs
new file mode 100644
--- /dev/null
+++ b/src/System/Directory/OsPath/FileType.hs
@@ -0,0 +1,59 @@
+-- |
+-- Module:     System.Directory.OsPath.FileType
+-- Copyright:  (c) Sergey Vinokurov 2023
+-- License:    Apache-2.0 (see LICENSE)
+-- Maintainer: serg.foo@gmail.com
+
+{-# LANGUAGE CPP #-}
+
+module System.Directory.OsPath.FileType
+  ( FileType(..)
+  , getFileType
+  ) where
+
+import System.OsPath.Types (OsPath)
+
+#ifdef mingw32_HOST_OS
+
+import System.Directory.OsPath (doesFileExist, doesDirectoryExist)
+
+getFileType :: OsPath -> IO FileType
+getFileType fp = do
+  isFile <- doesFileExist fp
+  if isFile
+  then pure File
+  else do
+    isDir <- doesDirectoryExist fp
+    pure $ if isDir then Directory else Other
+
+#else
+
+import Control.Exception (try, IOException)
+import System.OsString.Internal.Types (getOsString)
+import System.Posix.Files.PosixString qualified as PosixF
+
+getFileType :: OsPath -> IO FileType
+getFileType fp = do
+  s <- PosixF.getSymbolicLinkStatus $ getOsString fp
+  case () of
+    _ | PosixF.isRegularFile s  -> pure File
+      | PosixF.isDirectory s    -> pure Directory
+      | PosixF.isSymbolicLink s -> do
+        es' <- try $ PosixF.getFileStatus $ getOsString fp
+        case es' of
+          Left (_ :: IOException) -> pure Other
+          Right s'
+            | PosixF.isRegularFile s' -> pure FileSym
+            | PosixF.isDirectory s'   -> pure DirectorySym
+            | otherwise               -> pure Other
+      | otherwise -> pure Other
+
+#endif
+
+data FileType
+  = File
+  | FileSym -- ^ Symlink to a file.
+  | Directory
+  | DirectorySym -- ^ Symlink to a directory.
+  | Other
+  deriving (Show, Read, Eq, Ord)
diff --git a/src/System/Directory/OsPath/Streaming.hs b/src/System/Directory/OsPath/Streaming.hs
new file mode 100644
--- /dev/null
+++ b/src/System/Directory/OsPath/Streaming.hs
@@ -0,0 +1,78 @@
+{-# LANGUAGE CPP                 #-}
+{-# LANGUAGE QuasiQuotes         #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+
+-- | Streaming functions for interacting with the filesystem.
+
+module System.Directory.OsPath.Streaming
+  ( DirStream
+  , openDirStream
+  , readDirStream
+  , closeDirStream
+  ) where
+
+import System.OsPath (osp)
+
+#ifdef mingw32_HOST_OS
+
+import Control.Concurrent.Counter (Counter)
+import Control.Concurrent.Counter qualified as Counter
+import Control.Monad (unless)
+import System.OsPath.Types (OsPath)
+import System.OsString.Internal.Types (OsString(OsString), getOsString)
+import System.OsString.Windows (pstr)
+import System.Win32.Types qualified as Win32
+import System.Win32.WindowsString.File qualified as Win32
+
+data DirStream = DirStream !Win32.HANDLE !Win32.FindData !Counter
+
+openDirStream :: OsPath -> IO DirStream
+openDirStream fp = do
+  (h, fdat) <- Win32.findFirstFile $ getOsString fp <> [pstr|\*|]
+  hasMore <- Counter.new 1 -- always at least two records, "." and ".."
+  pure $! DirStream h fdat hasMore
+
+closeDirStream :: DirStream -> IO ()
+closeDirStream (DirStream h _ _) = Win32.findClose h
+
+readDirStream :: DirStream -> IO (Maybe OsPath)
+readDirStream (DirStream h fdat hasMore) = go
+  where
+    go = do
+      hasMore' <- Counter.get hasMore
+      if hasMore' /= 0
+      then do
+        filename  <- Win32.getFindDataFileName fdat
+        hasMore'' <- Win32.findNextFile h fdat
+        unless hasMore'' $
+          Counter.set hasMore 0
+        if filename == getOsString [osp|.|] || filename == getOsString [osp|..|]
+        then go
+        else pure $ Just $ OsString filename
+      else pure Nothing
+
+#else
+
+import System.OsPath.Types (OsPath)
+import System.OsString.Internal.Types (OsString(OsString), getOsString)
+import System.Posix.Directory.PosixPath (DirStream, closeDirStream)
+import System.Posix.Directory.PosixPath qualified as Posix
+
+openDirStream :: OsPath -> IO DirStream
+openDirStream = Posix.openDirStream . getOsString
+
+{-# INLINE readDirStream #-}
+readDirStream :: DirStream -> IO (Maybe OsPath)
+readDirStream ds = go
+  where
+    go = do
+      fp <- Posix.readDirStream ds
+      case () of
+        _ | fp == mempty
+          -> pure Nothing
+          | fp == getOsString [osp|.|] || fp == getOsString [osp|..|]
+          -> go
+          | otherwise
+          -> pure $ Just $ OsString fp
+
+#endif
diff --git a/test/TestMain.hs b/test/TestMain.hs
new file mode 100644
--- /dev/null
+++ b/test/TestMain.hs
@@ -0,0 +1,125 @@
+-- |
+-- Module:     TestMain
+-- Copyright:  (c) Sergey Vinokurov 2023
+-- License:    Apache-2.0 (see LICENSE)
+-- Maintainer: serg.foo@gmail.com
+
+{-# LANGUAGE ApplicativeDo       #-}
+{-# LANGUAGE CPP                 #-}
+{-# LANGUAGE ImportQualifiedPost #-}
+{-# LANGUAGE NamedFieldPuns      #-}
+{-# LANGUAGE QuasiQuotes         #-}
+{-# LANGUAGE RecordWildCards     #-}
+
+module TestMain (main) where
+
+import Control.Exception
+import Data.List qualified as L
+import Numeric (showHex)
+import System.Directory.OsPath
+import System.OsPath
+import System.OsString.Internal.Types (getOsString)
+import System.Random
+
+import System.Directory.OsPath.FileType
+import System.Directory.OsPath.Streaming
+
+import Test.Tasty
+import Test.Tasty.HUnit
+
+#ifndef mingw32_HOST_OS
+import System.Posix.Files.PosixString qualified as Posix
+#endif
+
+main :: IO ()
+main = defaultMain tests
+
+tests :: TestTree
+tests = testGroup "Tests"
+  [ testCase "readDirStream" $ do
+      res <- bracket (openDirStream [osp|test/filesystem|]) closeDirStream $ \ds -> do
+        Just w <- readDirStream ds
+        Just x <- readDirStream ds
+        Just y <- readDirStream ds
+        Just z <- readDirStream ds
+        return $ L.sort [w, x, y, z]
+      res @?= [[osp|bar.txt|], [osp|baz.txt|], [osp|bin|], [osp|foo.txt|]]
+  , testGroup "getFileType general"
+      [ testCase "file" $ do
+          getFileType [osp|directory-ospath-streaming.cabal|] >>= (@?= File)
+      , testCase "directory" $ do
+          getFileType [osp|test|] >>= (@?= Directory)
+      ]
+#ifndef mingw32_HOST_OS
+  , withResource
+      (do
+        tmp <- getTemporaryDirectory >>= canonicalizePath
+        createFreshTempDir tmp [osp|test|])
+      removeDirectoryRecursive
+      $ \mkTmpDir -> testGroup "getFileType unix"
+        [ testCase "file symlink" $ do
+            tmp     <- mkTmpDir
+            currDir <- getCurrentDirectory
+            let dest = tmp </> [osp|tmp1|]
+            Posix.createSymbolicLink
+              (getOsString (currDir </> [osp|directory-ospath-streaming.cabal|]))
+              (getOsString dest)
+            ft <- getFileType dest
+            ft @?= FileSym
+        , testCase "directory symlink" $ do
+            tmp     <- mkTmpDir
+            currDir <- getCurrentDirectory
+            let dest = tmp </> [osp|tmp2|]
+            Posix.createSymbolicLink
+              (getOsString (currDir </> [osp|src|]))
+              (getOsString dest)
+            ft <- getFileType dest
+            ft @?= DirectorySym
+        , testCase "other" $ do
+            tmp <- mkTmpDir
+            let dest = tmp </> [osp|tmp3|]
+            res    <- tryIO $ Posix.createNamedPipe (getOsString dest) 0
+            case res of
+              -- Creating named pipe might fail on some filesystems
+              Left _  -> pure ()
+              Right _ -> do
+                ft <- getFileType dest
+                ft @?= Other
+        , testCase "recursive symlink is other" $ do
+            tmp <- mkTmpDir
+            let dest = tmp </> [osp|tmp4|]
+            Posix.createSymbolicLink
+              (getOsString dest)
+              (getOsString dest)
+            ft <- getFileType dest
+            ft @?= Other
+        , testCase "dangling symlink is other" $ do
+            tmp <- mkTmpDir
+            let dest = tmp </> [osp|tmp5|]
+            Posix.createSymbolicLink
+              (getOsString (tmp </> [osp|does-not-exist|]))
+              (getOsString dest)
+            ft <- getFileType dest
+            ft @?= Other
+        ]
+#endif
+  ]
+
+#ifndef mingw32_HOST_OS
+tryIO :: IO a -> IO (Either IOException a)
+tryIO = try
+#endif
+
+createFreshTempDir :: OsPath -> OsPath -> IO OsPath
+createFreshTempDir dir prefix = go
+  where
+    go = do
+      (n :: Word) <- randomIO
+      n'          <- encodeUtf (showHex n [])
+      let path = dir </> prefix <> [osp|-|] <> n'
+      exists <- doesDirectoryExist path
+      if exists
+      then go
+      else do
+        createDirectory path
+        pure path
