You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

853 lines
25 KiB

  1. -- |
  2. -- Module : HPath.IO
  3. -- Copyright : © 2016 Julian Ospald
  4. -- License : BSD3
  5. --
  6. -- Maintainer : Julian Ospald <hasufell@posteo.de>
  7. -- Stability : experimental
  8. -- Portability : portable
  9. --
  10. -- This module provides high-level IO related file operations like
  11. -- copy, delete, move and so on. It only operates on /Path x/ which
  12. -- guarantees us well-typed paths. This is a thin wrapper over
  13. -- System.Posix.RawFilePath.Directory in 'hpath-directory'. It's
  14. -- encouraged to use this module.
  15. --
  16. -- Some of these operations are due to their nature __not atomic__, which
  17. -- means they may do multiple syscalls which form one context. Some
  18. -- of them also have to examine the filetypes explicitly before the
  19. -- syscalls, so a reasonable decision can be made. That means
  20. -- the result is undefined if another process changes that context
  21. -- while the non-atomic operation is still happening. However, where
  22. -- possible, as few syscalls as possible are used and the underlying
  23. -- exception handling is kept.
  24. --
  25. -- Note: `BlockDevice`, `CharacterDevice`, `NamedPipe` and `Socket`
  26. -- are ignored by some of the more high-level functions (like `easyCopy`).
  27. -- For other functions (like `copyFile`), the behavior on these file types is
  28. -- unreliable/unsafe. Check the documentation of those functions for details.
  29. {-# LANGUAGE PackageImports #-}
  30. module HPath.IO
  31. (
  32. -- * Types
  33. FileType(..)
  34. , RecursiveErrorMode(..)
  35. , CopyMode(..)
  36. -- * File copying
  37. , copyDirRecursive
  38. , recreateSymlink
  39. , copyFile
  40. , easyCopy
  41. -- * File deletion
  42. , deleteFile
  43. , deleteDir
  44. , deleteDirRecursive
  45. , easyDelete
  46. -- * File opening
  47. , openFile
  48. , executeFile
  49. -- * File creation
  50. , createRegularFile
  51. , createDir
  52. , createDirIfMissing
  53. , createDirRecursive
  54. , createSymlink
  55. -- * File renaming/moving
  56. , renameFile
  57. , moveFile
  58. -- * File reading
  59. , readFile
  60. , readFileStream
  61. -- * File writing
  62. , writeFile
  63. , writeFileL
  64. , appendFile
  65. -- * File permissions
  66. , RD.newFilePerms
  67. , RD.newDirPerms
  68. -- * File checks
  69. , doesExist
  70. , doesFileExist
  71. , doesDirectoryExist
  72. , isReadable
  73. , isWritable
  74. , isExecutable
  75. , canOpenDirectory
  76. -- * File times
  77. , getModificationTime
  78. , setModificationTime
  79. , setModificationTimeHiRes
  80. -- * Directory reading
  81. , getDirsFiles
  82. , getDirsFiles'
  83. -- * Filetype operations
  84. , getFileType
  85. -- * Others
  86. , canonicalizePath
  87. , toAbs
  88. , withRawFilePath
  89. , withHandle
  90. , module System.Posix.RawFilePath.Directory.Errors
  91. )
  92. where
  93. import Control.Exception.Safe ( bracketOnError
  94. , finally
  95. )
  96. import Control.Monad.Catch ( MonadThrow(..) )
  97. import Data.ByteString ( ByteString )
  98. import Data.Traversable ( for )
  99. import qualified Data.ByteString.Lazy as L
  100. import Data.Time.Clock
  101. import Data.Time.Clock.POSIX ( POSIXTime )
  102. import HPath
  103. import Prelude hiding ( appendFile
  104. , readFile
  105. , writeFile
  106. )
  107. import Streamly
  108. import qualified System.IO as SIO
  109. import System.Posix.Directory.ByteString
  110. ( getWorkingDirectory )
  111. import qualified "unix" System.Posix.IO.ByteString
  112. as SPI
  113. import System.Posix.FD ( openFd )
  114. import System.Posix.RawFilePath.Directory.Errors
  115. import System.Posix.Types ( FileMode
  116. , ProcessID
  117. , EpochTime
  118. )
  119. import qualified System.Posix.RawFilePath.Directory
  120. as RD
  121. import System.Posix.RawFilePath.Directory
  122. ( FileType
  123. , RecursiveErrorMode
  124. , CopyMode
  125. )
  126. --------------------
  127. --[ File Copying ]--
  128. --------------------
  129. -- |Copies the contents of a directory recursively to the given destination, while preserving permissions.
  130. -- Does not follow symbolic links. This behaves more or less like
  131. -- the following, without descending into the destination if it
  132. -- already exists:
  133. --
  134. -- @
  135. -- cp -a \/source\/dir \/destination\/somedir
  136. -- @
  137. --
  138. -- For directory contents, this will ignore any file type that is not
  139. -- `RegularFile`, `SymbolicLink` or `Directory`.
  140. --
  141. -- For `Overwrite` copy mode this does not prune destination directory
  142. -- contents, so the destination might contain more files than the source after
  143. -- the operation has completed. Permissions of existing directories are
  144. -- fixed.
  145. --
  146. -- Safety/reliability concerns:
  147. --
  148. -- * not atomic
  149. -- * examines filetypes explicitly
  150. -- * an explicit check `throwDestinationInSource` is carried out for the
  151. -- top directory for basic sanity, because otherwise we might end up
  152. -- with an infinite copy loop... however, this operation is not
  153. -- carried out recursively (because it's slow)
  154. --
  155. -- Throws:
  156. --
  157. -- - `NoSuchThing` if source directory does not exist
  158. -- - `PermissionDenied` if source directory can't be opened
  159. -- - `SameFile` if source and destination are the same file
  160. -- (`HPathIOException`)
  161. -- - `DestinationInSource` if destination is contained in source
  162. -- (`HPathIOException`)
  163. --
  164. -- Throws in `FailEarly` RecursiveErrorMode only:
  165. --
  166. -- - `PermissionDenied` if output directory is not writable
  167. -- - `InvalidArgument` if source directory is wrong type (symlink)
  168. -- - `InappropriateType` if source directory is wrong type (regular file)
  169. --
  170. -- Throws in `CollectFailures` RecursiveErrorMode only:
  171. --
  172. -- - `RecursiveFailure` if any of the recursive operations that are not
  173. -- part of the top-directory sanity-checks fail (`HPathIOException`)
  174. --
  175. -- Throws in `Strict` CopyMode only:
  176. --
  177. -- - `AlreadyExists` if destination already exists
  178. --
  179. -- Note: may call `getcwd` (only if destination is a relative path)
  180. copyDirRecursive :: Path b1 -- ^ source dir
  181. -> Path b2 -- ^ destination (parent dirs
  182. -- are not automatically created)
  183. -> CopyMode
  184. -> RecursiveErrorMode
  185. -> IO ()
  186. copyDirRecursive (Path fromp) (Path destdirp) cm rm =
  187. RD.copyDirRecursive fromp destdirp cm rm
  188. -- |Recreate a symlink.
  189. --
  190. -- In `Overwrite` copy mode only files and empty directories are deleted.
  191. --
  192. -- Safety/reliability concerns:
  193. --
  194. -- * `Overwrite` mode is inherently non-atomic
  195. --
  196. -- Throws:
  197. --
  198. -- - `InvalidArgument` if source file is wrong type (not a symlink)
  199. -- - `PermissionDenied` if output directory cannot be written to
  200. -- - `PermissionDenied` if source directory cannot be opened
  201. -- - `SameFile` if source and destination are the same file
  202. -- (`HPathIOException`)
  203. --
  204. --
  205. -- Throws in `Strict` mode only:
  206. --
  207. -- - `AlreadyExists` if destination already exists
  208. --
  209. -- Throws in `Overwrite` mode only:
  210. --
  211. -- - `UnsatisfiedConstraints` if destination file is non-empty directory
  212. --
  213. -- Notes:
  214. --
  215. -- - calls `symlink`
  216. -- - calls `getcwd` in Overwrite mode (if destination is a relative path)
  217. recreateSymlink :: Path b1 -- ^ the old symlink file
  218. -> Path b2 -- ^ destination file
  219. -> CopyMode
  220. -> IO ()
  221. recreateSymlink (Path symsourceBS) (Path newsymBS) cm =
  222. RD.recreateSymlink symsourceBS newsymBS cm
  223. -- |Copies the given regular file to the given destination.
  224. -- Neither follows symbolic links, nor accepts them.
  225. -- For "copying" symbolic links, use `recreateSymlink` instead.
  226. --
  227. -- Note that this is still sort of a low-level function and doesn't
  228. -- examine file types. For a more high-level version, use `easyCopy`
  229. -- instead.
  230. --
  231. -- In `Overwrite` copy mode only overwrites actual files, not directories.
  232. -- In `Strict` mode the destination file must not exist.
  233. --
  234. -- Safety/reliability concerns:
  235. --
  236. -- * `Overwrite` mode is not atomic
  237. -- * when used on `CharacterDevice`, reads the "contents" and copies
  238. -- them to a regular file, which might take indefinitely
  239. -- * when used on `BlockDevice`, may either read the "contents"
  240. -- and copy them to a regular file (potentially hanging indefinitely)
  241. -- or may create a regular empty destination file
  242. -- * when used on `NamedPipe`, will hang indefinitely
  243. --
  244. -- Throws:
  245. --
  246. -- - `NoSuchThing` if source file does not exist
  247. -- - `NoSuchThing` if source file is a a `Socket`
  248. -- - `PermissionDenied` if output directory is not writable
  249. -- - `PermissionDenied` if source directory can't be opened
  250. -- - `InvalidArgument` if source file is wrong type (symlink or directory)
  251. -- - `SameFile` if source and destination are the same file
  252. -- (`HPathIOException`)
  253. --
  254. -- Throws in `Strict` mode only:
  255. --
  256. -- - `AlreadyExists` if destination already exists
  257. --
  258. -- Notes:
  259. --
  260. -- - may call `getcwd` in Overwrite mode (if destination is a relative path)
  261. copyFile :: Path b1 -- ^ source file
  262. -> Path b2 -- ^ destination file
  263. -> CopyMode
  264. -> IO ()
  265. copyFile (Path from) (Path to) cm = RD.copyFile from to cm
  266. -- |Copies a regular file, directory or symbolic link. In case of a
  267. -- symbolic link it is just recreated, even if it points to a directory.
  268. -- Any other file type is ignored.
  269. --
  270. -- Safety/reliability concerns:
  271. --
  272. -- * examines filetypes explicitly
  273. -- * calls `copyDirRecursive` for directories
  274. --
  275. -- Note: may call `getcwd` in Overwrite mode (if destination is a relative path)
  276. easyCopy :: Path b1 -> Path b2 -> CopyMode -> RecursiveErrorMode -> IO ()
  277. easyCopy (Path from) (Path to) cm rm = RD.easyCopy from to cm rm
  278. ---------------------
  279. --[ File Deletion ]--
  280. ---------------------
  281. -- |Deletes the given file. Raises `eISDIR`
  282. -- if run on a directory. Does not follow symbolic links.
  283. --
  284. -- Throws:
  285. --
  286. -- - `InappropriateType` for wrong file type (directory)
  287. -- - `NoSuchThing` if the file does not exist
  288. -- - `PermissionDenied` if the directory cannot be read
  289. deleteFile :: Path b -> IO ()
  290. deleteFile (Path p) = RD.deleteFile p
  291. -- |Deletes the given directory, which must be empty, never symlinks.
  292. --
  293. -- Throws:
  294. --
  295. -- - `InappropriateType` for wrong file type (symlink to directory)
  296. -- - `InappropriateType` for wrong file type (regular file)
  297. -- - `NoSuchThing` if directory does not exist
  298. -- - `UnsatisfiedConstraints` if directory is not empty
  299. -- - `PermissionDenied` if we can't open or write to parent directory
  300. --
  301. -- Notes: calls `rmdir`
  302. deleteDir :: Path b -> IO ()
  303. deleteDir (Path p) = RD.deleteDir p
  304. -- |Deletes the given directory recursively. Does not follow symbolic
  305. -- links. Tries `deleteDir` first before attemtping a recursive
  306. -- deletion.
  307. --
  308. -- On directory contents this behaves like `easyDelete`
  309. -- and thus will ignore any file type that is not `RegularFile`,
  310. -- `SymbolicLink` or `Directory`.
  311. --
  312. -- Safety/reliability concerns:
  313. --
  314. -- * not atomic
  315. -- * examines filetypes explicitly
  316. --
  317. -- Throws:
  318. --
  319. -- - `InappropriateType` for wrong file type (symlink to directory)
  320. -- - `InappropriateType` for wrong file type (regular file)
  321. -- - `NoSuchThing` if directory does not exist
  322. -- - `PermissionDenied` if we can't open or write to parent directory
  323. deleteDirRecursive :: Path b -> IO ()
  324. deleteDirRecursive (Path p) = RD.deleteDirRecursive p
  325. -- |Deletes a file, directory or symlink.
  326. -- In case of directory, performs recursive deletion. In case of
  327. -- a symlink, the symlink file is deleted.
  328. -- Any other file type is ignored.
  329. --
  330. -- Safety/reliability concerns:
  331. --
  332. -- * examines filetypes explicitly
  333. -- * calls `deleteDirRecursive` for directories
  334. easyDelete :: Path b -> IO ()
  335. easyDelete (Path p) = RD.easyDelete p
  336. --------------------
  337. --[ File Opening ]--
  338. --------------------
  339. -- |Opens a file appropriately by invoking xdg-open. The file type
  340. -- is not checked. This forks a process.
  341. openFile :: Path b -> IO ProcessID
  342. openFile (Path fp) = RD.openFile fp
  343. -- |Executes a program with the given arguments. This forks a process.
  344. executeFile :: Path b -- ^ program
  345. -> [ByteString] -- ^ arguments
  346. -> IO ProcessID
  347. executeFile (Path fp) args = RD.executeFile fp args
  348. ---------------------
  349. --[ File Creation ]--
  350. ---------------------
  351. -- |Create an empty regular file at the given directory with the given
  352. -- filename.
  353. --
  354. -- Throws:
  355. --
  356. -- - `PermissionDenied` if output directory cannot be written to
  357. -- - `AlreadyExists` if destination already exists
  358. -- - `NoSuchThing` if any of the parent components of the path
  359. -- do not exist
  360. createRegularFile :: FileMode -> Path b -> IO ()
  361. createRegularFile fm (Path destBS) = RD.createRegularFile fm destBS
  362. -- |Create an empty directory at the given directory with the given filename.
  363. --
  364. -- Throws:
  365. --
  366. -- - `PermissionDenied` if output directory cannot be written to
  367. -- - `AlreadyExists` if destination already exists
  368. -- - `NoSuchThing` if any of the parent components of the path
  369. -- do not exist
  370. createDir :: FileMode -> Path b -> IO ()
  371. createDir fm (Path destBS) = RD.createDir fm destBS
  372. -- |Create an empty directory at the given directory with the given filename.
  373. --
  374. -- Throws:
  375. --
  376. -- - `PermissionDenied` if output directory cannot be written to
  377. -- - `NoSuchThing` if any of the parent components of the path
  378. -- do not exist
  379. createDirIfMissing :: FileMode -> Path b -> IO ()
  380. createDirIfMissing fm (Path destBS) = RD.createDirIfMissing fm destBS
  381. -- |Create an empty directory at the given directory with the given filename.
  382. -- All parent directories are created with the same filemode. This
  383. -- basically behaves like:
  384. --
  385. -- @
  386. -- mkdir -p \/some\/dir
  387. -- @
  388. --
  389. -- Safety/reliability concerns:
  390. --
  391. -- * not atomic
  392. --
  393. -- Throws:
  394. --
  395. -- - `PermissionDenied` if any part of the path components do not
  396. -- exist and cannot be written to
  397. -- - `AlreadyExists` if destination already exists and
  398. -- is *not* a directory
  399. --
  400. -- Note: calls `getcwd` if the input path is a relative path
  401. createDirRecursive :: FileMode -> Path b -> IO ()
  402. createDirRecursive fm (Path p) = RD.createDirRecursive fm p
  403. -- |Create a symlink.
  404. --
  405. -- Throws:
  406. --
  407. -- - `PermissionDenied` if output directory cannot be written to
  408. -- - `AlreadyExists` if destination file already exists
  409. -- - `NoSuchThing` if any of the parent components of the path
  410. -- do not exist
  411. --
  412. -- Note: calls `symlink`
  413. createSymlink :: Path b -- ^ destination file
  414. -> ByteString -- ^ path the symlink points to
  415. -> IO ()
  416. createSymlink (Path destBS) sympoint = RD.createSymlink destBS sympoint
  417. ----------------------------
  418. --[ File Renaming/Moving ]--
  419. ----------------------------
  420. -- |Rename a given file with the provided filename. Destination and source
  421. -- must be on the same device, otherwise `eXDEV` will be raised.
  422. --
  423. -- Does not follow symbolic links, but renames the symbolic link file.
  424. --
  425. -- Safety/reliability concerns:
  426. --
  427. -- * has a separate set of exception handling, apart from the syscall
  428. --
  429. -- Throws:
  430. --
  431. -- - `NoSuchThing` if source file does not exist
  432. -- - `PermissionDenied` if output directory cannot be written to
  433. -- - `PermissionDenied` if source directory cannot be opened
  434. -- - `UnsupportedOperation` if source and destination are on different
  435. -- devices
  436. -- - `AlreadyExists` if destination already exists
  437. -- - `SameFile` if destination and source are the same file
  438. -- (`HPathIOException`)
  439. --
  440. -- Note: calls `rename` (but does not allow to rename over existing files)
  441. renameFile :: Path b1 -> Path b2 -> IO ()
  442. renameFile (Path from) (Path to) = RD.renameFile from to
  443. -- |Move a file. This also works across devices by copy-delete fallback.
  444. -- And also works on directories.
  445. --
  446. -- Does not follow symbolic links, but renames the symbolic link file.
  447. --
  448. --
  449. -- Safety/reliability concerns:
  450. --
  451. -- * `Overwrite` mode is not atomic
  452. -- * copy-delete fallback is inherently non-atomic
  453. -- * since this function calls `easyCopy` and `easyDelete` as a fallback
  454. -- to `renameFile`, file types that are not `RegularFile`, `SymbolicLink`
  455. -- or `Directory` may be ignored
  456. -- * for `Overwrite` mode, the destination will be deleted (not recursively)
  457. -- before moving
  458. --
  459. -- Throws:
  460. --
  461. -- - `NoSuchThing` if source file does not exist
  462. -- - `PermissionDenied` if output directory cannot be written to
  463. -- - `PermissionDenied` if source directory cannot be opened
  464. -- - `SameFile` if destination and source are the same file
  465. -- (`HPathIOException`)
  466. --
  467. -- Throws in `Strict` mode only:
  468. --
  469. -- - `AlreadyExists` if destination already exists
  470. --
  471. -- Notes:
  472. --
  473. -- - calls `rename` (but does not allow to rename over existing files)
  474. -- - calls `getcwd` in Overwrite mode if destination is a relative path
  475. moveFile :: Path b1 -- ^ file to move
  476. -> Path b2 -- ^ destination
  477. -> CopyMode
  478. -> IO ()
  479. moveFile (Path from) (Path to) cm = RD.moveFile from to cm
  480. --------------------
  481. --[ File Reading ]--
  482. --------------------
  483. -- |Read the given file *at once* into memory as a lazy ByteString.
  484. -- Symbolic links are followed, no sanity checks on file size
  485. -- or file type. File must exist. Uses Builders under the hood
  486. -- (hence lazy ByteString).
  487. --
  488. -- Safety/reliability concerns:
  489. --
  490. -- * the whole file is read into memory, this doesn't read lazily
  491. --
  492. -- Throws:
  493. --
  494. -- - `InappropriateType` if file is not a regular file or a symlink
  495. -- - `PermissionDenied` if we cannot read the file or the directory
  496. -- containting it
  497. -- - `NoSuchThing` if the file does not exist
  498. readFile :: Path b -> IO L.ByteString
  499. readFile (Path path) = RD.readFile path
  500. -- | Open the given file as a filestream. Once the filestream is
  501. -- exits, the filehandle is cleaned up.
  502. --
  503. -- Throws:
  504. --
  505. -- - `InappropriateType` if file is not a regular file or a symlink
  506. -- - `PermissionDenied` if we cannot read the file or the directory
  507. -- containting it
  508. -- - `NoSuchThing` if the file does not exist
  509. readFileStream :: Path b -> IO (SerialT IO ByteString)
  510. readFileStream (Path fp) = RD.readFileStream fp
  511. --------------------
  512. --[ File Writing ]--
  513. --------------------
  514. -- |Write a given ByteString to a file, truncating the file beforehand.
  515. -- Follows symlinks.
  516. --
  517. -- Throws:
  518. --
  519. -- - `InappropriateType` if file is not a regular file or a symlink
  520. -- - `PermissionDenied` if we cannot read the file or the directory
  521. -- containting it
  522. -- - `NoSuchThing` if the file does not exist
  523. writeFile :: Path b
  524. -> Maybe FileMode -- ^ if Nothing, file must exist
  525. -> ByteString
  526. -> IO ()
  527. writeFile (Path fp) fmode bs = RD.writeFile fp fmode bs
  528. -- |Write a given lazy ByteString to a file, truncating the file beforehand.
  529. -- Follows symlinks.
  530. --
  531. -- Throws:
  532. --
  533. -- - `InappropriateType` if file is not a regular file or a symlink
  534. -- - `PermissionDenied` if we cannot read the file or the directory
  535. -- containting it
  536. -- - `NoSuchThing` if the file does not exist
  537. --
  538. -- Note: uses streamly under the hood
  539. writeFileL :: Path b
  540. -> Maybe FileMode -- ^ if Nothing, file must exist
  541. -> L.ByteString
  542. -> IO ()
  543. writeFileL (Path fp) fmode lbs = RD.writeFileL fp fmode lbs
  544. -- |Append a given ByteString to a file.
  545. -- The file must exist. Follows symlinks.
  546. --
  547. -- Throws:
  548. --
  549. -- - `InappropriateType` if file is not a regular file or a symlink
  550. -- - `PermissionDenied` if we cannot read the file or the directory
  551. -- containting it
  552. -- - `NoSuchThing` if the file does not exist
  553. appendFile :: Path b -> ByteString -> IO ()
  554. appendFile (Path fp) bs = RD.appendFile fp bs
  555. -------------------
  556. --[ File checks ]--
  557. -------------------
  558. -- |Checks if the given file exists.
  559. -- Does not follow symlinks.
  560. --
  561. -- Only eNOENT is catched (and returns False).
  562. doesExist :: Path b -> IO Bool
  563. doesExist (Path bs) = RD.doesExist bs
  564. -- |Checks if the given file exists and is not a directory.
  565. -- Does not follow symlinks.
  566. --
  567. -- Only eNOENT is catched (and returns False).
  568. doesFileExist :: Path b -> IO Bool
  569. doesFileExist (Path bs) = RD.doesFileExist bs
  570. -- |Checks if the given file exists and is a directory.
  571. -- Does not follow symlinks.
  572. --
  573. -- Only eNOENT is catched (and returns False).
  574. doesDirectoryExist :: Path b -> IO Bool
  575. doesDirectoryExist (Path bs) = RD.doesDirectoryExist bs
  576. -- |Checks whether a file or folder is readable.
  577. --
  578. -- Only eACCES, eROFS, eTXTBSY, ePERM are catched (and return False).
  579. --
  580. -- Throws:
  581. --
  582. -- - `NoSuchThing` if the file does not exist
  583. isReadable :: Path b -> IO Bool
  584. isReadable (Path bs) = RD.isReadable bs
  585. -- |Checks whether a file or folder is writable.
  586. --
  587. -- Only eACCES, eROFS, eTXTBSY, ePERM are catched (and return False).
  588. --
  589. -- Throws:
  590. --
  591. -- - `NoSuchThing` if the file does not exist
  592. isWritable :: Path b -> IO Bool
  593. isWritable (Path bs) = RD.isWritable bs
  594. -- |Checks whether a file or folder is executable.
  595. --
  596. -- Only eACCES, eROFS, eTXTBSY, ePERM are catched (and return False).
  597. --
  598. -- Throws:
  599. --
  600. -- - `NoSuchThing` if the file does not exist
  601. isExecutable :: Path b -> IO Bool
  602. isExecutable (Path bs) = RD.isExecutable bs
  603. -- |Checks whether the directory at the given path exists and can be
  604. -- opened. This invokes `openDirStream` which follows symlinks.
  605. canOpenDirectory :: Path b -> IO Bool
  606. canOpenDirectory (Path bs) = RD.canOpenDirectory bs
  607. ------------------
  608. --[ File times ]--
  609. ------------------
  610. getModificationTime :: Path b -> IO UTCTime
  611. getModificationTime (Path bs) = RD.getModificationTime bs
  612. setModificationTime :: Path b -> EpochTime -> IO ()
  613. setModificationTime (Path bs) t = RD.setModificationTime bs t
  614. setModificationTimeHiRes :: Path b -> POSIXTime -> IO ()
  615. setModificationTimeHiRes (Path bs) t = RD.setModificationTimeHiRes bs t
  616. -------------------------
  617. --[ Directory reading ]--
  618. -------------------------
  619. -- |Gets all filenames of the given directory. This excludes "." and "..".
  620. -- This version does not follow symbolic links.
  621. --
  622. -- The contents are not sorted and there is no guarantee on the ordering.
  623. --
  624. -- Throws:
  625. --
  626. -- - `NoSuchThing` if directory does not exist
  627. -- - `InappropriateType` if file type is wrong (file)
  628. -- - `InappropriateType` if file type is wrong (symlink to file)
  629. -- - `InappropriateType` if file type is wrong (symlink to dir)
  630. -- - `PermissionDenied` if directory cannot be opened
  631. -- - `PathParseException` if a filename could not be parsed (should never happen)
  632. getDirsFiles :: Path b -- ^ dir to read
  633. -> IO [Path b]
  634. getDirsFiles p = do
  635. contents <- getDirsFiles' p
  636. pure $ fmap (p </>) contents
  637. -- | Like 'getDirsFiles', but returns the filename only, instead
  638. -- of prepending the base path.
  639. getDirsFiles' :: Path b -- ^ dir to read
  640. -> IO [Path Rel]
  641. getDirsFiles' (Path fp) = do
  642. rawContents <- RD.getDirsFiles' fp
  643. for rawContents $ \r -> parseRel r
  644. ---------------------------
  645. --[ FileType operations ]--
  646. ---------------------------
  647. -- |Get the file type of the file located at the given path. Does
  648. -- not follow symbolic links.
  649. --
  650. -- Throws:
  651. --
  652. -- - `NoSuchThing` if the file does not exist
  653. -- - `PermissionDenied` if any part of the path is not accessible
  654. getFileType :: Path b -> IO FileType
  655. getFileType (Path fp) = RD.getFileType fp
  656. --------------
  657. --[ Others ]--
  658. --------------
  659. -- |Applies `realpath` on the given path.
  660. --
  661. -- Throws:
  662. --
  663. -- - `NoSuchThing` if the file at the given path does not exist
  664. -- - `NoSuchThing` if the symlink is broken
  665. -- - `PathParseException` if realpath does not return an absolute path
  666. canonicalizePath :: Path b -> IO (Path Abs)
  667. canonicalizePath (Path l) = do
  668. nl <- RD.canonicalizePath l
  669. parseAbs nl
  670. -- |Converts any path to an absolute path.
  671. -- This is done in the following way:
  672. --
  673. -- - if the path is already an absolute one, just return it
  674. -- - if it's a relative path, prepend the current directory to it
  675. toAbs :: Path b -> IO (Path Abs)
  676. toAbs (Path bs) = do
  677. let mabs = parseAbs bs :: Maybe (Path Abs)
  678. case mabs of
  679. Just a -> return a
  680. Nothing -> do
  681. cwd <- getWorkingDirectory >>= parseAbs
  682. r <- parseRel bs -- we know it must be relative now
  683. return $ cwd </> r
  684. -- | Helper function to use the Path library without
  685. -- buying into the Path type too much. This uses 'parseAny'
  686. -- under the hood and may throw `PathParseException`.
  687. --
  688. -- Throws:
  689. --
  690. -- - `PathParseException` if the bytestring could neither be parsed as
  691. -- relative or absolute Path
  692. withRawFilePath :: MonadThrow m
  693. => ByteString
  694. -> (Either (Path Abs) (Path Rel) -> m b)
  695. -> m b
  696. withRawFilePath bs action = do
  697. path <- parseAny bs
  698. action path
  699. -- | Convenience function to open the path as a handle.
  700. --
  701. -- If the file does not exist, it will be created with 'newFilePerms'.
  702. --
  703. -- Throws:
  704. --
  705. -- - `PathParseException` if the bytestring could neither be parsed as
  706. -- relative or absolute Path
  707. withHandle :: ByteString
  708. -> SPI.OpenMode
  709. -> ((SIO.Handle, Either (Path Abs) (Path Rel)) -> IO a)
  710. -> IO a
  711. withHandle bs mode action = do
  712. path <- parseAny bs
  713. handle <-
  714. bracketOnError (openFd bs mode [] (Just RD.newFilePerms)) (SPI.closeFd)
  715. $ SPI.fdToHandle
  716. finally (action (handle, path)) (SIO.hClose handle)