Compare commits
164 Commits
0.5.5
...
a0510eaec1
| Author | SHA1 | Date | |
|---|---|---|---|
| a0510eaec1 | |||
| cce2c68cab | |||
| 3c5f06f41d | |||
| 6bc5381108 | |||
| ef51863180 | |||
| 09062351f5 | |||
| ab4137572e | |||
| c03a7ec18f | |||
|
|
df298f187e | ||
| 466c72924a | |||
| 9dfb803ba8 | |||
| de46a0c568 | |||
| d9ba67b6f0 | |||
| 9342abeb7a | |||
| e8cbc632c9 | |||
| c556a3d3e4 | |||
| 3aee719130 | |||
| 10662ee803 | |||
| 672875f48f | |||
| 3e6d93182a | |||
| 1c95c9f8f9 | |||
| 0ec2cf8ca5 | |||
| 9ac10a6a7d | |||
| 1a2c77c6a6 | |||
| 3baecb7b51 | |||
| 5d5b0ae3c1 | |||
| f47c8edb42 | |||
| ef66a24f87 | |||
| f6a5cb8668 | |||
| 4dec385332 | |||
| 5b08e14b55 | |||
| ac381cbf60 | |||
| ce7fdcdcd6 | |||
| a31c9d1e88 | |||
| a5942ff026 | |||
| 4d71ad08ce | |||
| 92017ab630 | |||
| 16af98b32d | |||
| 6da01e382f | |||
| ed06543981 | |||
| d3eb2fc254 | |||
| a1eb06324f | |||
| d12ce30f57 | |||
| 7a6f0e8728 | |||
| 7ed5829d47 | |||
| d708f80a1f | |||
| f07619b7c6 | |||
| c5bcb90b65 | |||
| 4f047dbc77 | |||
| bc348c7dd5 | |||
| 5d1c5cc2ce | |||
| 8f6ca81d22 | |||
| a27d4ed55d | |||
| 64ae6db83a | |||
| 2a0a88a96d | |||
| 69dbf6714d | |||
| 2d96311b33 | |||
| 21668f12fe | |||
| 6e37e18bc8 | |||
| ae24f87c74 | |||
| 9f6734e700 | |||
| 741c510b91 | |||
| bb590a7692 | |||
| 641e23c3ef | |||
| 82ea75cc88 | |||
| abf043be14 | |||
| 10adc4be27 | |||
| a176e4970b | |||
| 08de2ebefb | |||
| d15d7761c1 | |||
| 7e924d3386 | |||
| 21fccc9ca9 | |||
| 79dbcd8b55 | |||
| b603f72407 | |||
| 98ca6c5d86 | |||
| 8d948366f9 | |||
| 86e7496917 | |||
| 1b9b8cc886 | |||
| 395621b27a | |||
| 51da8bf5c2 | |||
| bebc96fa6d | |||
| 08fa277b31 | |||
| 51609781b2 | |||
| 3cb3a822d7 | |||
| 7fa4c041a9 | |||
| e66074af1c | |||
| 4032629407 | |||
| f2fe5a3419 | |||
| 5ac7450495 | |||
| b55cf6d9f3 | |||
| ae9a806c2e | |||
| 9c199c6da2 | |||
| eb72fce33f | |||
| 65bb09d133 | |||
| 908513da2b | |||
| 47dd729e8a | |||
| 620550dab4 | |||
| ebab5355bc | |||
| 8fdf1bf956 | |||
| 39913faed6 | |||
| 5ed249f5d6 | |||
| a8ccfc2587 | |||
| 8fec862304 | |||
| 646fe7cfea | |||
| 1bf27258c1 | |||
| 797dcaf725 | |||
| 0fa66cd581 | |||
| ee3ace362b | |||
| 05fcad14f1 | |||
| 456af3b1ab | |||
| f841a53985 | |||
| eb27c368e6 | |||
| c76df7f159 | |||
| 613754c58f | |||
| d8b0b99edf | |||
| 794c3a2fc4 | |||
| 8a28a5dd0f | |||
| 78a3baeb25 | |||
| a3b1528974 | |||
| 7aac9bcb93 | |||
| 930b021a32 | |||
| 6b6c7f05c9 | |||
| a83e96259f | |||
| 14b48515a2 | |||
| 820bf8814d | |||
| e18d2dd2d6 | |||
| 117b3dc7d7 | |||
| c0ceccf716 | |||
| 010756e190 | |||
| 9a4fd00710 | |||
| f27becc4df | |||
| 3bbde22377 | |||
| 3da8533b48 | |||
| 687a113252 | |||
| 86a4b9ade2 | |||
| 6638cd8cc1 | |||
| 4ce35b9bec | |||
| 196647a383 | |||
| cfe626b6d4 | |||
| a946387330 | |||
| 1263fac7ec | |||
| 45b515d1db | |||
| a5360f29a3 | |||
| f1875da780 | |||
| 7e8c745e35 | |||
| 577ecf6750 | |||
| 8f7e5806e3 | |||
| c570505297 | |||
| 148eeb619f | |||
| 877d8c4089 | |||
| 8c1bd139c0 | |||
| bd71947b2a | |||
| 491efe44a3 | |||
|
|
c7229061d0 | ||
| 3a52a9ea4b | |||
| 3c3a2d2766 | |||
| d15e4b8ad9 | |||
|
|
e2974d3152 | ||
|
|
dbee82c69b | ||
|
|
05271c94af | ||
|
|
9d357b24c8 | ||
|
|
62c681819c | ||
|
|
c41fa86d17 | ||
|
|
04608e0e53 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -8,3 +8,7 @@ TAGS
|
||||
tags
|
||||
*.tag
|
||||
.stack-work/
|
||||
.cabal-sandbox/
|
||||
cabal.sandbox.config
|
||||
dist-newstyle/
|
||||
.ghc.environment.*
|
||||
|
||||
67
.travis.yml
Normal file
67
.travis.yml
Normal file
@@ -0,0 +1,67 @@
|
||||
# See https://github.com/hvr/multi-ghc-travis for more information
|
||||
|
||||
language: c
|
||||
|
||||
sudo: required
|
||||
dist: trusty
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- env: CABALVER=1.18 GHCVER=7.6.3
|
||||
addons: {apt: {packages: [cabal-install-1.18,ghc-7.6.3], sources: [hvr-ghc]}}
|
||||
- env: CABALVER=1.22 GHCVER=7.8.4
|
||||
addons: {apt: {packages: [cabal-install-1.22,ghc-7.8.4], sources: [hvr-ghc]}}
|
||||
- env: CABALVER=1.24 GHCVER=7.10.2
|
||||
addons: {apt: {packages: [cabal-install-1.24,ghc-7.10.2], sources: [hvr-ghc]}}
|
||||
- env: CABALVER=1.24 GHCVER=8.0.1
|
||||
addons: {apt: {packages: [cabal-install-1.24,ghc-8.0.1], sources: [hvr-ghc]}}
|
||||
- env: CABALVER=2.0 GHCVER=8.2.2
|
||||
addons: {apt: {packages: [cabal-install-2.0,ghc-8.2.2], sources: [hvr-ghc]}}
|
||||
- env: CABALVER=2.2 GHCVER=8.4.1
|
||||
addons: {apt: {packages: [cabal-install-2.2,ghc-8.4.1], sources: [hvr-ghc]}}
|
||||
- env: CABALVER=head GHCVER=head
|
||||
addons: {apt: {packages: [cabal-install-head,ghc-head], sources: [hvr-ghc]}}
|
||||
|
||||
allow_failures:
|
||||
- env: CABALVER=head GHCVER=head
|
||||
|
||||
env:
|
||||
global:
|
||||
- secure: q++z4DGwOHYjmed00oxMnGhBTzOBzKYunXvVcnCEmvmzW3qZERtXj3B7CLW4vRtmBlo3SiM0fb25NeYao+ByzTjo8jk9noiBVZvffwRmlKCeVwYx7T4/rsDhfV97k2JOeahBSgxWNuTkt+5gv07HpKdTiIxJsiv/QdBxQeq6/Ly6dyRskmCt+VuFvQg+cqPMugxIXtY6F7eZ1zgl/LxlamWjO3E4lX0Myf4o8+SU1HRDVkkVe+ytnRcVcYI2FHuFV/sSoDMTweXQToA9roVjOkfhq4rGlPCuXJkBPyZW2otLXgAV7I2kjwgxqmS5Yw752CcFjMMbG6R1u8sEAcGrJNKHfx8sKqBwI0AVoq4CJn+nKSElTDl0KI1mqazmazK4/mddkD9NGIVXCFmw4b+YGf1uDj8FAR94UmOiEFkEObGkQxG1XK/uzDaUJ1tO3MYXjPPEIE89BJORo+ZskmKFEoqbrBR/vEjbXxJHWP7SaaoM+mWpMiSssEFb/Z5mDBFPb2P/2f7nO4ZDfOYp/9hZdBvDaVM8FmTQfzF6jIUIOFmeeiSZWIBAHoDfdZDRrM/hC5JzqfMumW9frwllsQtYytkAsUqlNnCW86jlc5/5L6D8eY2NERFI2DRqrBi7bP2AfYXsozY0gMO1RL5+iQSQVKlPhk6IyAJYCWCYnrA+dz4=
|
||||
|
||||
before_install:
|
||||
- sudo apt-get install -y hscolour
|
||||
- export PATH=/opt/ghc/$GHCVER/bin:/opt/cabal/$CABALVER/bin:$PATH
|
||||
|
||||
install:
|
||||
- cabal --version
|
||||
- travis_retry cabal update
|
||||
- cabal sandbox init
|
||||
- cabal install --only-dependencies --enable-tests -j
|
||||
|
||||
script:
|
||||
- cabal configure --enable-tests -v2
|
||||
- cabal build
|
||||
- cabal test
|
||||
- cabal check
|
||||
- cabal sdist
|
||||
- cabal haddock --hyperlink-source --html-location=https://hackage.haskell.org/package/\$pkg-\$version/docs/
|
||||
# check that the generated source-distribution can be built & installed
|
||||
- export SRC_TGZ=$(cabal info . | awk '{print $2 ".tar.gz";exit}') ;
|
||||
cd dist/;
|
||||
cabal sandbox init;
|
||||
if [ -f "$SRC_TGZ" ]; then
|
||||
cabal install "$SRC_TGZ" --enable-tests;
|
||||
else
|
||||
echo "expected '$SRC_TGZ' not found";
|
||||
exit 1;
|
||||
fi;
|
||||
cd ..
|
||||
|
||||
after_script:
|
||||
- ./update-gh-pages.sh
|
||||
|
||||
notifications:
|
||||
email:
|
||||
- hasufell@posteo.de
|
||||
|
||||
49
CHANGELOG
49
CHANGELOG
@@ -1,3 +1,52 @@
|
||||
0.9.2
|
||||
* fix build with ghc-7.6
|
||||
* raise required bytestring version
|
||||
* Tighten base bound to prevent building before GHC 7.6 (by George Wilson)
|
||||
0.9.1
|
||||
* fix build with ghc-7.8 and 7.10
|
||||
0.9.0
|
||||
* don't force "Path Abs" anymore in IO module, abstract more over Path types
|
||||
* add 'toAbs'
|
||||
0.8.1
|
||||
* add 'readFile', 'readFileEOF', 'writeFile' and 'appendFile'
|
||||
0.8.0
|
||||
* 'copyDirRecursiveOverwrite', 'copyFileOverwrite', 'easyCopyOverwrite' and 'moveFileOverwrite' have been removed, instead use the versions without the *Overwrite suffix and pass in 'Strict' (for default behavior) or 'Overwrite' as the CopyMode argument
|
||||
* introduced a new 'RecursiveErrorMode' type to allow controlling recursive behavior of 'copyDirRecursive' (use 'FailEarly' for default behavior)
|
||||
* 'createRegularFile' and 'createDir' now take FileMode as a parameter (also see 'newFilePerms' and 'newDirPerms')
|
||||
* various documentation fixes
|
||||
* improved reliability of tests
|
||||
0.7.5:
|
||||
* relicense to BSD3
|
||||
0.7.3:
|
||||
* don't expose HPath.Internal
|
||||
0.7.2:
|
||||
* fix tests, so they work with the sdist tarball too
|
||||
* added the following function to HPath.IO: createSymlink
|
||||
0.7.1:
|
||||
* various cleanups and documentation improvements
|
||||
* added the following functions to System.Posix.FilePath: splitSearchPath, getSearchPath, stripExtension, makeRelative, makeValid
|
||||
0.7.0:
|
||||
* use 'sendfile' from 'simple-sendfile' in _copyFile and do read/write as a fallback only
|
||||
* add isFileName, hasParentDir, hiddenFile to System.Posix.FilePath
|
||||
* add our own openFd version for more control
|
||||
* small documentation improvements
|
||||
* add a getDirectoryContents' version that works on Fd
|
||||
* lift version constraints in benchmark
|
||||
* remove fpToString and userStringToFP, use Data.ByteString.UTF8 directly instead
|
||||
0.6.0:
|
||||
* fixes 'throwDestinationInSource' to be more reliable.
|
||||
* removes some unused HPathIOException constructors
|
||||
* consistently provide exception constructor identifiers
|
||||
* be less harsh when non-supported file types get passed to our functions, possibly ignoring them
|
||||
* minor cleanups
|
||||
0.5.9:
|
||||
* Adds our posix-paths fork and a lot of IO operations.
|
||||
0.5.8:
|
||||
* First version of the fork.
|
||||
0.5.7:
|
||||
* Fix haddock problem.
|
||||
0.5.6:
|
||||
* Reject only .. and .
|
||||
0.5.5:
|
||||
* Use filepath's isValid function for additional sanity checks
|
||||
0.5.4:
|
||||
|
||||
44
LICENSE
44
LICENSE
@@ -1,24 +1,30 @@
|
||||
Copyright (c) 2015–2016, FP Complete
|
||||
Copyright (c) Julian Ospald
|
||||
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of paths nor the
|
||||
names of its contributors may be used to endorse or promote products
|
||||
derived from this software without specific prior written permission.
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the author nor the names of his contributors
|
||||
may be used to endorse or promote products derived from this software
|
||||
without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE CONTRIBUTORS ``AS IS'' AND ANY EXPRESS
|
||||
OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
|
||||
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
||||
OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
|
||||
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
||||
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
567
README.md
567
README.md
@@ -1,518 +1,87 @@
|
||||
# Path
|
||||
# HPath
|
||||
|
||||
Support for well-typed paths in Haskell.
|
||||
[](https://gitter.im/hasufell/hpath?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [](https://hackage.haskell.org/package/hpath) [](http://travis-ci.org/hasufell/hpath) [](http://packdeps.haskellers.com/feed?needle=hpath)
|
||||
|
||||
* [Motivation](#motivation)
|
||||
* [Approach](#approach)
|
||||
* [Solution](#solution)
|
||||
* [Implementation](#implementation)
|
||||
* [The data types](#the-data-types)
|
||||
* [Parsers](#parsers)
|
||||
* [Smart constructors](#smart-constructors)
|
||||
* [Overloaded stings](#overloaded-strings)
|
||||
* [Operations](#operations)
|
||||
* [Review](#review)
|
||||
* [Relative vs absolute confusion](#relative-vs-absolute-confusion)
|
||||
* [The equality problem](#the-equality-problem)
|
||||
* [Unpredictable concatenation issues](#unpredictable-concatenation-issues)
|
||||
* [Confusing files and directories](#confusing-files-and-directories)
|
||||
* [Self-documentation](#self-documentation)
|
||||
* [In practice](#in-practice)
|
||||
* [Doing I/O](#doing-io)
|
||||
* [Doing textual manipulations](#doing-textual-manipulations)
|
||||
* [Accepting user input](#accepting-user-input)
|
||||
* [Comparing with existing path libraries](#comparing-with-existing-path-libraries)
|
||||
* [filepath and system-filepath](#filepath-and-system-filepath)
|
||||
* [system-canonicalpath, canonical-filepath, directory-tree](#system-canonicalpath-canonical-filepath-directory-tree)
|
||||
* [pathtype](#pathtype)
|
||||
* [data-filepath](#data-filepath)
|
||||
* [Summary](#summary)
|
||||
Support for well-typed paths in Haskell. Also provides ByteString based filepath
|
||||
manipulation.
|
||||
|
||||
## Motivation
|
||||
|
||||
It was after working on a number of projects at FP Complete that use file
|
||||
paths in various ways. We used the system-filepath package, which was
|
||||
supposed to solve many path problems by being an opaque path type. It
|
||||
occurred to me that the same kind of bugs kept cropping up:
|
||||
The motivation came during development of
|
||||
[hsfm](https://github.com/hasufell/hsfm)
|
||||
which has a pretty strict File type, but lacks a strict Path type, e.g.
|
||||
for user input.
|
||||
|
||||
* Expected a path to be absolute but it was relative, or vice-versa.
|
||||
The library that came closest to my needs was
|
||||
[path](https://github.com/chrisdone/path),
|
||||
but the API turned out to be oddly complicated for my use case, so I
|
||||
decided to fork it.
|
||||
|
||||
* Expected two equivalent paths to be equal or order the same, but they did
|
||||
not (`/home//foo` vs `/home/foo/` vs `/home/bar/../foo`, etc.).
|
||||
Similarly, [posix-paths](https://github.com/JohnLato/posix-paths)
|
||||
was exactly what I wanted for the low-level operations, but upstream seems dead,
|
||||
so it is forked as well and merged into this library.
|
||||
|
||||
* Unpredictable behaviour with regards to concatenating paths.
|
||||
## Goals
|
||||
|
||||
* Confusing files and directories.
|
||||
* well-typed paths
|
||||
* high-level API to file operations like recursive directory copy
|
||||
* safe filepath manipulation, never using String as filepath, but ByteString
|
||||
* still allowing sufficient control to interact with the underlying low-level calls
|
||||
|
||||
* Not knowing whether a path was a file or directory or relative or absolute
|
||||
based on the type alone was a drag.
|
||||
Note: this library was written for __posix__ systems and it will probably not support other systems.
|
||||
|
||||
All of these bugs are preventable.
|
||||
## Differences to 'path'
|
||||
|
||||
## Approach
|
||||
* doesn't attempt to fake IO-related information into the path, so whether a path points to a file or directory is up to your IO-code to decide...
|
||||
* trailing path separators will be preserved if they exist, no messing with that
|
||||
* uses safe ByteString for filepaths under the hood instead of unsafe String
|
||||
* fixes broken [dirname](https://github.com/chrisdone/path/issues/18)
|
||||
* renames dirname/filename to basename/dirname to match the POSIX shell functions
|
||||
* introduces a new `Path Fn` for safe filename guarantees and a `RelC` class
|
||||
* allows pattern matching via unidirectional PatternSynonym
|
||||
* uses simple doctest for testing
|
||||
* allows `~/` as relative path, because on posix level `~` is just a regular filename that does _NOT_ point to `$HOME`
|
||||
* remove TH, it sucks
|
||||
|
||||
My approach to problems like this is to make a type that encodes the
|
||||
properties I want and then make it impossible to let those invariants be
|
||||
broken, without compromise or backdoors to let the wrong value “slip
|
||||
in”. Once I have a path, I want to be able to trust it fully. This theme
|
||||
will be seen throughout the things I lay out below.
|
||||
## Differences to 'posix-paths'
|
||||
|
||||
## Solution
|
||||
* uses the `word8` package for save word8 literals instead of `OverloadedStrings`
|
||||
* `hasTrailingPathSeparator` and `dropTrailingPathSeparator` behave in the same way as their `System.FilePath` counterpart
|
||||
* added various functions:
|
||||
* `equalFilePath`
|
||||
* `getSearchPath`
|
||||
* `hasParentDir`
|
||||
* `hiddenFile`
|
||||
* `isFileName`
|
||||
* `isValid`
|
||||
* `makeRelative`
|
||||
* `makeValid`
|
||||
* `normalise`
|
||||
* `splitSearchPath`
|
||||
* `stripExtension`
|
||||
* has a custom versions of `openFd` which allows more control over the flags than its unix package counterpart
|
||||
* adds a `getDirectoryContents'` version that works on Fd
|
||||
|
||||
After having to fix bugs due to these in our software, I put my foot down
|
||||
and made:
|
||||
## Examples in ghci
|
||||
|
||||
* An opaque `Path` type (a newtype wrapper around `String`).
|
||||
Start ghci via `cabal repl`:
|
||||
|
||||
* Smart constructors which are very stringent in the parsing.
|
||||
|
||||
* Make the parsers highly normalizing.
|
||||
|
||||
* Leave equality and concatenation to basic string equality and
|
||||
concatenation.
|
||||
|
||||
* Include relativity (absolute/relative) and type (directory/file) in the
|
||||
type itself.
|
||||
|
||||
* Use the already cross-platform
|
||||
[filepath](http://hackage.haskell.org/package/filepath) package for
|
||||
implementation details.
|
||||
|
||||
## Implementation
|
||||
|
||||
### The data types
|
||||
|
||||
Here is the type:
|
||||
|
||||
```haskell
|
||||
newtype Path b t = Path FilePath
|
||||
deriving (Typeable)
|
||||
```hs
|
||||
-- enable OverloadedStrings
|
||||
:set -XOverloadedStrings
|
||||
-- import HPath.IO
|
||||
import HPath.IO
|
||||
-- parse an absolute path
|
||||
abspath <- parseAbs "/home"
|
||||
-- parse a relative path (e.g. user users home directory)
|
||||
relpath <- parseRel "jule"
|
||||
-- concatenate paths
|
||||
let newpath = abspath </> relpath
|
||||
-- get file type
|
||||
getFileType newpath
|
||||
-- return all contents of that directory
|
||||
getDirsFiles newpath
|
||||
-- return all contents of the parent directory
|
||||
getDirsFiles (dirname newpath)
|
||||
```
|
||||
|
||||
The type variables are:
|
||||
|
||||
* `b` — base, the base location of the path; absolute or relative.
|
||||
* `t` — type, whether file or directory.
|
||||
|
||||
The base types can be filled with these:
|
||||
|
||||
```haskell
|
||||
data Abs deriving (Typeable)
|
||||
data Rel deriving (Typeable)
|
||||
```
|
||||
|
||||
And the type can be filled with these:
|
||||
|
||||
```haskell
|
||||
data File deriving (Typeable)
|
||||
data Dir deriving (Typeable)
|
||||
```
|
||||
|
||||
(Why not use data kinds like `data Type = File | Dir`? Because that imposes
|
||||
an extension overhead of adding `{-# LANGUAGE DataKinds #-}` to every module
|
||||
you might want to write out a path type in. Given that one cannot construct
|
||||
paths of types other than these, via the operations in the module, it’s not
|
||||
a concern for me.)
|
||||
|
||||
There is a conversion function to give you back the filepath:
|
||||
|
||||
```haskell
|
||||
toFilePath :: Path b t -> FilePath
|
||||
toFilePath (Path l) = l
|
||||
```
|
||||
|
||||
Beginning from version 0.5.3, there are type-constrained versions of
|
||||
`toFilePath` with the following signatures:
|
||||
|
||||
```haskell
|
||||
fromAbsDir :: Path Abs Dir -> FilePath
|
||||
fromRelDir :: Path Rel Dir -> FilePath
|
||||
fromAbsFile :: Path Abs File -> FilePath
|
||||
fromRelFile :: Path Rel File -> FilePath
|
||||
```
|
||||
|
||||
### Parsers
|
||||
|
||||
To get a `Path` value, you need to use one of the four parsers:
|
||||
|
||||
```haskell
|
||||
parseAbsDir :: MonadThrow m => FilePath -> m (Path Abs Dir)
|
||||
parseRelDir :: MonadThrow m => FilePath -> m (Path Rel Dir)
|
||||
parseAbsFile :: MonadThrow m => FilePath -> m (Path Abs File)
|
||||
parseRelFile :: MonadThrow m => FilePath -> m (Path Rel File)
|
||||
```
|
||||
|
||||
The following properties apply:
|
||||
|
||||
* Absolute parsers will reject non-absolute paths.
|
||||
|
||||
* The only delimiter syntax accepted is the path separator; `/` on POSIX and
|
||||
`\` on Windows.
|
||||
|
||||
* Any other delimiter is rejected; `..`, `~/`, `/./`, etc.
|
||||
|
||||
* All parsers normalize into single separators: `/home//foo` → `/home/foo`.
|
||||
|
||||
* Directory parsers always normalize with a final trailing `/`. So `/home/foo`
|
||||
parses into the string `/home/foo/`.
|
||||
|
||||
It was discussed briefly whether we should just have a class for parsing
|
||||
rather than four separate parsing functions. In my experience so far, I have
|
||||
had type errors where I wrote something `like x <- parseAbsDir
|
||||
someAbsDirString` because `x` was then passed to a place that expected a
|
||||
relative directory. In this way, overloading the return value would’ve just
|
||||
been accepted. So I don’t think having a class is a good idea. Being
|
||||
explicit here doesn’t exactly waste our time, either.
|
||||
|
||||
Why are these functions in `MonadThrow`? Because it means I can have it
|
||||
return an `Either`, or a `Maybe`, if I’m in pure code, and if I’m in `IO`,
|
||||
and I don’t expect parsing to ever fail, I can use it in IO like this:
|
||||
|
||||
```haskell
|
||||
do x <- parseRelFile (fromCabalFileName x)
|
||||
foo x
|
||||
…
|
||||
```
|
||||
|
||||
That’s really convenient and we take advantage of this at FP Complete a lot.
|
||||
The instances
|
||||
|
||||
Equality, ordering and printing are simply re-using the `String` instances:
|
||||
|
||||
```haskell
|
||||
instance Eq (Path b t) where
|
||||
(==) (Path x) (Path y) = x == y
|
||||
|
||||
instance Ord (Path b t) where
|
||||
compare (Path x) (Path y) = compare x y
|
||||
|
||||
instance Show (Path b t) where
|
||||
show (Path x) = show x
|
||||
```
|
||||
|
||||
Which gives us for free the following equational properties:
|
||||
|
||||
```haskell
|
||||
toFilePath x == toFilePath y ≡ x == y -- Eq instance
|
||||
toFilePath x `compare` toFilePath y ≡ x `compare` y -- Ord instance
|
||||
toFilePath x == toFilePath y ≡ show x == show y -- Show instance
|
||||
```
|
||||
|
||||
In other words, the representation and the path you get out at the end are
|
||||
the same. Two paths that are equal will always give you back the same thing.
|
||||
|
||||
### Smart constructors
|
||||
|
||||
For when you know what a path will be at compile-time, there are
|
||||
constructors for that:
|
||||
|
||||
```haskell
|
||||
$(mkAbsDir "/home/chris")
|
||||
$(mkRelDir "chris")
|
||||
$(mkAbsFile "/home/chris/x.txt")
|
||||
$(mkRelFile "chris/x.txt")
|
||||
```
|
||||
|
||||
These will run at compile-time and underneath use the appropriate parser.
|
||||
|
||||
### Overloaded strings
|
||||
|
||||
No `IsString` instance is provided, because that has no way to statically
|
||||
determine whether the path is correct, and would otherwise have to be a
|
||||
partial function.
|
||||
|
||||
In practice I have written the wrong path format in a `$(mk… "")` and been
|
||||
thankful it was caught early.
|
||||
|
||||
### Operations
|
||||
|
||||
There is path concatenation:
|
||||
|
||||
```haskell
|
||||
(</>) :: Path b Dir -> Path Rel t -> Path b t
|
||||
```
|
||||
|
||||
Get the parent directory of a path:
|
||||
|
||||
```haskell
|
||||
parent :: Path Abs t -> Path Abs Dir
|
||||
```
|
||||
|
||||
Get the filename of a file path:
|
||||
|
||||
```haskell
|
||||
filename :: Path b File -> Path Rel File
|
||||
```
|
||||
|
||||
Get the directory name of a directory path:
|
||||
|
||||
```haskell
|
||||
dirname :: Path b Dir -> Path Rel Dir
|
||||
```
|
||||
|
||||
Stripping the parent directory from a path:
|
||||
|
||||
```haskell
|
||||
stripDir :: MonadThrow m => Path b Dir -> Path b t -> m (Path Rel t)
|
||||
```
|
||||
|
||||
## Review
|
||||
|
||||
Let’s review my initial list of complaints and see if they’ve been
|
||||
satisfied.
|
||||
|
||||
### Relative vs absolute confusion
|
||||
|
||||
Paths now distinguish in the type system whether they are relative or
|
||||
absolute. You can’t append two absolute paths, for example:
|
||||
|
||||
```haskell
|
||||
λ> $(mkAbsDir "/home/chris") </> $(mkAbsDir "/home/chris")
|
||||
<interactive>:23:31-55:
|
||||
Couldn't match type ‘Abs’ with ‘Rel’
|
||||
```
|
||||
|
||||
### The equality problem
|
||||
|
||||
Paths are now stringently normalized. They have to be a valid path, and they
|
||||
only support single path separators, and all directories are suffixed with a
|
||||
trailing path separator:
|
||||
|
||||
```haskell
|
||||
λ> $(mkAbsDir "/home/chris//") == $(mkAbsDir "/./home//chris")
|
||||
True
|
||||
λ> toFilePath $(mkAbsDir "/home/chris//") ==
|
||||
toFilePath $(mkAbsDir "/./home//chris")
|
||||
True
|
||||
λ> ($(mkAbsDir "/home/chris//"),toFilePath $(mkAbsDir "/./home//chris"))
|
||||
("/home/chris/","/home/chris/")
|
||||
```
|
||||
|
||||
### Unpredictable concatenation issues
|
||||
|
||||
Because of the stringent normalization, path concatenation, as seen above,
|
||||
is simply string concatenation. This is about as predictable as it can get:
|
||||
|
||||
```haskell
|
||||
λ> toFilePath $(mkAbsDir "/home/chris//")
|
||||
"/home/chris/"
|
||||
λ> toFilePath $(mkRelDir "foo//bar")
|
||||
"foo/bar/"
|
||||
λ> $(mkAbsDir "/home/chris//") </> $(mkRelDir "foo//bar")
|
||||
"/home/chris/foo/bar/"
|
||||
```
|
||||
|
||||
### Confusing files and directories
|
||||
|
||||
Now that the path type is encoded in the type system, our `</>` operator
|
||||
prevents improper appending:
|
||||
|
||||
```haskell
|
||||
λ> $(mkAbsDir "/home/chris/") </> $(mkRelFile "foo//bar")
|
||||
"/home/chris/foo/bar"
|
||||
λ> $(mkAbsFile "/home/chris") </> $(mkRelFile "foo//bar")
|
||||
<interactive>:35:1-26:
|
||||
Couldn't match type ‘File’ with ‘Dir’
|
||||
```
|
||||
|
||||
### Self-documentation
|
||||
|
||||
Now I can read the path like:
|
||||
|
||||
```haskell
|
||||
{ fooPath :: Path Rel Dir, ... }
|
||||
```
|
||||
|
||||
And know that this refers to the directory relative to some other path,
|
||||
meaning I should be careful to consider the current directory when using
|
||||
this in IO, or that I’ll probably need a parent to append to it at some
|
||||
point.
|
||||
|
||||
## In practice
|
||||
|
||||
We’ve been using this at FP Complete in a number of packages for some months
|
||||
now, it’s turned out surprisingly sufficient for most of our path work with
|
||||
only one bug found. We weren’t sure initially whether it would just be too
|
||||
much of a pain to use, but really it’s quite acceptable given the
|
||||
advantages. You can see its use all over the
|
||||
[`stack`](https://github.com/commercialhaskell/stack) codebase.
|
||||
|
||||
## Doing I/O
|
||||
|
||||
Currently any operations involving I/O can be done by using the existing I/O
|
||||
library:
|
||||
|
||||
```haskell
|
||||
doesFileExist (toFilePath fp)
|
||||
readFile (toFilePath fp)
|
||||
```
|
||||
|
||||
etc. This has problems with respect to accidentally running something like:
|
||||
|
||||
```haskell
|
||||
doesFileExist $(mkRelDir "foo")
|
||||
```
|
||||
|
||||
But I/O is currently outside the scope of what this package solves. Once you
|
||||
leave the realm of the `Path` type invariants are back to your responsibility.
|
||||
|
||||
As with the original version of this library, we’re currently building up a
|
||||
set of functions in a `Path.IO` module over time that fits our real-world
|
||||
use-cases. It may or may not appear in the path package eventually. It’ll
|
||||
need cleaning up and considering what should really be included.
|
||||
|
||||
**Edit:** There is now
|
||||
[`path-io`](https://hackage.haskell.org/package/path-io) package that
|
||||
complements the `path` library and includes complete well-typed interface to
|
||||
[`directory`](https://hackage.haskell.org/package/directory) and
|
||||
[`temporary`](https://hackage.haskell.org/package/temporary). There is work
|
||||
to add more generally useful functions from Stack's `Path.IO` to it and make
|
||||
Stack depend on the `path-io` package.
|
||||
|
||||
## Doing textual manipulations
|
||||
|
||||
One problem that crops up sometimes is wanting to manipulate
|
||||
paths. Currently the way we do it is via the filepath library and re-parsing
|
||||
the path:
|
||||
|
||||
```haskell
|
||||
parseAbsFile . addExtension "/directory/path" "ext" . toFilePath
|
||||
```
|
||||
|
||||
It doesn’t happen too often, in our experience, to the extent this needs to
|
||||
be more convenient.
|
||||
|
||||
## Accepting user input
|
||||
|
||||
Sometimes you have user input that contains `../`. The solution we went with
|
||||
is to have a function like `resolveDir`:
|
||||
|
||||
```haskell
|
||||
resolveDir :: (MonadIO m, MonadThrow m)
|
||||
=> Path Abs Dir -> FilePath -> m (Path Abs Dir)
|
||||
```
|
||||
|
||||
Which will call `canonicalizePath` which collapses and normalizes a path and
|
||||
then we parse with regular old `parseAbsDir` and we’re cooking with
|
||||
gas. This and others like it might get added to the `path` package.
|
||||
|
||||
## Comparing with existing path libraries
|
||||
|
||||
### filepath and system-filepath
|
||||
|
||||
The [filepath](http://hackage.haskell.org/package/filepath) package is
|
||||
intended as the complimentary package to be used before parsing into a Path
|
||||
value, and/or after printing from a Path value. The package itself contains
|
||||
no type-safety, instead contains a range of cross-platform textual
|
||||
operations. Definitely reach for this library when you want to do more
|
||||
involved manipulations.
|
||||
|
||||
The `system-filepath` package is deprecated in favour of `filepath`.
|
||||
|
||||
### system-canonicalpath, canonical-filepath, directory-tree
|
||||
|
||||
The
|
||||
[`system-canonicalpath`](http://hackage.haskell.org/package/system-canonicalpath)
|
||||
and the
|
||||
[`canonical-filepath`](http://hackage.haskell.org/package/canonical-filepath)
|
||||
packages both are a kind of subset of `path`. They canonicalize a string
|
||||
into an opaque path, but neither distinguish directories from files or
|
||||
absolute/relative. Useful if you just want a canonical path but doesn’t do
|
||||
anything else.
|
||||
|
||||
The [`directory-tree`](http://hackage.haskell.org/package/directory-tree)
|
||||
package contains a sum type of dir/file/etc but doesn’t distinguish in its
|
||||
operations relativity or path type.
|
||||
|
||||
### pathtype
|
||||
|
||||
Finally, we come to a path library that path is similar to: the
|
||||
[`pathtype`](http://hackage.haskell.org/package/pathtype) library. There are
|
||||
the same types of `Path Abs File` / `Path Rel Dir`, etc.
|
||||
|
||||
The points where this library isn’t enough for me are:
|
||||
|
||||
* There is an `IsString` instance, which means people will use it, and will
|
||||
make mistakes.
|
||||
|
||||
* Paths are not normalized into a predictable format, leading to me being
|
||||
unsure when equality will succeed. This is the same problem I encountered
|
||||
in `system-filepath`. The equality function normalizes, but according to
|
||||
what properties I can reason about? I don’t know.
|
||||
|
||||
```haskell
|
||||
System.Path.Posix> ("/tmp//" :: Path a Dir) == ("/tmp" :: Path a Dir)
|
||||
True
|
||||
System.Path.Posix> ("tmp" :: Path a Dir) == ("/tmp" :: Path a Dir)
|
||||
True
|
||||
System.Path.Posix> ("/etc/passwd/" :: Path a b) == ("/etc/passwd" :: Path a b)
|
||||
True
|
||||
System.Path.Posix> ("/tmp//" :: Path Abs Dir) == ("/tmp/./" :: Path Abs Dir)
|
||||
False
|
||||
System.Path.Posix> ("/tmp/../" :: Path Abs Dir) == ("/" :: Path Abs Dir)
|
||||
False
|
||||
```
|
||||
* Empty string should not be allowed, and introduction of `.` due to that
|
||||
gets weird:
|
||||
|
||||
```haskell
|
||||
System.Path.Posix> fmap getPathString (Right ("." :: Path Rel File))
|
||||
Right "."
|
||||
System.Path.Posix> fmap getPathString (mkPathAbsOrRel "")
|
||||
Right "."
|
||||
System.Path.Posix> (Right ("." :: Path Rel File)) == (mkPathAbsOrRel "")
|
||||
False
|
||||
System.Path.Posix> takeDirectory ("tmp" :: Path Rel Dir)
|
||||
.
|
||||
System.Path.Posix> (getPathString ("." :: Path Rel File) ==
|
||||
getPathString ("" :: Path Rel File))
|
||||
True
|
||||
System.Path.Posix> (("." :: Path Rel File) == ("" :: Path Rel File))
|
||||
False
|
||||
```
|
||||
|
||||
* It has functions like `<.>/addExtension` which lets you insert an
|
||||
arbitrary string into a path.
|
||||
|
||||
* Some functions let you produce nonsense (could be prevented by a stricter
|
||||
type), for example:
|
||||
|
||||
```haskell
|
||||
System.Path.Posix> takeFileName ("/tmp/" :: Path Abs Dir)
|
||||
tmp
|
||||
```
|
||||
|
||||
I’m being a bit picky here, a bit unfair. But the point is really to show
|
||||
the kind of things I tried to avoid in `path`. In summary, it’s just hard to
|
||||
know where things can go wrong, similar to what was going on in
|
||||
`system-filepath`.
|
||||
|
||||
### data-filepath
|
||||
|
||||
The [`data-filepath`](https://hackage.haskell.org/package/data-filepath) is
|
||||
also very similar, I discovered it after writing my own at work and was
|
||||
pleased to see it’s mostly the same. The main differences are:
|
||||
|
||||
* Uses `DataKinds` for the relative/absolute and file/dir distinction which
|
||||
as I said above is an overhead.
|
||||
|
||||
* Uses a GADT for the path type, which is fine. In my case I wanted to
|
||||
retain the original string which functions that work on the `FilePath`
|
||||
(`String`) type already deal with well. It does change the parsing step
|
||||
somewhat, because it parses into segments.
|
||||
|
||||
* It’s more lenient at parsing (allowing `..` and trailing `.`).
|
||||
|
||||
The API is a bit awkward to just parse a directory, requires a couple
|
||||
functions to get it (going via `WeakFilePath`), returning only an `Either`,
|
||||
and there are no functions like parent. But there’s not much to complain
|
||||
about. It’s a fine library, but I didn’t feel the need to drop my own in
|
||||
favor of it. Check it out and decide for yourself.
|
||||
|
||||
## Summary
|
||||
|
||||
There’s a growing interest in making practical use of well-typed file path
|
||||
handling. I think everyone’s wanted it for a while, but few people have
|
||||
really committed to it in practice. Now that I’ve been using `path` for a
|
||||
while, I can’t really go back. It’ll be interesting to see what new packages
|
||||
crop up in the coming year, I expect there’ll be more.
|
||||
|
||||
7
cbits/dirutils.c
Normal file
7
cbits/dirutils.c
Normal file
@@ -0,0 +1,7 @@
|
||||
#include "dirutils.h"
|
||||
unsigned int
|
||||
__posixdir_d_type(struct dirent* d)
|
||||
{
|
||||
return(d -> d_type);
|
||||
}
|
||||
|
||||
13
cbits/dirutils.h
Normal file
13
cbits/dirutils.h
Normal file
@@ -0,0 +1,13 @@
|
||||
#ifndef POSIXPATHS_CBITS_DIRUTILS_H
|
||||
#define POSIXPATHS_CBITS_DIRUTILS_H
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <dirent.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/stat.h>
|
||||
#include <fcntl.h>
|
||||
|
||||
extern unsigned int
|
||||
__posixdir_d_type(struct dirent* d)
|
||||
;
|
||||
#endif
|
||||
13
doctests-hpath.hs
Normal file
13
doctests-hpath.hs
Normal file
@@ -0,0 +1,13 @@
|
||||
module Main where
|
||||
|
||||
|
||||
import Test.DocTest
|
||||
import Test.HUnit
|
||||
|
||||
main =
|
||||
doctest
|
||||
["-isrc"
|
||||
, "-XOverloadedStrings"
|
||||
, "src/HPath.hs"
|
||||
]
|
||||
|
||||
25
doctests-posix.hs
Normal file
25
doctests-posix.hs
Normal file
@@ -0,0 +1,25 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module Main where
|
||||
|
||||
import Control.Applicative
|
||||
import System.Posix.Directory.Traversals
|
||||
|
||||
import Test.DocTest
|
||||
import Test.HUnit
|
||||
|
||||
main = do
|
||||
doctest
|
||||
[ "-isrc"
|
||||
, "-XOverloadedStrings"
|
||||
, "System.Posix.FilePath"
|
||||
]
|
||||
runTestTT unitTests
|
||||
|
||||
|
||||
unitTests :: Test
|
||||
unitTests = test
|
||||
[ TestCase $ do
|
||||
r <- (==) <$> allDirectoryContents "." <*> allDirectoryContents' "."
|
||||
assertBool "allDirectoryContents == allDirectoryContents'" r
|
||||
]
|
||||
132
hpath.cabal
Normal file
132
hpath.cabal
Normal file
@@ -0,0 +1,132 @@
|
||||
name: hpath
|
||||
version: 0.9.2
|
||||
synopsis: Support for well-typed paths
|
||||
description: Support for well-typed paths, utilizing ByteString under the hood.
|
||||
license: BSD3
|
||||
license-file: LICENSE
|
||||
author: Julian Ospald <hasufell@posteo.de>
|
||||
maintainer: Julian Ospald <hasufell@posteo.de>
|
||||
copyright: Julian Ospald 2016
|
||||
category: Filesystem
|
||||
build-type: Simple
|
||||
cabal-version: 1.14
|
||||
extra-source-files: README.md
|
||||
CHANGELOG
|
||||
cbits/dirutils.h
|
||||
doctests-hpath.hs
|
||||
doctests-posix.hs
|
||||
|
||||
library
|
||||
if os(windows)
|
||||
build-depends: unbuildable<0
|
||||
buildable: False
|
||||
hs-source-dirs: src/
|
||||
default-language: Haskell2010
|
||||
if impl(ghc >= 8.0)
|
||||
ghc-options: -Wall -Wno-redundant-constraints
|
||||
else
|
||||
ghc-options: -Wall
|
||||
c-sources: cbits/dirutils.c
|
||||
exposed-modules: HPath,
|
||||
HPath.IO,
|
||||
HPath.IO.Errors,
|
||||
System.Posix.Directory.Foreign,
|
||||
System.Posix.Directory.Traversals,
|
||||
System.Posix.FD,
|
||||
System.Posix.FilePath
|
||||
other-modules: HPath.Internal
|
||||
build-depends: base >= 4.6 && <5
|
||||
, IfElse
|
||||
, bytestring >= 0.10.0.0
|
||||
, deepseq
|
||||
, exceptions
|
||||
, hspec
|
||||
, simple-sendfile >= 0.2.24
|
||||
, unix >= 2.5
|
||||
, unix-bytestring
|
||||
, utf8-string
|
||||
, word8
|
||||
|
||||
|
||||
test-suite doctests-hpath
|
||||
if os(windows)
|
||||
build-depends: unbuildable<0
|
||||
buildable: False
|
||||
default-language: Haskell2010
|
||||
type: exitcode-stdio-1.0
|
||||
ghc-options: -threaded
|
||||
main-is: doctests-hpath.hs
|
||||
build-depends: base
|
||||
, HUnit
|
||||
, QuickCheck
|
||||
, doctest >= 0.8
|
||||
, hpath
|
||||
|
||||
test-suite doctests-posix
|
||||
if os(windows)
|
||||
build-depends: unbuildable<0
|
||||
buildable: False
|
||||
default-language: Haskell2010
|
||||
type: exitcode-stdio-1.0
|
||||
ghc-options: -threaded
|
||||
main-is: doctests-posix.hs
|
||||
build-depends: base,
|
||||
bytestring >= 0.10.0.0,
|
||||
unix,
|
||||
hpath,
|
||||
doctest >= 0.8,
|
||||
HUnit,
|
||||
QuickCheck
|
||||
|
||||
test-suite spec
|
||||
if os(windows)
|
||||
build-depends: unbuildable<0
|
||||
buildable: False
|
||||
Type: exitcode-stdio-1.0
|
||||
Default-Language: Haskell2010
|
||||
Hs-Source-Dirs: test
|
||||
Main-Is: Main.hs
|
||||
other-modules:
|
||||
HPath.IO.AppendFileSpec
|
||||
HPath.IO.CanonicalizePathSpec
|
||||
HPath.IO.CopyDirRecursiveCollectFailuresSpec
|
||||
HPath.IO.CopyDirRecursiveOverwriteSpec
|
||||
HPath.IO.CopyDirRecursiveSpec
|
||||
HPath.IO.CopyFileOverwriteSpec
|
||||
HPath.IO.CopyFileSpec
|
||||
HPath.IO.CreateDirRecursiveSpec
|
||||
HPath.IO.CreateDirSpec
|
||||
HPath.IO.CreateRegularFileSpec
|
||||
HPath.IO.CreateSymlinkSpec
|
||||
HPath.IO.DeleteDirRecursiveSpec
|
||||
HPath.IO.DeleteDirSpec
|
||||
HPath.IO.DeleteFileSpec
|
||||
HPath.IO.GetDirsFilesSpec
|
||||
HPath.IO.GetFileTypeSpec
|
||||
HPath.IO.MoveFileOverwriteSpec
|
||||
HPath.IO.MoveFileSpec
|
||||
HPath.IO.ReadFileEOFSpec
|
||||
HPath.IO.ReadFileSpec
|
||||
HPath.IO.RecreateSymlinkOverwriteSpec
|
||||
HPath.IO.RecreateSymlinkSpec
|
||||
HPath.IO.RenameFileSpec
|
||||
HPath.IO.ToAbsSpec
|
||||
HPath.IO.WriteFileSpec
|
||||
Spec
|
||||
Utils
|
||||
GHC-Options: -Wall
|
||||
Build-Depends: base
|
||||
, HUnit
|
||||
, IfElse
|
||||
, bytestring >= 0.10.0.0
|
||||
, hpath
|
||||
, hspec >= 1.3
|
||||
, process
|
||||
, unix
|
||||
, unix-bytestring
|
||||
, utf8-string
|
||||
|
||||
source-repository head
|
||||
type: git
|
||||
location: https://github.com/hasufell/hpath
|
||||
|
||||
37
path.cabal
37
path.cabal
@@ -1,37 +0,0 @@
|
||||
name: path
|
||||
version: 0.5.5
|
||||
synopsis: Support for well-typed paths
|
||||
description: Support for will-typed paths.
|
||||
license: BSD3
|
||||
license-file: LICENSE
|
||||
author: Chris Done <chrisdone@fpcomplete.com>
|
||||
maintainer: Chris Done <chrisdone@fpcomplete.com>
|
||||
copyright: 2015–2016 FP Complete
|
||||
category: Filesystem
|
||||
build-type: Simple
|
||||
cabal-version: >=1.8
|
||||
extra-source-files: README.md, CHANGELOG
|
||||
|
||||
library
|
||||
hs-source-dirs: src/
|
||||
ghc-options: -Wall -O2
|
||||
exposed-modules: Path, Path.Internal
|
||||
build-depends: base >= 4 && <5
|
||||
, exceptions
|
||||
, filepath
|
||||
, template-haskell
|
||||
, deepseq
|
||||
|
||||
test-suite test
|
||||
type: exitcode-stdio-1.0
|
||||
main-is: Main.hs
|
||||
hs-source-dirs: test
|
||||
build-depends: HUnit
|
||||
, base
|
||||
, hspec
|
||||
, mtl
|
||||
, path
|
||||
|
||||
source-repository head
|
||||
type: git
|
||||
location: https://github.com/chrisdone/path.git
|
||||
376
src/HPath.hs
Normal file
376
src/HPath.hs
Normal file
@@ -0,0 +1,376 @@
|
||||
-- |
|
||||
-- Module : HPath
|
||||
-- Copyright : © 2015–2016 FP Complete, 2016 Julian Ospald
|
||||
-- License : BSD 3 clause
|
||||
--
|
||||
-- Maintainer : Julian Ospald <hasufell@posteo.de>
|
||||
-- Stability : experimental
|
||||
-- Portability : portable
|
||||
--
|
||||
-- Support for well-typed paths.
|
||||
|
||||
|
||||
{-# LANGUAGE CPP #-}
|
||||
{-# LANGUAGE DeriveDataTypeable #-}
|
||||
{-# LANGUAGE EmptyDataDecls #-}
|
||||
#if __GLASGOW_HASKELL__ >= 708
|
||||
{-# LANGUAGE PatternSynonyms #-}
|
||||
#endif
|
||||
|
||||
module HPath
|
||||
(
|
||||
-- * Types
|
||||
Abs
|
||||
,Path
|
||||
,Rel
|
||||
,Fn
|
||||
,PathParseException
|
||||
,PathException
|
||||
,RelC
|
||||
#if __GLASGOW_HASKELL__ >= 708
|
||||
-- * PatternSynonyms/ViewPatterns
|
||||
,pattern Path
|
||||
#endif
|
||||
-- * Path Parsing
|
||||
,parseAbs
|
||||
,parseFn
|
||||
,parseRel
|
||||
-- * Path Conversion
|
||||
,fromAbs
|
||||
,fromRel
|
||||
,toFilePath
|
||||
-- * Path Operations
|
||||
,(</>)
|
||||
,basename
|
||||
,dirname
|
||||
,isParentOf
|
||||
,getAllParents
|
||||
,stripDir
|
||||
-- * Path IO helpers
|
||||
,withAbsPath
|
||||
,withRelPath
|
||||
,withFnPath
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Exception (Exception)
|
||||
import Control.Monad.Catch (MonadThrow(..))
|
||||
#if MIN_VERSION_bytestring(0,10,8)
|
||||
import Data.ByteString(ByteString, stripPrefix)
|
||||
#else
|
||||
import Data.ByteString(ByteString)
|
||||
import qualified Data.List as L
|
||||
#endif
|
||||
import qualified Data.ByteString as BS
|
||||
import Data.Data
|
||||
import Data.Maybe
|
||||
import Data.Word8
|
||||
import HPath.Internal
|
||||
import System.Posix.FilePath hiding ((</>))
|
||||
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Types
|
||||
|
||||
-- | An absolute path.
|
||||
data Abs deriving (Typeable)
|
||||
|
||||
-- | A relative path; one without a root.
|
||||
data Rel deriving (Typeable)
|
||||
|
||||
-- | A filename, without any '/'.
|
||||
data Fn deriving (Typeable)
|
||||
|
||||
-- | Exception when parsing a location.
|
||||
data PathParseException
|
||||
= InvalidAbs ByteString
|
||||
| InvalidRel ByteString
|
||||
| InvalidFn ByteString
|
||||
| Couldn'tStripPrefixTPS ByteString ByteString
|
||||
deriving (Show,Typeable)
|
||||
instance Exception PathParseException
|
||||
|
||||
data PathException = RootDirHasNoBasename
|
||||
deriving (Show,Typeable)
|
||||
instance Exception PathException
|
||||
|
||||
class RelC m
|
||||
|
||||
instance RelC Rel
|
||||
instance RelC Fn
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- PatternSynonyms
|
||||
|
||||
#if __GLASGOW_HASKELL__ >= 710
|
||||
pattern Path :: ByteString -> Path a
|
||||
#endif
|
||||
#if __GLASGOW_HASKELL__ >= 708
|
||||
pattern Path x <- (MkPath x)
|
||||
#endif
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Path Parsers
|
||||
|
||||
|
||||
|
||||
-- | Get a location for an absolute path. Produces a normalised path.
|
||||
--
|
||||
-- Throws: 'PathParseException'
|
||||
--
|
||||
-- >>> parseAbs "/abc" :: Maybe (Path Abs)
|
||||
-- Just "/abc"
|
||||
-- >>> parseAbs "/" :: Maybe (Path Abs)
|
||||
-- Just "/"
|
||||
-- >>> parseAbs "/abc/def" :: Maybe (Path Abs)
|
||||
-- Just "/abc/def"
|
||||
-- >>> parseAbs "/abc/def/.///" :: Maybe (Path Abs)
|
||||
-- Just "/abc/def/"
|
||||
-- >>> parseAbs "abc" :: Maybe (Path Abs)
|
||||
-- Nothing
|
||||
-- >>> parseAbs "" :: Maybe (Path Abs)
|
||||
-- Nothing
|
||||
-- >>> parseAbs "/abc/../foo" :: Maybe (Path Abs)
|
||||
-- Nothing
|
||||
parseAbs :: MonadThrow m
|
||||
=> ByteString -> m (Path Abs)
|
||||
parseAbs filepath =
|
||||
if isAbsolute filepath &&
|
||||
isValid filepath &&
|
||||
not (hasParentDir filepath)
|
||||
then return (MkPath $ normalise filepath)
|
||||
else throwM (InvalidAbs filepath)
|
||||
|
||||
|
||||
-- | Get a location for a relative path. Produces a normalised
|
||||
-- path.
|
||||
--
|
||||
-- Note that @filepath@ may contain any number of @./@ but may not consist
|
||||
-- solely of @./@. It also may not contain a single @..@ anywhere.
|
||||
--
|
||||
-- Throws: 'PathParseException'
|
||||
--
|
||||
-- >>> parseRel "abc" :: Maybe (Path Rel)
|
||||
-- Just "abc"
|
||||
-- >>> parseRel "def/" :: Maybe (Path Rel)
|
||||
-- Just "def/"
|
||||
-- >>> parseRel "abc/def" :: Maybe (Path Rel)
|
||||
-- Just "abc/def"
|
||||
-- >>> parseRel "abc/def/." :: Maybe (Path Rel)
|
||||
-- Just "abc/def/"
|
||||
-- >>> parseRel "/abc" :: Maybe (Path Rel)
|
||||
-- Nothing
|
||||
-- >>> parseRel "" :: Maybe (Path Rel)
|
||||
-- Nothing
|
||||
-- >>> parseRel "abc/../foo" :: Maybe (Path Rel)
|
||||
-- Nothing
|
||||
-- >>> parseRel "." :: Maybe (Path Rel)
|
||||
-- Nothing
|
||||
-- >>> parseRel ".." :: Maybe (Path Rel)
|
||||
-- Nothing
|
||||
parseRel :: MonadThrow m
|
||||
=> ByteString -> m (Path Rel)
|
||||
parseRel filepath =
|
||||
if not (isAbsolute filepath) &&
|
||||
filepath /= BS.singleton _period &&
|
||||
filepath /= BS.pack [_period, _period] &&
|
||||
not (hasParentDir filepath) &&
|
||||
isValid filepath
|
||||
then return (MkPath $ normalise filepath)
|
||||
else throwM (InvalidRel filepath)
|
||||
|
||||
|
||||
-- | Parses a filename. Filenames must not contain slashes.
|
||||
-- Excludes '.' and '..'.
|
||||
--
|
||||
-- Throws: 'PathParseException'
|
||||
--
|
||||
-- >>> parseFn "abc" :: Maybe (Path Fn)
|
||||
-- Just "abc"
|
||||
-- >>> parseFn "..." :: Maybe (Path Fn)
|
||||
-- Just "..."
|
||||
-- >>> parseFn "def/" :: Maybe (Path Fn)
|
||||
-- Nothing
|
||||
-- >>> parseFn "abc/def" :: Maybe (Path Fn)
|
||||
-- Nothing
|
||||
-- >>> parseFn "abc/def/." :: Maybe (Path Fn)
|
||||
-- Nothing
|
||||
-- >>> parseFn "/abc" :: Maybe (Path Fn)
|
||||
-- Nothing
|
||||
-- >>> parseFn "" :: Maybe (Path Fn)
|
||||
-- Nothing
|
||||
-- >>> parseFn "abc/../foo" :: Maybe (Path Fn)
|
||||
-- Nothing
|
||||
-- >>> parseFn "." :: Maybe (Path Fn)
|
||||
-- Nothing
|
||||
-- >>> parseFn ".." :: Maybe (Path Fn)
|
||||
-- Nothing
|
||||
parseFn :: MonadThrow m
|
||||
=> ByteString -> m (Path Fn)
|
||||
parseFn filepath =
|
||||
if isFileName filepath &&
|
||||
filepath /= BS.singleton _period &&
|
||||
filepath /= BS.pack [_period, _period] &&
|
||||
isValid filepath
|
||||
then return (MkPath filepath)
|
||||
else throwM (InvalidFn filepath)
|
||||
|
||||
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Path Conversion
|
||||
|
||||
-- | Convert any Path to a ByteString type.
|
||||
toFilePath :: Path b -> ByteString
|
||||
toFilePath (MkPath l) = l
|
||||
|
||||
-- | Convert an absolute Path to a ByteString type.
|
||||
fromAbs :: Path Abs -> ByteString
|
||||
fromAbs = toFilePath
|
||||
|
||||
-- | Convert a relative Path to a ByteString type.
|
||||
fromRel :: RelC r => Path r -> ByteString
|
||||
fromRel = toFilePath
|
||||
|
||||
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Path Operations
|
||||
|
||||
-- | Append two paths.
|
||||
--
|
||||
-- The second argument must always be a relative path, which ensures
|
||||
-- that undefinable things like `"/abc" </> "/def"` cannot happen.
|
||||
--
|
||||
-- Technically, the first argument can be a path that points to a non-directory,
|
||||
-- because this library is IO-agnostic and makes no assumptions about
|
||||
-- file types.
|
||||
--
|
||||
-- >>> (MkPath "/") </> (MkPath "file" :: Path Rel)
|
||||
-- "/file"
|
||||
-- >>> (MkPath "/path/to") </> (MkPath "file" :: Path Rel)
|
||||
-- "/path/to/file"
|
||||
-- >>> (MkPath "/") </> (MkPath "file/lal" :: Path Rel)
|
||||
-- "/file/lal"
|
||||
-- >>> (MkPath "/") </> (MkPath "file/" :: Path Rel)
|
||||
-- "/file/"
|
||||
(</>) :: RelC r => Path b -> Path r -> Path b
|
||||
(</>) (MkPath a) (MkPath b) = MkPath (a' `BS.append` b)
|
||||
where
|
||||
a' = if BS.last a == pathSeparator
|
||||
then a
|
||||
else addTrailingPathSeparator a
|
||||
|
||||
-- | Strip directory from path, making it relative to that directory.
|
||||
-- Throws 'Couldn'tStripPrefixDir' if directory is not a parent of the path.
|
||||
--
|
||||
-- The bases must match.
|
||||
--
|
||||
-- >>> (MkPath "/lal/lad") `stripDir` (MkPath "/lal/lad/fad") :: Maybe (Path Rel)
|
||||
-- Just "fad"
|
||||
-- >>> (MkPath "lal/lad") `stripDir` (MkPath "lal/lad/fad") :: Maybe (Path Rel)
|
||||
-- Just "fad"
|
||||
-- >>> (MkPath "/") `stripDir` (MkPath "/") :: Maybe (Path Rel)
|
||||
-- Nothing
|
||||
-- >>> (MkPath "/lal/lad/fad") `stripDir` (MkPath "/lal/lad") :: Maybe (Path Rel)
|
||||
-- Nothing
|
||||
-- >>> (MkPath "fad") `stripDir` (MkPath "fad") :: Maybe (Path Rel)
|
||||
-- Nothing
|
||||
stripDir :: MonadThrow m
|
||||
=> Path b -> Path b -> m (Path Rel)
|
||||
stripDir (MkPath p) (MkPath l) =
|
||||
case stripPrefix p' l of
|
||||
Nothing -> throwM (Couldn'tStripPrefixTPS p' l)
|
||||
Just ok -> if BS.null ok
|
||||
then throwM (Couldn'tStripPrefixTPS p' l)
|
||||
else return (MkPath ok)
|
||||
where
|
||||
p' = addTrailingPathSeparator p
|
||||
|
||||
-- | Is p a parent of the given location? Implemented in terms of
|
||||
-- 'stripDir'. The bases must match.
|
||||
--
|
||||
-- >>> (MkPath "/lal/lad") `isParentOf` (MkPath "/lal/lad/fad")
|
||||
-- True
|
||||
-- >>> (MkPath "lal/lad") `isParentOf` (MkPath "lal/lad/fad")
|
||||
-- True
|
||||
-- >>> (MkPath "/") `isParentOf` (MkPath "/")
|
||||
-- False
|
||||
-- >>> (MkPath "/lal/lad/fad") `isParentOf` (MkPath "/lal/lad")
|
||||
-- False
|
||||
-- >>> (MkPath "fad") `isParentOf` (MkPath "fad")
|
||||
-- False
|
||||
isParentOf :: Path b -> Path b -> Bool
|
||||
isParentOf p l = isJust (stripDir p l :: Maybe (Path Rel))
|
||||
|
||||
|
||||
-- |Get all parents of a path.
|
||||
--
|
||||
-- >>> getAllParents (MkPath "/abs/def/dod")
|
||||
-- ["/abs/def","/abs","/"]
|
||||
-- >>> getAllParents (MkPath "/")
|
||||
-- []
|
||||
getAllParents :: Path Abs -> [Path Abs]
|
||||
getAllParents (MkPath p)
|
||||
| np == BS.singleton pathSeparator = []
|
||||
| otherwise = dirname (MkPath np) : getAllParents (dirname $ MkPath np)
|
||||
where
|
||||
np = dropTrailingPathSeparator . normalise $ p
|
||||
|
||||
|
||||
-- | Extract the directory name of a path.
|
||||
--
|
||||
-- >>> dirname (MkPath "/abc/def/dod")
|
||||
-- "/abc/def"
|
||||
-- >>> dirname (MkPath "/")
|
||||
-- "/"
|
||||
dirname :: Path Abs -> Path Abs
|
||||
dirname (MkPath fp) = MkPath (takeDirectory $ dropTrailingPathSeparator fp)
|
||||
|
||||
-- | Extract the file part of a path.
|
||||
--
|
||||
--
|
||||
-- The following properties hold:
|
||||
--
|
||||
-- @basename (p \<\/> a) == basename a@
|
||||
--
|
||||
-- Throws: `PathException` if given the root path "/"
|
||||
--
|
||||
-- >>> basename (MkPath "/abc/def/dod") :: Maybe (Path Fn)
|
||||
-- Just "dod"
|
||||
-- >>> basename (MkPath "/abc/def/dod/") :: Maybe (Path Fn)
|
||||
-- Just "dod"
|
||||
-- >>> basename (MkPath "/") :: Maybe (Path Fn)
|
||||
-- Nothing
|
||||
basename :: MonadThrow m => Path b -> m (Path Fn)
|
||||
basename (MkPath l)
|
||||
| not (isAbsolute rl) = return $ MkPath rl
|
||||
| otherwise = throwM RootDirHasNoBasename
|
||||
where
|
||||
rl = last . splitPath . dropTrailingPathSeparator $ l
|
||||
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Path IO helpers
|
||||
|
||||
|
||||
withAbsPath :: Path Abs -> (ByteString -> IO a) -> IO a
|
||||
withAbsPath (MkPath p) action = action p
|
||||
|
||||
|
||||
withRelPath :: Path Rel -> (ByteString -> IO a) -> IO a
|
||||
withRelPath (MkPath p) action = action p
|
||||
|
||||
|
||||
withFnPath :: Path Fn -> (ByteString -> IO a) -> IO a
|
||||
withFnPath (MkPath p) action = action p
|
||||
|
||||
|
||||
------------------------
|
||||
-- ByteString helpers
|
||||
|
||||
#if MIN_VERSION_bytestring(0,10,8)
|
||||
#else
|
||||
stripPrefix :: ByteString -> ByteString -> Maybe ByteString
|
||||
stripPrefix a b = BS.pack `fmap` L.stripPrefix (BS.unpack a) (BS.unpack b)
|
||||
#endif
|
||||
1119
src/HPath/IO.hs
Normal file
1119
src/HPath/IO.hs
Normal file
File diff suppressed because it is too large
Load Diff
8
src/HPath/IO.hs-boot
Normal file
8
src/HPath/IO.hs-boot
Normal file
@@ -0,0 +1,8 @@
|
||||
module HPath.IO where
|
||||
|
||||
|
||||
import HPath
|
||||
|
||||
canonicalizePath :: Path b -> IO (Path Abs)
|
||||
|
||||
toAbs :: Path b -> IO (Path Abs)
|
||||
360
src/HPath/IO/Errors.hs
Normal file
360
src/HPath/IO/Errors.hs
Normal file
@@ -0,0 +1,360 @@
|
||||
-- |
|
||||
-- Module : HPath.IO.Errors
|
||||
-- Copyright : © 2016 Julian Ospald
|
||||
-- License : BSD3
|
||||
--
|
||||
-- Maintainer : Julian Ospald <hasufell@posteo.de>
|
||||
-- Stability : experimental
|
||||
-- Portability : portable
|
||||
--
|
||||
-- Provides error handling.
|
||||
|
||||
{-# LANGUAGE DeriveDataTypeable #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
|
||||
module HPath.IO.Errors
|
||||
(
|
||||
-- * Types
|
||||
HPathIOException(..)
|
||||
, RecursiveFailureHint(..)
|
||||
|
||||
-- * Exception identifiers
|
||||
, isSameFile
|
||||
, isDestinationInSource
|
||||
, isRecursiveFailure
|
||||
, isReadContentsFailed
|
||||
, isCreateDirFailed
|
||||
, isCopyFileFailed
|
||||
, isRecreateSymlinkFailed
|
||||
|
||||
-- * Path based functions
|
||||
, throwFileDoesExist
|
||||
, throwDirDoesExist
|
||||
, throwSameFile
|
||||
, sameFile
|
||||
, throwDestinationInSource
|
||||
, doesFileExist
|
||||
, doesDirectoryExist
|
||||
, isWritable
|
||||
, canOpenDirectory
|
||||
|
||||
-- * Error handling functions
|
||||
, catchErrno
|
||||
, rethrowErrnoAs
|
||||
, handleIOError
|
||||
, bracketeer
|
||||
, reactOnError
|
||||
)
|
||||
where
|
||||
|
||||
|
||||
import Control.Applicative
|
||||
(
|
||||
(<$>)
|
||||
)
|
||||
import Control.Exception
|
||||
import Control.Monad
|
||||
(
|
||||
forM
|
||||
, when
|
||||
)
|
||||
import Control.Monad.IfElse
|
||||
(
|
||||
whenM
|
||||
)
|
||||
import Data.ByteString
|
||||
(
|
||||
ByteString
|
||||
)
|
||||
import Data.ByteString.UTF8
|
||||
(
|
||||
toString
|
||||
)
|
||||
import Data.Typeable
|
||||
(
|
||||
Typeable
|
||||
)
|
||||
import Foreign.C.Error
|
||||
(
|
||||
getErrno
|
||||
, Errno
|
||||
)
|
||||
import GHC.IO.Exception
|
||||
(
|
||||
IOErrorType
|
||||
)
|
||||
import HPath
|
||||
import HPath.Internal
|
||||
(
|
||||
Path(..)
|
||||
)
|
||||
import {-# SOURCE #-} HPath.IO
|
||||
(
|
||||
canonicalizePath
|
||||
, toAbs
|
||||
)
|
||||
import System.IO.Error
|
||||
(
|
||||
alreadyExistsErrorType
|
||||
, catchIOError
|
||||
, ioeGetErrorType
|
||||
, mkIOError
|
||||
)
|
||||
|
||||
import qualified System.Posix.Directory.ByteString as PFD
|
||||
import System.Posix.Files.ByteString
|
||||
(
|
||||
fileAccess
|
||||
, getFileStatus
|
||||
)
|
||||
import qualified System.Posix.Files.ByteString as PF
|
||||
|
||||
|
||||
-- |Additional generic IO exceptions that the posix functions
|
||||
-- do not provide.
|
||||
data HPathIOException = SameFile ByteString ByteString
|
||||
| DestinationInSource ByteString ByteString
|
||||
| RecursiveFailure [(RecursiveFailureHint, IOException)]
|
||||
deriving (Eq, Show, Typeable)
|
||||
|
||||
|
||||
-- |A type for giving failure hints on recursive failure, which allows
|
||||
-- to programmatically make choices without examining
|
||||
-- the weakly typed I/O error attributes (like `ioeGetFileName`).
|
||||
--
|
||||
-- The first argument to the data constructor is always the
|
||||
-- source and the second the destination.
|
||||
data RecursiveFailureHint = ReadContentsFailed ByteString ByteString
|
||||
| CreateDirFailed ByteString ByteString
|
||||
| CopyFileFailed ByteString ByteString
|
||||
| RecreateSymlinkFailed ByteString ByteString
|
||||
deriving (Eq, Show)
|
||||
|
||||
|
||||
instance Exception HPathIOException
|
||||
|
||||
|
||||
toConstr :: HPathIOException -> String
|
||||
toConstr SameFile {} = "SameFile"
|
||||
toConstr DestinationInSource {} = "DestinationInSource"
|
||||
toConstr RecursiveFailure {} = "RecursiveFailure"
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
-----------------------------
|
||||
--[ Exception identifiers ]--
|
||||
-----------------------------
|
||||
|
||||
|
||||
isSameFile, isDestinationInSource, isRecursiveFailure :: HPathIOException -> Bool
|
||||
isSameFile ex = toConstr (ex :: HPathIOException) == toConstr SameFile{}
|
||||
isDestinationInSource ex = toConstr (ex :: HPathIOException) == toConstr DestinationInSource{}
|
||||
isRecursiveFailure ex = toConstr (ex :: HPathIOException) == toConstr RecursiveFailure{}
|
||||
|
||||
|
||||
isReadContentsFailed, isCreateDirFailed, isCopyFileFailed, isRecreateSymlinkFailed ::RecursiveFailureHint -> Bool
|
||||
isReadContentsFailed ReadContentsFailed{} = True
|
||||
isReadContentsFailed _ = False
|
||||
isCreateDirFailed CreateDirFailed{} = True
|
||||
isCreateDirFailed _ = False
|
||||
isCopyFileFailed CopyFileFailed{} = True
|
||||
isCopyFileFailed _ = False
|
||||
isRecreateSymlinkFailed RecreateSymlinkFailed{} = True
|
||||
isRecreateSymlinkFailed _ = False
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
----------------------------
|
||||
--[ Path based functions ]--
|
||||
----------------------------
|
||||
|
||||
|
||||
-- |Throws `AlreadyExists` `IOError` if file exists.
|
||||
throwFileDoesExist :: Path b -> IO ()
|
||||
throwFileDoesExist fp@(MkPath bs) =
|
||||
whenM (doesFileExist fp)
|
||||
(ioError . mkIOError
|
||||
alreadyExistsErrorType
|
||||
"File already exists"
|
||||
Nothing
|
||||
$ (Just (toString $ bs))
|
||||
)
|
||||
|
||||
|
||||
-- |Throws `AlreadyExists` `IOError` if directory exists.
|
||||
throwDirDoesExist :: Path b -> IO ()
|
||||
throwDirDoesExist fp@(MkPath bs) =
|
||||
whenM (doesDirectoryExist fp)
|
||||
(ioError . mkIOError
|
||||
alreadyExistsErrorType
|
||||
"Directory already exists"
|
||||
Nothing
|
||||
$ (Just (toString $ bs))
|
||||
)
|
||||
|
||||
|
||||
-- |Uses `isSameFile` and throws `SameFile` if it returns True.
|
||||
throwSameFile :: Path b1
|
||||
-> Path b2
|
||||
-> IO ()
|
||||
throwSameFile fp1@(MkPath bs1) fp2@(MkPath bs2) =
|
||||
whenM (sameFile fp1 fp2)
|
||||
(throwIO $ SameFile bs1 bs2)
|
||||
|
||||
|
||||
-- |Check if the files are the same by examining device and file id.
|
||||
-- This follows symbolic links.
|
||||
sameFile :: Path b1 -> Path b2 -> IO Bool
|
||||
sameFile (MkPath fp1) (MkPath fp2) =
|
||||
handleIOError (\_ -> return False) $ do
|
||||
fs1 <- getFileStatus fp1
|
||||
fs2 <- getFileStatus fp2
|
||||
|
||||
if ((PF.deviceID fs1, PF.fileID fs1) ==
|
||||
(PF.deviceID fs2, PF.fileID fs2))
|
||||
then return True
|
||||
else return False
|
||||
|
||||
|
||||
-- TODO: make this more robust when destination does not exist
|
||||
-- |Checks whether the destination directory is contained
|
||||
-- within the source directory by comparing the device+file ID of the
|
||||
-- source directory with all device+file IDs of the parent directories
|
||||
-- of the destination.
|
||||
throwDestinationInSource :: Path b1 -- ^ source dir
|
||||
-> Path b2 -- ^ full destination, @dirname dest@
|
||||
-- must exist
|
||||
-> IO ()
|
||||
throwDestinationInSource (MkPath sbs) dest@(MkPath dbs) = do
|
||||
destAbs <- toAbs dest
|
||||
dest' <- (\x -> maybe x (\y -> x </> y) $ basename dest)
|
||||
<$> (canonicalizePath $ dirname destAbs)
|
||||
dids <- forM (getAllParents dest') $ \p -> do
|
||||
fs <- PF.getSymbolicLinkStatus (fromAbs p)
|
||||
return (PF.deviceID fs, PF.fileID fs)
|
||||
sid <- fmap (\x -> (PF.deviceID x, PF.fileID x))
|
||||
$ PF.getFileStatus sbs
|
||||
when (elem sid dids)
|
||||
(throwIO $ DestinationInSource dbs sbs)
|
||||
|
||||
|
||||
-- |Checks if the given file exists and is not a directory.
|
||||
-- Does not follow symlinks.
|
||||
doesFileExist :: Path b -> IO Bool
|
||||
doesFileExist (MkPath bs) =
|
||||
handleIOError (\_ -> return False) $ do
|
||||
fs <- PF.getSymbolicLinkStatus bs
|
||||
return $ not . PF.isDirectory $ fs
|
||||
|
||||
|
||||
-- |Checks if the given file exists and is a directory.
|
||||
-- Does not follow symlinks.
|
||||
doesDirectoryExist :: Path b -> IO Bool
|
||||
doesDirectoryExist (MkPath bs) =
|
||||
handleIOError (\_ -> return False) $ do
|
||||
fs <- PF.getSymbolicLinkStatus bs
|
||||
return $ PF.isDirectory fs
|
||||
|
||||
|
||||
-- |Checks whether a file or folder is writable.
|
||||
isWritable :: Path b -> IO Bool
|
||||
isWritable (MkPath bs) =
|
||||
handleIOError (\_ -> return False) $
|
||||
fileAccess bs False True False
|
||||
|
||||
|
||||
-- |Checks whether the directory at the given path exists and can be
|
||||
-- opened. This invokes `openDirStream` which follows symlinks.
|
||||
canOpenDirectory :: Path b -> IO Bool
|
||||
canOpenDirectory (MkPath bs) =
|
||||
handleIOError (\_ -> return False) $ do
|
||||
bracket (PFD.openDirStream bs)
|
||||
PFD.closeDirStream
|
||||
(\_ -> return ())
|
||||
return True
|
||||
|
||||
|
||||
|
||||
|
||||
--------------------------------
|
||||
--[ Error handling functions ]--
|
||||
--------------------------------
|
||||
|
||||
|
||||
-- |Carries out an action, then checks if there is an IOException and
|
||||
-- a specific errno. If so, then it carries out another action, otherwise
|
||||
-- it rethrows the error.
|
||||
catchErrno :: [Errno] -- ^ errno to catch
|
||||
-> IO a -- ^ action to try, which can raise an IOException
|
||||
-> IO a -- ^ action to carry out in case of an IOException and
|
||||
-- if errno matches
|
||||
-> IO a
|
||||
catchErrno en a1 a2 =
|
||||
catchIOError a1 $ \e -> do
|
||||
errno <- getErrno
|
||||
if errno `elem` en
|
||||
then a2
|
||||
else ioError e
|
||||
|
||||
|
||||
-- |Execute the given action and retrow IO exceptions as a new Exception
|
||||
-- that have the given errno. If errno does not match the exception is rethrown
|
||||
-- as is.
|
||||
rethrowErrnoAs :: Exception e
|
||||
=> [Errno] -- ^ errno to catch
|
||||
-> e -- ^ rethrow as if errno matches
|
||||
-> IO a -- ^ action to try
|
||||
-> IO a
|
||||
rethrowErrnoAs en fmex action = catchErrno en action (throwIO fmex)
|
||||
|
||||
|
||||
|
||||
-- |Like `catchIOError`, with arguments swapped.
|
||||
handleIOError :: (IOError -> IO a) -> IO a -> IO a
|
||||
handleIOError = flip catchIOError
|
||||
|
||||
|
||||
-- |Like `bracket`, but allows to have different clean-up
|
||||
-- actions depending on whether the in-between computation
|
||||
-- has raised an exception or not.
|
||||
bracketeer :: IO a -- ^ computation to run first
|
||||
-> (a -> IO b) -- ^ computation to run last, when
|
||||
-- no exception was raised
|
||||
-> (a -> IO b) -- ^ computation to run last,
|
||||
-- when an exception was raised
|
||||
-> (a -> IO c) -- ^ computation to run in-between
|
||||
-> IO c
|
||||
bracketeer before after afterEx thing =
|
||||
mask $ \restore -> do
|
||||
a <- before
|
||||
r <- restore (thing a) `onException` afterEx a
|
||||
_ <- after a
|
||||
return r
|
||||
|
||||
|
||||
reactOnError :: IO a
|
||||
-> [(IOErrorType, IO a)] -- ^ reaction on IO errors
|
||||
-> [(HPathIOException, IO a)] -- ^ reaction on HPathIOException
|
||||
-> IO a
|
||||
reactOnError a ios fmios =
|
||||
a `catches` [iohandler, fmiohandler]
|
||||
where
|
||||
iohandler = Handler $
|
||||
\(ex :: IOException) ->
|
||||
foldr (\(t, a') y -> if ioeGetErrorType ex == t
|
||||
then a'
|
||||
else y)
|
||||
(throwIO ex)
|
||||
ios
|
||||
fmiohandler = Handler $
|
||||
\(ex :: HPathIOException) ->
|
||||
foldr (\(t, a') y -> if toConstr ex == toConstr t
|
||||
then a'
|
||||
else y)
|
||||
(throwIO ex)
|
||||
fmios
|
||||
|
||||
51
src/HPath/Internal.hs
Normal file
51
src/HPath/Internal.hs
Normal file
@@ -0,0 +1,51 @@
|
||||
{-# LANGUAGE DeriveDataTypeable #-}
|
||||
|
||||
-- | Internal types and functions.
|
||||
|
||||
module HPath.Internal
|
||||
(Path(..))
|
||||
where
|
||||
|
||||
import Control.DeepSeq (NFData (..))
|
||||
import Data.ByteString (ByteString)
|
||||
import Data.Data
|
||||
|
||||
-- | Path of some base and type.
|
||||
--
|
||||
-- Internally is a ByteString. The ByteString can be of two formats only:
|
||||
--
|
||||
-- 1. without trailing path separator: @file.txt@, @foo\/bar.txt@, @\/foo\/bar.txt@
|
||||
-- 2. with trailing path separator: @foo\/@, @\/foo\/bar\/@
|
||||
--
|
||||
-- There are no duplicate
|
||||
-- path separators @\/\/@, no @..@, no @.\/@, no @~\/@, etc.
|
||||
data Path b = MkPath ByteString
|
||||
deriving (Typeable)
|
||||
|
||||
-- | ByteString equality.
|
||||
--
|
||||
-- The following property holds:
|
||||
--
|
||||
-- @show x == show y ≡ x == y@
|
||||
instance Eq (Path b) where
|
||||
(==) (MkPath x) (MkPath y) = x == y
|
||||
|
||||
-- | ByteString ordering.
|
||||
--
|
||||
-- The following property holds:
|
||||
--
|
||||
-- @show x \`compare\` show y ≡ x \`compare\` y@
|
||||
instance Ord (Path b) where
|
||||
compare (MkPath x) (MkPath y) = compare x y
|
||||
|
||||
-- | Same as 'HPath.toFilePath'.
|
||||
--
|
||||
-- The following property holds:
|
||||
--
|
||||
-- @x == y ≡ show x == show y@
|
||||
instance Show (Path b) where
|
||||
show (MkPath x) = show x
|
||||
|
||||
instance NFData (Path b) where
|
||||
rnf (MkPath x) = rnf x
|
||||
|
||||
353
src/Path.hs
353
src/Path.hs
@@ -1,353 +0,0 @@
|
||||
-- |
|
||||
-- Module : Path
|
||||
-- Copyright : © 2015–2016 FP Complete
|
||||
-- License : BSD 3 clause
|
||||
--
|
||||
-- Maintainer : Chris Done <chrisdone@fpcomplete.com>
|
||||
-- Stability : experimental
|
||||
-- Portability : portable
|
||||
--
|
||||
-- Support for well-typed paths.
|
||||
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
{-# LANGUAGE DeriveDataTypeable #-}
|
||||
{-# LANGUAGE EmptyDataDecls #-}
|
||||
|
||||
-- | A normalizing well-typed path type.
|
||||
|
||||
module Path
|
||||
(-- * Types
|
||||
Path
|
||||
,Abs
|
||||
,Rel
|
||||
,File
|
||||
,Dir
|
||||
-- * Parsing
|
||||
,parseAbsDir
|
||||
,parseRelDir
|
||||
,parseAbsFile
|
||||
,parseRelFile
|
||||
,PathParseException
|
||||
-- * Constructors
|
||||
,mkAbsDir
|
||||
,mkRelDir
|
||||
,mkAbsFile
|
||||
,mkRelFile
|
||||
-- * Operations
|
||||
,(</>)
|
||||
,stripDir
|
||||
,isParentOf
|
||||
,parent
|
||||
,filename
|
||||
,dirname
|
||||
-- * Conversion
|
||||
,toFilePath
|
||||
,fromAbsDir
|
||||
,fromRelDir
|
||||
,fromAbsFile
|
||||
,fromRelFile
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Exception (Exception)
|
||||
import Control.Monad.Catch (MonadThrow(..))
|
||||
import Data.Data
|
||||
import Data.List
|
||||
import Data.Maybe
|
||||
import Language.Haskell.TH
|
||||
import Path.Internal
|
||||
import qualified System.FilePath as FilePath
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Types
|
||||
|
||||
-- | An absolute path.
|
||||
data Abs deriving (Typeable)
|
||||
|
||||
-- | A relative path; one without a root.
|
||||
data Rel deriving (Typeable)
|
||||
|
||||
-- | A file path.
|
||||
data File deriving (Typeable)
|
||||
|
||||
-- | A directory path.
|
||||
data Dir deriving (Typeable)
|
||||
|
||||
-- | Exception when parsing a location.
|
||||
data PathParseException
|
||||
= InvalidAbsDir FilePath
|
||||
| InvalidRelDir FilePath
|
||||
| InvalidAbsFile FilePath
|
||||
| InvalidRelFile FilePath
|
||||
| Couldn'tStripPrefixDir FilePath FilePath
|
||||
deriving (Show,Typeable)
|
||||
instance Exception PathParseException
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Parsers
|
||||
|
||||
-- | Get a location for an absolute directory. Produces a normalized
|
||||
-- path which always ends in a path separator.
|
||||
--
|
||||
-- Throws: 'PathParseException'
|
||||
--
|
||||
parseAbsDir :: MonadThrow m
|
||||
=> FilePath -> m (Path Abs Dir)
|
||||
parseAbsDir filepath =
|
||||
if FilePath.isAbsolute filepath &&
|
||||
not (null (normalizeDir filepath)) &&
|
||||
not ("~/" `isPrefixOf` filepath) &&
|
||||
not (hasParentDir filepath) &&
|
||||
FilePath.isValid filepath
|
||||
then return (Path (normalizeDir filepath))
|
||||
else throwM (InvalidAbsDir filepath)
|
||||
|
||||
-- | Get a location for a relative directory. Produces a normalized
|
||||
-- path which always ends in a path separator.
|
||||
--
|
||||
-- Note that @filepath@ may contain any number of @./@ but may not consist solely of @./@. It also may not contain a single @..@ anywhere.
|
||||
--
|
||||
-- Throws: 'PathParseException'
|
||||
--
|
||||
parseRelDir :: MonadThrow m
|
||||
=> FilePath -> m (Path Rel Dir)
|
||||
parseRelDir filepath =
|
||||
if not (FilePath.isAbsolute filepath) &&
|
||||
not (null filepath) &&
|
||||
not ("~/" `isPrefixOf` filepath) &&
|
||||
not (hasParentDir filepath) &&
|
||||
not (null (normalizeDir filepath)) &&
|
||||
not (all (== '.') filepath) &&
|
||||
FilePath.isValid filepath
|
||||
then return (Path (normalizeDir filepath))
|
||||
else throwM (InvalidRelDir filepath)
|
||||
|
||||
-- | Get a location for an absolute file.
|
||||
--
|
||||
-- Throws: 'PathParseException'
|
||||
--
|
||||
parseAbsFile :: MonadThrow m
|
||||
=> FilePath -> m (Path Abs File)
|
||||
parseAbsFile filepath =
|
||||
if FilePath.isAbsolute filepath &&
|
||||
not (FilePath.hasTrailingPathSeparator filepath) &&
|
||||
not ("~/" `isPrefixOf` filepath) &&
|
||||
not (hasParentDir filepath) &&
|
||||
not (null (normalizeFile filepath)) &&
|
||||
not (all (== '.') filepath) &&
|
||||
FilePath.isValid filepath
|
||||
then return (Path (normalizeFile filepath))
|
||||
else throwM (InvalidAbsFile filepath)
|
||||
|
||||
-- | Get a location for a relative file.
|
||||
--
|
||||
-- Note that @filepath@ may contain any number of @./@ but may not contain a single @..@ anywhere.
|
||||
--
|
||||
-- Throws: 'PathParseException'
|
||||
--
|
||||
parseRelFile :: MonadThrow m
|
||||
=> FilePath -> m (Path Rel File)
|
||||
parseRelFile filepath =
|
||||
if not (FilePath.isAbsolute filepath ||
|
||||
FilePath.hasTrailingPathSeparator filepath) &&
|
||||
not (null filepath) &&
|
||||
not ("~/" `isPrefixOf` filepath) &&
|
||||
not (hasParentDir filepath) &&
|
||||
not (null (normalizeFile filepath)) &&
|
||||
not (all (== '.') filepath) &&
|
||||
FilePath.isValid filepath
|
||||
then return (Path (normalizeFile filepath))
|
||||
else throwM (InvalidRelFile filepath)
|
||||
|
||||
-- | Helper function: check if the filepath has any parent directories in it.
|
||||
-- This handles the logic of checking for different path separators on Windows.
|
||||
hasParentDir :: FilePath -> Bool
|
||||
hasParentDir filepath' =
|
||||
("/.." `isSuffixOf` filepath) ||
|
||||
("/../" `isInfixOf` filepath) ||
|
||||
("../" `isPrefixOf` filepath)
|
||||
where
|
||||
filepath =
|
||||
case FilePath.pathSeparator of
|
||||
'/' -> filepath'
|
||||
x -> map (\y -> if x == y then '/' else y) filepath'
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Constructors
|
||||
|
||||
-- | Make a 'Path Abs Dir'.
|
||||
--
|
||||
-- Remember: due to the nature of absolute paths this (e.g. @\/home\/foo@)
|
||||
-- may compile on your platform, but it may not compile on another
|
||||
-- platform (Windows).
|
||||
mkAbsDir :: FilePath -> Q Exp
|
||||
mkAbsDir s =
|
||||
case parseAbsDir s of
|
||||
Left err -> error (show err)
|
||||
Right (Path str) ->
|
||||
[|Path $(return (LitE (StringL str))) :: Path Abs Dir|]
|
||||
|
||||
-- | Make a 'Path Rel Dir'.
|
||||
mkRelDir :: FilePath -> Q Exp
|
||||
mkRelDir s =
|
||||
case parseRelDir s of
|
||||
Left err -> error (show err)
|
||||
Right (Path str) ->
|
||||
[|Path $(return (LitE (StringL str))) :: Path Rel Dir|]
|
||||
|
||||
-- | Make a 'Path Abs File'.
|
||||
--
|
||||
-- Remember: due to the nature of absolute paths this (e.g. @\/home\/foo@)
|
||||
-- may compile on your platform, but it may not compile on another
|
||||
-- platform (Windows).
|
||||
mkAbsFile :: FilePath -> Q Exp
|
||||
mkAbsFile s =
|
||||
case parseAbsFile s of
|
||||
Left err -> error (show err)
|
||||
Right (Path str) ->
|
||||
[|Path $(return (LitE (StringL str))) :: Path Abs File|]
|
||||
|
||||
-- | Make a 'Path Rel File'.
|
||||
mkRelFile :: FilePath -> Q Exp
|
||||
mkRelFile s =
|
||||
case parseRelFile s of
|
||||
Left err -> error (show err)
|
||||
Right (Path str) ->
|
||||
[|Path $(return (LitE (StringL str))) :: Path Rel File|]
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Conversion
|
||||
|
||||
-- | Convert to a 'FilePath' type.
|
||||
--
|
||||
-- All directories have a trailing slash, so if you want no trailing
|
||||
-- slash, you can use 'System.FilePath.dropTrailingPathSeparator' from
|
||||
-- the filepath package.
|
||||
toFilePath :: Path b t -> FilePath
|
||||
toFilePath (Path l) = l
|
||||
|
||||
-- | Convert absolute path to directory to 'FilePath' type.
|
||||
fromAbsDir :: Path Abs Dir -> FilePath
|
||||
fromAbsDir = toFilePath
|
||||
|
||||
-- | Convert relative path to directory to 'FilePath' type.
|
||||
fromRelDir :: Path Rel Dir -> FilePath
|
||||
fromRelDir = toFilePath
|
||||
|
||||
-- | Convert absolute path to file to 'FilePath' type.
|
||||
fromAbsFile :: Path Abs File -> FilePath
|
||||
fromAbsFile = toFilePath
|
||||
|
||||
-- | Convert relative path to file to 'FilePath' type.
|
||||
fromRelFile :: Path Rel File -> FilePath
|
||||
fromRelFile = toFilePath
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Operations
|
||||
|
||||
-- | Append two paths.
|
||||
--
|
||||
-- The following cases are valid and the equalities hold:
|
||||
--
|
||||
-- @$(mkAbsDir x) \<\/> $(mkRelDir y) = $(mkAbsDir (x ++ \"/\" ++ y))@
|
||||
--
|
||||
-- @$(mkAbsDir x) \<\/> $(mkRelFile y) = $(mkAbsFile (x ++ \"/\" ++ y))@
|
||||
--
|
||||
-- @$(mkRelDir x) \<\/> $(mkRelDir y) = $(mkRelDir (x ++ \"/\" ++ y))@
|
||||
--
|
||||
-- @$(mkRelDir x) \<\/> $(mkRelFile y) = $(mkRelFile (x ++ \"/\" ++ y))@
|
||||
--
|
||||
-- The following are proven not possible to express:
|
||||
--
|
||||
-- @$(mkAbsFile …) \<\/> x@
|
||||
--
|
||||
-- @$(mkRelFile …) \<\/> x@
|
||||
--
|
||||
-- @x \<\/> $(mkAbsFile …)@
|
||||
--
|
||||
-- @x \<\/> $(mkAbsDir …)@
|
||||
--
|
||||
(</>) :: Path b Dir -> Path Rel t -> Path b t
|
||||
(</>) (Path a) (Path b) = Path (a ++ b)
|
||||
|
||||
-- | Strip directory from path, making it relative to that directory.
|
||||
-- Throws 'Couldn'tStripPrefixDir' if directory is not a parent of the path.
|
||||
--
|
||||
-- The following properties hold:
|
||||
--
|
||||
-- @stripDir x (x \<\/> y) = y@
|
||||
--
|
||||
-- Cases which are proven not possible:
|
||||
--
|
||||
-- @stripDir (a :: Path Abs …) (b :: Path Rel …)@
|
||||
--
|
||||
-- @stripDir (a :: Path Rel …) (b :: Path Abs …)@
|
||||
--
|
||||
-- In other words the bases must match.
|
||||
--
|
||||
stripDir :: MonadThrow m
|
||||
=> Path b Dir -> Path b t -> m (Path Rel t)
|
||||
stripDir (Path p) (Path l) =
|
||||
case stripPrefix p l of
|
||||
Nothing -> throwM (Couldn'tStripPrefixDir p l)
|
||||
Just "" -> throwM (Couldn'tStripPrefixDir p l)
|
||||
Just ok -> return (Path ok)
|
||||
|
||||
-- | Is p a parent of the given location? Implemented in terms of
|
||||
-- 'stripDir'. The bases must match.
|
||||
isParentOf :: Path b Dir -> Path b t -> Bool
|
||||
isParentOf p l =
|
||||
isJust (stripDir p l)
|
||||
|
||||
-- | Take the absolute parent directory from the absolute path.
|
||||
--
|
||||
-- The following properties hold:
|
||||
--
|
||||
-- @parent (x \<\/> y) == x@
|
||||
--
|
||||
-- On the root, getting the parent is idempotent:
|
||||
--
|
||||
-- @parent (parent \"\/\") = \"\/\"@
|
||||
--
|
||||
parent :: Path Abs t -> Path Abs Dir
|
||||
parent (Path fp) =
|
||||
Path (normalizeDir (FilePath.takeDirectory (FilePath.dropTrailingPathSeparator fp)))
|
||||
|
||||
-- | Extract the file part of a path.
|
||||
--
|
||||
-- The following properties hold:
|
||||
--
|
||||
-- @filename (p \<\/> a) == filename a@
|
||||
--
|
||||
filename :: Path b File -> Path Rel File
|
||||
filename (Path l) =
|
||||
Path (normalizeFile (FilePath.takeFileName l))
|
||||
|
||||
-- | Extract the last directory name of a path.
|
||||
--
|
||||
-- The following properties hold:
|
||||
--
|
||||
-- @dirname (p \<\/> a) == dirname a@
|
||||
--
|
||||
dirname :: Path b Dir -> Path Rel Dir
|
||||
dirname (Path l) =
|
||||
Path (last (FilePath.splitPath l))
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Internal functions
|
||||
|
||||
-- | Internal use for normalizing a directory.
|
||||
normalizeDir :: FilePath -> FilePath
|
||||
normalizeDir =
|
||||
clean . FilePath.addTrailingPathSeparator . FilePath.normalise
|
||||
where clean "./" = ""
|
||||
clean ('/':'/':xs) = clean ('/':xs)
|
||||
clean x = x
|
||||
|
||||
-- | Internal use for normalizing a fileectory.
|
||||
normalizeFile :: FilePath -> FilePath
|
||||
normalizeFile =
|
||||
clean . FilePath.normalise
|
||||
where clean "./" = ""
|
||||
clean ('/':'/':xs) = clean ('/':xs)
|
||||
clean x = x
|
||||
@@ -1,49 +0,0 @@
|
||||
{-# LANGUAGE DeriveDataTypeable #-}
|
||||
|
||||
-- | Internal types and functions.
|
||||
|
||||
module Path.Internal
|
||||
(Path(..))
|
||||
where
|
||||
|
||||
import Control.DeepSeq (NFData (..))
|
||||
import Data.Data
|
||||
|
||||
-- | Path of some base and type.
|
||||
--
|
||||
-- Internally is a string. The string can be of two formats only:
|
||||
--
|
||||
-- 1. File format: @file.txt@, @foo\/bar.txt@, @\/foo\/bar.txt@
|
||||
-- 2. Directory format: @foo\/@, @\/foo\/bar\/@
|
||||
--
|
||||
-- All directories end in a trailing separator. There are no duplicate
|
||||
-- path separators @\/\/@, no @..@, no @.\/@, no @~\/@, etc.
|
||||
newtype Path b t = Path FilePath
|
||||
deriving (Typeable)
|
||||
|
||||
-- | String equality.
|
||||
--
|
||||
-- The following property holds:
|
||||
--
|
||||
-- @show x == show y ≡ x == y@
|
||||
instance Eq (Path b t) where
|
||||
(==) (Path x) (Path y) = x == y
|
||||
|
||||
-- | String ordering.
|
||||
--
|
||||
-- The following property holds:
|
||||
--
|
||||
-- @show x \`compare\` show y ≡ x \`compare\` y@
|
||||
instance Ord (Path b t) where
|
||||
compare (Path x) (Path y) = compare x y
|
||||
|
||||
-- | Same as 'Path.toFilePath'.
|
||||
--
|
||||
-- The following property holds:
|
||||
--
|
||||
-- @x == y ≡ show x == show y@
|
||||
instance Show (Path b t) where
|
||||
show (Path x) = show x
|
||||
|
||||
instance NFData (Path b t) where
|
||||
rnf (Path x) = rnf x
|
||||
55
src/System/Posix/Directory/Foreign.hsc
Normal file
55
src/System/Posix/Directory/Foreign.hsc
Normal file
@@ -0,0 +1,55 @@
|
||||
module System.Posix.Directory.Foreign where
|
||||
|
||||
import Data.Bits
|
||||
import Data.List (foldl')
|
||||
import Foreign.C.Types
|
||||
|
||||
#include <limits.h>
|
||||
#include <stdlib.h>
|
||||
#include <dirent.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/stat.h>
|
||||
#include <fcntl.h>
|
||||
|
||||
newtype DirType = DirType Int deriving (Eq, Show)
|
||||
data Flags = Flags Int | UnsupportedFlag String deriving (Eq, Show)
|
||||
|
||||
unFlags :: Flags -> Int
|
||||
unFlags (Flags i) = i
|
||||
unFlags (UnsupportedFlag name) = error (name ++ " is not supported on this platform")
|
||||
|
||||
-- |Returns @True@ if posix-paths was compiled with support for the provided
|
||||
-- flag. (As of this writing, the only flag for which this check may be
|
||||
-- necessary is 'oCloexec'; all other flags will always yield @True@.)
|
||||
isSupported :: Flags -> Bool
|
||||
isSupported (Flags _) = True
|
||||
isSupported _ = False
|
||||
|
||||
-- |@O_CLOEXEC@ is not supported on every POSIX platform. Use
|
||||
-- @'isSupported' oCloexec@ to determine if support for @O_CLOEXEC@ was
|
||||
-- compiled into your version of posix-paths. (If not, using @oCloexec@ will
|
||||
-- throw an exception.)
|
||||
oCloexec :: Flags
|
||||
#ifdef O_CLOEXEC
|
||||
oCloexec = Flags #{const O_CLOEXEC}
|
||||
#else
|
||||
{-# WARNING oCloexec
|
||||
"This version of posix-paths was compiled without @O_CLOEXEC@ support." #-}
|
||||
oCloexec = UnsupportedFlag "O_CLOEXEC"
|
||||
#endif
|
||||
|
||||
|
||||
|
||||
-- If these enum declarations occur earlier in the file, haddock
|
||||
-- gets royally confused about the above doc comments.
|
||||
-- Probably http://trac.haskell.org/haddock/ticket/138
|
||||
|
||||
#{enum DirType, DirType, DT_BLK, DT_CHR, DT_DIR, DT_FIFO, DT_LNK, DT_REG, DT_SOCK, DT_UNKNOWN}
|
||||
|
||||
#{enum Flags, Flags, O_APPEND, O_ASYNC, O_CREAT, O_DIRECTORY, O_EXCL, O_NOCTTY, O_NOFOLLOW, O_NONBLOCK, O_RDONLY, O_WRONLY, O_RDWR, O_SYNC, O_TRUNC}
|
||||
|
||||
pathMax :: Int
|
||||
pathMax = #{const PATH_MAX}
|
||||
|
||||
unionFlags :: [Flags] -> CInt
|
||||
unionFlags = fromIntegral . foldl' ((. unFlags) . (.|.)) 0
|
||||
264
src/System/Posix/Directory/Traversals.hs
Normal file
264
src/System/Posix/Directory/Traversals.hs
Normal file
@@ -0,0 +1,264 @@
|
||||
-- |
|
||||
-- Module : System.Posix.Directory.Traversals
|
||||
-- Copyright : © 2016 Julian Ospald
|
||||
-- License : BSD3
|
||||
--
|
||||
-- Maintainer : Julian Ospald <hasufell@posteo.de>
|
||||
-- Stability : experimental
|
||||
-- Portability : portable
|
||||
--
|
||||
-- Traversal and read operations on directories.
|
||||
|
||||
|
||||
{-# LANGUAGE CPP #-}
|
||||
{-# LANGUAGE ForeignFunctionInterface #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE PackageImports #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
{-# LANGUAGE ViewPatterns #-}
|
||||
|
||||
{-# OPTIONS_GHC -Wall #-}
|
||||
|
||||
|
||||
module System.Posix.Directory.Traversals (
|
||||
|
||||
getDirectoryContents
|
||||
, getDirectoryContents'
|
||||
|
||||
, allDirectoryContents
|
||||
, allDirectoryContents'
|
||||
, traverseDirectory
|
||||
|
||||
-- lower-level stuff
|
||||
, readDirEnt
|
||||
, packDirStream
|
||||
, unpackDirStream
|
||||
, fdOpendir
|
||||
|
||||
, realpath
|
||||
) where
|
||||
|
||||
|
||||
#if __GLASGOW_HASKELL__ < 710
|
||||
import Control.Applicative ((<$>))
|
||||
#endif
|
||||
import Control.Monad
|
||||
import System.Posix.FilePath ((</>))
|
||||
import System.Posix.Directory.Foreign
|
||||
|
||||
import qualified System.Posix as Posix
|
||||
import System.IO.Error
|
||||
import Control.Exception
|
||||
import qualified Data.ByteString.Char8 as BS
|
||||
import System.Posix.ByteString.FilePath
|
||||
import System.Posix.Directory.ByteString as PosixBS
|
||||
import System.Posix.Files.ByteString
|
||||
|
||||
import System.IO.Unsafe
|
||||
import "unix" System.Posix.IO.ByteString (closeFd)
|
||||
import Unsafe.Coerce (unsafeCoerce)
|
||||
import Foreign.C.Error
|
||||
import Foreign.C.String
|
||||
import Foreign.C.Types
|
||||
import Foreign.Marshal.Alloc (alloca,allocaBytes)
|
||||
import Foreign.Ptr
|
||||
import Foreign.Storable
|
||||
|
||||
|
||||
|
||||
|
||||
----------------------------------------------------------
|
||||
|
||||
-- | Get all files from a directory and its subdirectories.
|
||||
--
|
||||
-- Upon entering a directory, 'allDirectoryContents' will get all entries
|
||||
-- strictly. However the returned list is lazy in that directories will only
|
||||
-- be accessed on demand.
|
||||
--
|
||||
-- Follows symbolic links for the input dir.
|
||||
allDirectoryContents :: RawFilePath -> IO [RawFilePath]
|
||||
allDirectoryContents topdir = do
|
||||
namesAndTypes <- getDirectoryContents topdir
|
||||
let properNames = filter ((`notElem` [".", ".."]) . snd) namesAndTypes
|
||||
paths <- forM properNames $ \(typ,name) -> unsafeInterleaveIO $ do
|
||||
let path = topdir </> name
|
||||
case () of
|
||||
() | typ == dtDir -> allDirectoryContents path
|
||||
| typ == dtUnknown -> do
|
||||
isDir <- isDirectory <$> getFileStatus path
|
||||
if isDir
|
||||
then allDirectoryContents path
|
||||
else return [path]
|
||||
| otherwise -> return [path]
|
||||
return (topdir : concat paths)
|
||||
|
||||
-- | Get all files from a directory and its subdirectories strictly.
|
||||
--
|
||||
-- Follows symbolic links for the input dir.
|
||||
allDirectoryContents' :: RawFilePath -> IO [RawFilePath]
|
||||
allDirectoryContents' = fmap reverse . traverseDirectory (\acc fp -> return (fp:acc)) []
|
||||
-- this uses traverseDirectory because it's more efficient than forcing the
|
||||
-- lazy version.
|
||||
|
||||
-- | Recursively apply the 'action' to the parent directory and all
|
||||
-- files/subdirectories.
|
||||
--
|
||||
-- This function allows for memory-efficient traversals.
|
||||
--
|
||||
-- Follows symbolic links for the input dir.
|
||||
traverseDirectory :: (s -> RawFilePath -> IO s) -> s -> RawFilePath -> IO s
|
||||
traverseDirectory act s0 topdir = toploop
|
||||
where
|
||||
toploop = do
|
||||
isDir <- isDirectory <$> getFileStatus topdir
|
||||
s' <- act s0 topdir
|
||||
if isDir then actOnDirContents topdir s' loop
|
||||
else return s'
|
||||
loop typ path acc = do
|
||||
isDir <- case () of
|
||||
() | typ == dtDir -> return True
|
||||
| typ == dtUnknown -> isDirectory <$> getFileStatus path
|
||||
| otherwise -> return False
|
||||
if isDir
|
||||
then act acc path >>= \acc' -> actOnDirContents path acc' loop
|
||||
else act acc path
|
||||
|
||||
actOnDirContents :: RawFilePath
|
||||
-> b
|
||||
-> (DirType -> RawFilePath -> b -> IO b)
|
||||
-> IO b
|
||||
actOnDirContents pathRelToTop b f =
|
||||
modifyIOError ((`ioeSetFileName` (BS.unpack pathRelToTop)) .
|
||||
(`ioeSetLocation` "findBSTypRel")) $
|
||||
bracket
|
||||
(openDirStream pathRelToTop)
|
||||
Posix.closeDirStream
|
||||
(\dirp -> loop dirp b)
|
||||
where
|
||||
loop dirp b' = do
|
||||
(typ,e) <- readDirEnt dirp
|
||||
if (e == "")
|
||||
then return b'
|
||||
else
|
||||
if (e == "." || e == "..")
|
||||
then loop dirp b'
|
||||
else f typ (pathRelToTop </> e) b' >>= loop dirp
|
||||
|
||||
|
||||
----------------------------------------------------------
|
||||
-- dodgy stuff
|
||||
|
||||
type CDir = ()
|
||||
type CDirent = ()
|
||||
|
||||
-- Posix doesn't export DirStream, so to re-use that type we need to use
|
||||
-- unsafeCoerce. It's just a newtype, so this is a legitimate usage.
|
||||
-- ugly trick.
|
||||
unpackDirStream :: DirStream -> Ptr CDir
|
||||
unpackDirStream = unsafeCoerce
|
||||
|
||||
packDirStream :: Ptr CDir -> DirStream
|
||||
packDirStream = unsafeCoerce
|
||||
|
||||
-- the __hscore_* functions are defined in the unix package. We can import them and let
|
||||
-- the linker figure it out.
|
||||
foreign import ccall unsafe "__hscore_readdir"
|
||||
c_readdir :: Ptr CDir -> Ptr (Ptr CDirent) -> IO CInt
|
||||
|
||||
foreign import ccall unsafe "__hscore_free_dirent"
|
||||
c_freeDirEnt :: Ptr CDirent -> IO ()
|
||||
|
||||
foreign import ccall unsafe "__hscore_d_name"
|
||||
c_name :: Ptr CDirent -> IO CString
|
||||
|
||||
foreign import ccall unsafe "__posixdir_d_type"
|
||||
c_type :: Ptr CDirent -> IO DirType
|
||||
|
||||
foreign import ccall "realpath"
|
||||
c_realpath :: CString -> CString -> IO CString
|
||||
|
||||
foreign import ccall unsafe "fdopendir"
|
||||
c_fdopendir :: Posix.Fd -> IO (Ptr ())
|
||||
|
||||
----------------------------------------------------------
|
||||
-- less dodgy but still lower-level
|
||||
|
||||
|
||||
readDirEnt :: DirStream -> IO (DirType, RawFilePath)
|
||||
readDirEnt (unpackDirStream -> dirp) =
|
||||
alloca $ \ptr_dEnt -> loop ptr_dEnt
|
||||
where
|
||||
loop ptr_dEnt = do
|
||||
resetErrno
|
||||
r <- c_readdir dirp ptr_dEnt
|
||||
if (r == 0)
|
||||
then do
|
||||
dEnt <- peek ptr_dEnt
|
||||
if (dEnt == nullPtr)
|
||||
then return (dtUnknown,BS.empty)
|
||||
else do
|
||||
dName <- c_name dEnt >>= peekFilePath
|
||||
dType <- c_type dEnt
|
||||
c_freeDirEnt dEnt
|
||||
return (dType, dName)
|
||||
else do
|
||||
errno <- getErrno
|
||||
if (errno == eINTR)
|
||||
then loop ptr_dEnt
|
||||
else do
|
||||
let (Errno eo) = errno
|
||||
if (eo == 0)
|
||||
then return (dtUnknown,BS.empty)
|
||||
else throwErrno "readDirEnt"
|
||||
|
||||
|
||||
-- |Gets all directory contents (not recursively).
|
||||
getDirectoryContents :: RawFilePath -> IO [(DirType, RawFilePath)]
|
||||
getDirectoryContents path =
|
||||
modifyIOError ((`ioeSetFileName` (BS.unpack path)) .
|
||||
(`ioeSetLocation` "System.Posix.Directory.Traversals.getDirectoryContents")) $
|
||||
bracket
|
||||
(PosixBS.openDirStream path)
|
||||
PosixBS.closeDirStream
|
||||
_dirloop
|
||||
|
||||
|
||||
-- |Binding to @fdopendir(3)@.
|
||||
fdOpendir :: Posix.Fd -> IO DirStream
|
||||
fdOpendir fd =
|
||||
packDirStream <$> throwErrnoIfNull "fdOpendir" (c_fdopendir fd)
|
||||
|
||||
|
||||
-- |Like `getDirectoryContents` except for a file descriptor.
|
||||
--
|
||||
-- To avoid complicated error checks, the file descriptor is
|
||||
-- __always__ closed, even if `fdOpendir` fails. Usually, this
|
||||
-- only happens on successful `fdOpendir` and after the directory
|
||||
-- stream is closed. Also see the manpage of @fdopendir(3)@ for
|
||||
-- more details.
|
||||
getDirectoryContents' :: Posix.Fd -> IO [(DirType, RawFilePath)]
|
||||
getDirectoryContents' fd = do
|
||||
dirstream <- fdOpendir fd `catchIOError` \e -> do
|
||||
closeFd fd
|
||||
ioError e
|
||||
-- closeDirStream closes the filedescriptor
|
||||
finally (_dirloop dirstream) (PosixBS.closeDirStream dirstream)
|
||||
|
||||
|
||||
_dirloop :: DirStream -> IO [(DirType, RawFilePath)]
|
||||
{-# INLINE _dirloop #-}
|
||||
_dirloop dirp = do
|
||||
t@(_typ,e) <- readDirEnt dirp
|
||||
if BS.null e then return [] else do
|
||||
es <- _dirloop dirp
|
||||
return (t:es)
|
||||
|
||||
|
||||
-- | return the canonicalized absolute pathname
|
||||
--
|
||||
-- like canonicalizePath, but uses @realpath(3)@
|
||||
realpath :: RawFilePath -> IO RawFilePath
|
||||
realpath inp =
|
||||
allocaBytes pathMax $ \tmp -> do
|
||||
void $ BS.useAsCString inp $ \cstr -> throwErrnoIfNull "realpath" $ c_realpath cstr tmp
|
||||
BS.packCString tmp
|
||||
75
src/System/Posix/FD.hs
Normal file
75
src/System/Posix/FD.hs
Normal file
@@ -0,0 +1,75 @@
|
||||
-- |
|
||||
-- Module : System.Posix.FD
|
||||
-- Copyright : © 2016 Julian Ospald
|
||||
-- License : BSD3
|
||||
--
|
||||
-- Maintainer : Julian Ospald <hasufell@posteo.de>
|
||||
-- Stability : experimental
|
||||
-- Portability : portable
|
||||
--
|
||||
-- Provides an alternative for `System.Posix.IO.ByteString.openFd`
|
||||
-- which gives us more control on what status flags to pass to the
|
||||
-- low-level @open(2)@ call, in contrast to the unix package.
|
||||
|
||||
|
||||
{-# LANGUAGE ForeignFunctionInterface #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
|
||||
{-# OPTIONS_GHC -Wall #-}
|
||||
|
||||
|
||||
module System.Posix.FD (
|
||||
openFd
|
||||
) where
|
||||
|
||||
|
||||
import Foreign.C.String
|
||||
import Foreign.C.Types
|
||||
import System.Posix.Directory.Foreign
|
||||
import qualified System.Posix as Posix
|
||||
import System.Posix.ByteString.FilePath
|
||||
|
||||
|
||||
foreign import ccall unsafe "open"
|
||||
c_open :: CString -> CInt -> Posix.CMode -> IO CInt
|
||||
|
||||
|
||||
open_ :: CString
|
||||
-> Posix.OpenMode
|
||||
-> [Flags]
|
||||
-> Maybe Posix.FileMode
|
||||
-> IO Posix.Fd
|
||||
open_ str how optional_flags maybe_mode = do
|
||||
fd <- c_open str all_flags mode_w
|
||||
return (Posix.Fd fd)
|
||||
where
|
||||
all_flags = unionFlags $ optional_flags ++ [open_mode] ++ creat
|
||||
|
||||
|
||||
(creat, mode_w) = case maybe_mode of
|
||||
Nothing -> ([],0)
|
||||
Just x -> ([oCreat], x)
|
||||
|
||||
open_mode = case how of
|
||||
Posix.ReadOnly -> oRdonly
|
||||
Posix.WriteOnly -> oWronly
|
||||
Posix.ReadWrite -> oRdwr
|
||||
|
||||
|
||||
-- |Open and optionally create this file. See 'System.Posix.Files'
|
||||
-- for information on how to use the 'FileMode' type.
|
||||
--
|
||||
-- Note that passing @Just x@ as the 4th argument triggers the
|
||||
-- `oCreat` status flag, which must be set when you pass in `oExcl`
|
||||
-- to the status flags. Also see the manpage for @open(2)@.
|
||||
openFd :: RawFilePath
|
||||
-> Posix.OpenMode
|
||||
-> [Flags] -- ^ status flags of @open(2)@
|
||||
-> Maybe Posix.FileMode -- ^ @Just x@ => creates the file with the given modes, Nothing => the file must exist.
|
||||
-> IO Posix.Fd
|
||||
openFd name how optional_flags maybe_mode =
|
||||
withFilePath name $ \str ->
|
||||
throwErrnoPathIfMinus1Retry "openFd" name $
|
||||
open_ str how optional_flags maybe_mode
|
||||
|
||||
823
src/System/Posix/FilePath.hs
Normal file
823
src/System/Posix/FilePath.hs
Normal file
@@ -0,0 +1,823 @@
|
||||
-- |
|
||||
-- Module : System.Posix.FilePath
|
||||
-- Copyright : © 2016 Julian Ospald
|
||||
-- License : BSD3
|
||||
--
|
||||
-- Maintainer : Julian Ospald <hasufell@posteo.de>
|
||||
-- Stability : experimental
|
||||
-- Portability : portable
|
||||
--
|
||||
-- The equivalent of "System.FilePath" on raw (byte string) file paths.
|
||||
--
|
||||
-- Not all functions of "System.FilePath" are implemented yet. Feel free to contribute!
|
||||
|
||||
|
||||
{-# LANGUAGE CPP #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
|
||||
{-# OPTIONS_GHC -Wall #-}
|
||||
|
||||
|
||||
module System.Posix.FilePath (
|
||||
|
||||
-- * Separator predicates
|
||||
pathSeparator
|
||||
, isPathSeparator
|
||||
, searchPathSeparator
|
||||
, isSearchPathSeparator
|
||||
, extSeparator
|
||||
, isExtSeparator
|
||||
|
||||
-- * $PATH methods
|
||||
, splitSearchPath
|
||||
, getSearchPath
|
||||
|
||||
-- * Extension functions
|
||||
, splitExtension
|
||||
, takeExtension
|
||||
, replaceExtension
|
||||
, dropExtension
|
||||
, addExtension
|
||||
, hasExtension
|
||||
, (<.>)
|
||||
, splitExtensions
|
||||
, dropExtensions
|
||||
, takeExtensions
|
||||
, stripExtension
|
||||
|
||||
-- * Filename\/directory functions
|
||||
, splitFileName
|
||||
, takeFileName
|
||||
, replaceFileName
|
||||
, dropFileName
|
||||
, takeBaseName
|
||||
, replaceBaseName
|
||||
, takeDirectory
|
||||
, replaceDirectory
|
||||
, combine
|
||||
, (</>)
|
||||
, splitPath
|
||||
, joinPath
|
||||
, splitDirectories
|
||||
|
||||
-- * Trailing slash functions
|
||||
, hasTrailingPathSeparator
|
||||
, addTrailingPathSeparator
|
||||
, dropTrailingPathSeparator
|
||||
|
||||
-- * File name manipulations
|
||||
, normalise
|
||||
, makeRelative
|
||||
, equalFilePath
|
||||
, isRelative
|
||||
, isAbsolute
|
||||
, isValid
|
||||
, makeValid
|
||||
, isFileName
|
||||
, hasParentDir
|
||||
, hiddenFile
|
||||
|
||||
, module System.Posix.ByteString.FilePath
|
||||
) where
|
||||
|
||||
import Data.ByteString (ByteString)
|
||||
import qualified Data.ByteString as BS
|
||||
import Data.String (fromString)
|
||||
import System.Posix.ByteString.FilePath
|
||||
import qualified System.Posix.Env.ByteString as PE
|
||||
|
||||
import Data.Maybe (isJust)
|
||||
import Data.Word8
|
||||
#if !MIN_VERSION_bytestring(0,10,8)
|
||||
import qualified Data.List as L
|
||||
#endif
|
||||
import Control.Arrow (second)
|
||||
|
||||
-- $setup
|
||||
-- >>> import Data.Char
|
||||
-- >>> import Data.Maybe
|
||||
-- >>> import Test.QuickCheck
|
||||
-- >>> import Control.Applicative
|
||||
-- >>> import qualified Data.ByteString as BS
|
||||
-- >>> instance Arbitrary ByteString where arbitrary = BS.pack <$> arbitrary
|
||||
-- >>> instance CoArbitrary ByteString where coarbitrary = coarbitrary . BS.unpack
|
||||
--
|
||||
-- >>> let _chr :: Word8 -> Char; _chr = chr . fromIntegral
|
||||
|
||||
|
||||
|
||||
------------------------
|
||||
-- Separator predicates
|
||||
|
||||
|
||||
-- | Path separator character
|
||||
pathSeparator :: Word8
|
||||
pathSeparator = _slash
|
||||
|
||||
|
||||
-- | Check if a character is the path separator
|
||||
--
|
||||
-- prop> \n -> (_chr n == '/') == isPathSeparator n
|
||||
isPathSeparator :: Word8 -> Bool
|
||||
isPathSeparator = (== pathSeparator)
|
||||
|
||||
|
||||
-- | Search path separator
|
||||
searchPathSeparator :: Word8
|
||||
searchPathSeparator = _colon
|
||||
|
||||
|
||||
-- | Check if a character is the search path separator
|
||||
--
|
||||
-- prop> \n -> (_chr n == ':') == isSearchPathSeparator n
|
||||
isSearchPathSeparator :: Word8 -> Bool
|
||||
isSearchPathSeparator = (== searchPathSeparator)
|
||||
|
||||
|
||||
-- | File extension separator
|
||||
extSeparator :: Word8
|
||||
extSeparator = _period
|
||||
|
||||
|
||||
-- | Check if a character is the file extension separator
|
||||
--
|
||||
-- prop> \n -> (_chr n == '.') == isExtSeparator n
|
||||
isExtSeparator :: Word8 -> Bool
|
||||
isExtSeparator = (== extSeparator)
|
||||
|
||||
|
||||
|
||||
------------------------
|
||||
-- $PATH methods
|
||||
|
||||
|
||||
-- | Take a ByteString, split it on the 'searchPathSeparator'.
|
||||
-- Blank items are converted to @.@.
|
||||
--
|
||||
-- Follows the recommendations in
|
||||
-- <http://www.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap08.html>
|
||||
--
|
||||
-- >>> splitSearchPath "File1:File2:File3"
|
||||
-- ["File1","File2","File3"]
|
||||
-- >>> splitSearchPath "File1::File2:File3"
|
||||
-- ["File1",".","File2","File3"]
|
||||
-- >>> splitSearchPath ""
|
||||
-- ["."]
|
||||
splitSearchPath :: ByteString -> [RawFilePath]
|
||||
splitSearchPath = f
|
||||
where
|
||||
f bs = let (pre, post) = BS.break isSearchPathSeparator bs
|
||||
in if BS.null post
|
||||
then g pre
|
||||
else g pre ++ f (BS.tail post)
|
||||
g x
|
||||
| BS.null x = [BS.singleton _period]
|
||||
| otherwise = [x]
|
||||
|
||||
|
||||
-- | Get a list of 'RawFilePath's in the $PATH variable.
|
||||
getSearchPath :: IO [RawFilePath]
|
||||
getSearchPath = fmap (maybe [] splitSearchPath) (PE.getEnv $ fromString "PATH")
|
||||
|
||||
|
||||
|
||||
------------------------
|
||||
-- Extension functions
|
||||
|
||||
-- | Split a 'RawFilePath' into a path+filename and extension
|
||||
--
|
||||
-- >>> splitExtension "file.exe"
|
||||
-- ("file",".exe")
|
||||
-- >>> splitExtension "file"
|
||||
-- ("file","")
|
||||
-- >>> splitExtension "/path/file.tar.gz"
|
||||
-- ("/path/file.tar",".gz")
|
||||
--
|
||||
-- prop> \path -> uncurry (BS.append) (splitExtension path) == path
|
||||
splitExtension :: RawFilePath -> (RawFilePath, ByteString)
|
||||
splitExtension x = if BS.null basename
|
||||
then (x,BS.empty)
|
||||
else (BS.append path (BS.init basename),BS.cons extSeparator fileExt)
|
||||
where
|
||||
(path,file) = splitFileNameRaw x
|
||||
(basename,fileExt) = BS.breakEnd isExtSeparator file
|
||||
|
||||
|
||||
-- | Get the final extension from a 'RawFilePath'
|
||||
--
|
||||
-- >>> takeExtension "file.exe"
|
||||
-- ".exe"
|
||||
-- >>> takeExtension "file"
|
||||
-- ""
|
||||
-- >>> takeExtension "/path/file.tar.gz"
|
||||
-- ".gz"
|
||||
takeExtension :: RawFilePath -> ByteString
|
||||
takeExtension = snd . splitExtension
|
||||
|
||||
|
||||
-- | Change a file's extension
|
||||
--
|
||||
-- prop> \path -> let ext = takeExtension path in replaceExtension path ext == path
|
||||
replaceExtension :: RawFilePath -> ByteString -> RawFilePath
|
||||
replaceExtension path ext = dropExtension path <.> ext
|
||||
|
||||
|
||||
-- | Drop the final extension from a 'RawFilePath'
|
||||
--
|
||||
-- >>> dropExtension "file.exe"
|
||||
-- "file"
|
||||
-- >>> dropExtension "file"
|
||||
-- "file"
|
||||
-- >>> dropExtension "/path/file.tar.gz"
|
||||
-- "/path/file.tar"
|
||||
dropExtension :: RawFilePath -> RawFilePath
|
||||
dropExtension = fst . splitExtension
|
||||
|
||||
|
||||
-- | Add an extension to a 'RawFilePath'
|
||||
--
|
||||
-- >>> addExtension "file" ".exe"
|
||||
-- "file.exe"
|
||||
-- >>> addExtension "file.tar" ".gz"
|
||||
-- "file.tar.gz"
|
||||
-- >>> addExtension "/path/" ".ext"
|
||||
-- "/path/.ext"
|
||||
addExtension :: RawFilePath -> ByteString -> RawFilePath
|
||||
addExtension file ext
|
||||
| BS.null ext = file
|
||||
| isExtSeparator (BS.head ext) = BS.append file ext
|
||||
| otherwise = BS.intercalate (BS.singleton extSeparator) [file, ext]
|
||||
|
||||
|
||||
-- | Check if a 'RawFilePath' has an extension
|
||||
--
|
||||
-- >>> hasExtension "file"
|
||||
-- False
|
||||
-- >>> hasExtension "file.tar"
|
||||
-- True
|
||||
-- >>> hasExtension "/path.part1/"
|
||||
-- False
|
||||
hasExtension :: RawFilePath -> Bool
|
||||
hasExtension = isJust . BS.elemIndex extSeparator . takeFileName
|
||||
|
||||
|
||||
-- | Operator version of 'addExtension'
|
||||
(<.>) :: RawFilePath -> ByteString -> RawFilePath
|
||||
(<.>) = addExtension
|
||||
|
||||
|
||||
-- | Split a 'RawFilePath' on the first extension.
|
||||
--
|
||||
-- >>> splitExtensions "/path/file.tar.gz"
|
||||
-- ("/path/file",".tar.gz")
|
||||
--
|
||||
-- prop> \path -> uncurry addExtension (splitExtensions path) == path
|
||||
splitExtensions :: RawFilePath -> (RawFilePath, ByteString)
|
||||
splitExtensions x = if BS.null basename
|
||||
then (path,fileExt)
|
||||
else (BS.append path basename,fileExt)
|
||||
where
|
||||
(path,file) = splitFileNameRaw x
|
||||
(basename,fileExt) = BS.break isExtSeparator file
|
||||
|
||||
|
||||
-- | Remove all extensions from a 'RawFilePath'
|
||||
--
|
||||
-- >>> dropExtensions "/path/file.tar.gz"
|
||||
-- "/path/file"
|
||||
dropExtensions :: RawFilePath -> RawFilePath
|
||||
dropExtensions = fst . splitExtensions
|
||||
|
||||
|
||||
-- | Take all extensions from a 'RawFilePath'
|
||||
--
|
||||
-- >>> takeExtensions "/path/file.tar.gz"
|
||||
-- ".tar.gz"
|
||||
takeExtensions :: RawFilePath -> ByteString
|
||||
takeExtensions = snd . splitExtensions
|
||||
|
||||
|
||||
-- | Drop the given extension from a FilePath, and the @\".\"@ preceding it.
|
||||
-- Returns 'Nothing' if the FilePath does not have the given extension, or
|
||||
-- 'Just' and the part before the extension if it does.
|
||||
--
|
||||
-- This function can be more predictable than 'dropExtensions',
|
||||
-- especially if the filename might itself contain @.@ characters.
|
||||
--
|
||||
-- >>> stripExtension "hs.o" "foo.x.hs.o"
|
||||
-- Just "foo.x"
|
||||
-- >>> stripExtension "hi.o" "foo.x.hs.o"
|
||||
-- Nothing
|
||||
-- >>> stripExtension ".c.d" "a.b.c.d"
|
||||
-- Just "a.b"
|
||||
-- >>> stripExtension ".c.d" "a.b..c.d"
|
||||
-- Just "a.b."
|
||||
-- >>> stripExtension "baz" "foo.bar"
|
||||
-- Nothing
|
||||
-- >>> stripExtension "bar" "foobar"
|
||||
-- Nothing
|
||||
--
|
||||
-- prop> \path -> stripExtension "" path == Just path
|
||||
-- prop> \path -> dropExtension path == fromJust (stripExtension (takeExtension path) path)
|
||||
-- prop> \path -> dropExtensions path == fromJust (stripExtension (takeExtensions path) path)
|
||||
stripExtension :: ByteString -> RawFilePath -> Maybe RawFilePath
|
||||
stripExtension bs path
|
||||
| BS.null bs = Just path
|
||||
| otherwise = stripSuffix' dotExt path
|
||||
where
|
||||
dotExt = if isExtSeparator $ BS.head bs
|
||||
then bs
|
||||
else extSeparator `BS.cons` bs
|
||||
#if MIN_VERSION_bytestring(0,10,8)
|
||||
stripSuffix' = BS.stripSuffix
|
||||
#else
|
||||
stripSuffix' xs ys = fmap (BS.pack . reverse) $ L.stripPrefix (reverse $ BS.unpack xs) (reverse $ BS.unpack ys)
|
||||
#endif
|
||||
|
||||
|
||||
------------------------
|
||||
-- Filename/directory functions
|
||||
|
||||
|
||||
-- | Split a 'RawFilePath' into (path,file). 'combine' is the inverse
|
||||
--
|
||||
-- >>> splitFileName "path/file.txt"
|
||||
-- ("path/","file.txt")
|
||||
-- >>> splitFileName "path/"
|
||||
-- ("path/","")
|
||||
-- >>> splitFileName "file.txt"
|
||||
-- ("./","file.txt")
|
||||
--
|
||||
-- prop> \path -> uncurry combine (splitFileName path) == path || fst (splitFileName path) == "./"
|
||||
splitFileName :: RawFilePath -> (RawFilePath, RawFilePath)
|
||||
splitFileName x = if BS.null path
|
||||
then (dotSlash, file)
|
||||
else (path,file)
|
||||
where
|
||||
(path,file) = splitFileNameRaw x
|
||||
dotSlash = _period `BS.cons` (BS.singleton pathSeparator)
|
||||
|
||||
|
||||
-- | Get the file name
|
||||
--
|
||||
-- >>> takeFileName "path/file.txt"
|
||||
-- "file.txt"
|
||||
-- >>> takeFileName "path/"
|
||||
-- ""
|
||||
takeFileName :: RawFilePath -> RawFilePath
|
||||
takeFileName = snd . splitFileName
|
||||
|
||||
|
||||
-- | Change the file name
|
||||
--
|
||||
-- prop> \path -> replaceFileName path (takeFileName path) == path
|
||||
replaceFileName :: RawFilePath -> ByteString -> RawFilePath
|
||||
replaceFileName x y = fst (splitFileNameRaw x) </> y
|
||||
|
||||
|
||||
-- | Drop the file name
|
||||
--
|
||||
-- >>> dropFileName "path/file.txt"
|
||||
-- "path/"
|
||||
-- >>> dropFileName "file.txt"
|
||||
-- "./"
|
||||
dropFileName :: RawFilePath -> RawFilePath
|
||||
dropFileName = fst . splitFileName
|
||||
|
||||
|
||||
-- | Get the file name, without a trailing extension
|
||||
--
|
||||
-- >>> takeBaseName "path/file.tar.gz"
|
||||
-- "file.tar"
|
||||
-- >>> takeBaseName ""
|
||||
-- ""
|
||||
takeBaseName :: RawFilePath -> ByteString
|
||||
takeBaseName = dropExtension . takeFileName
|
||||
|
||||
|
||||
-- | Change the base name
|
||||
--
|
||||
-- >>> replaceBaseName "path/file.tar.gz" "bob"
|
||||
-- "path/bob.gz"
|
||||
--
|
||||
-- prop> \path -> replaceBaseName path (takeBaseName path) == path
|
||||
replaceBaseName :: RawFilePath -> ByteString -> RawFilePath
|
||||
replaceBaseName path name = combineRaw dir (name <.> ext)
|
||||
where
|
||||
(dir,file) = splitFileNameRaw path
|
||||
ext = takeExtension file
|
||||
|
||||
|
||||
-- | Get the directory, moving up one level if it's already a directory
|
||||
--
|
||||
-- >>> takeDirectory "path/file.txt"
|
||||
-- "path"
|
||||
-- >>> takeDirectory "file"
|
||||
-- "."
|
||||
-- >>> takeDirectory "/path/to/"
|
||||
-- "/path/to"
|
||||
-- >>> takeDirectory "/path/to"
|
||||
-- "/path"
|
||||
takeDirectory :: RawFilePath -> RawFilePath
|
||||
takeDirectory x = case () of
|
||||
() | x == BS.singleton pathSeparator -> x
|
||||
| BS.null res && not (BS.null file) -> file
|
||||
| otherwise -> res
|
||||
where
|
||||
res = fst $ BS.spanEnd isPathSeparator file
|
||||
file = dropFileName x
|
||||
|
||||
|
||||
-- | Change the directory component of a 'RawFilePath'
|
||||
--
|
||||
-- prop> \path -> replaceDirectory path (takeDirectory path) `equalFilePath` path || takeDirectory path == "."
|
||||
replaceDirectory :: RawFilePath -> ByteString -> RawFilePath
|
||||
replaceDirectory file dir = combineRaw dir (takeFileName file)
|
||||
|
||||
|
||||
-- | Join two paths together
|
||||
--
|
||||
-- >>> combine "/" "file"
|
||||
-- "/file"
|
||||
-- >>> combine "/path/to" "file"
|
||||
-- "/path/to/file"
|
||||
-- >>> combine "file" "/absolute/path"
|
||||
-- "/absolute/path"
|
||||
combine :: RawFilePath -> RawFilePath -> RawFilePath
|
||||
combine a b | not (BS.null b) && isPathSeparator (BS.head b) = b
|
||||
| otherwise = combineRaw a b
|
||||
|
||||
|
||||
-- | Operator version of combine
|
||||
(</>) :: RawFilePath -> RawFilePath -> RawFilePath
|
||||
(</>) = combine
|
||||
|
||||
-- | Split a path into a list of components:
|
||||
--
|
||||
-- >>> splitPath "/path/to/file.txt"
|
||||
-- ["/","path/","to/","file.txt"]
|
||||
--
|
||||
-- prop> \path -> BS.concat (splitPath path) == path
|
||||
splitPath :: RawFilePath -> [RawFilePath]
|
||||
splitPath = splitter
|
||||
where
|
||||
splitter x
|
||||
| BS.null x = []
|
||||
| otherwise = case BS.elemIndex pathSeparator x of
|
||||
Nothing -> [x]
|
||||
Just ix -> case BS.findIndex (not . isPathSeparator) $ BS.drop (ix+1) x of
|
||||
Nothing -> [x]
|
||||
Just runlen -> uncurry (:) . second splitter $ BS.splitAt (ix+1+runlen) x
|
||||
|
||||
|
||||
-- | Join a split path back together
|
||||
--
|
||||
-- prop> \path -> joinPath (splitPath path) == path
|
||||
--
|
||||
-- >>> joinPath ["path","to","file.txt"]
|
||||
-- "path/to/file.txt"
|
||||
joinPath :: [RawFilePath] -> RawFilePath
|
||||
joinPath = foldr (</>) BS.empty
|
||||
|
||||
|
||||
-- | Like 'splitPath', but without trailing slashes
|
||||
--
|
||||
-- >>> splitDirectories "/path/to/file.txt"
|
||||
-- ["/","path","to","file.txt"]
|
||||
-- >>> splitDirectories ""
|
||||
-- []
|
||||
splitDirectories :: RawFilePath -> [RawFilePath]
|
||||
splitDirectories x
|
||||
| BS.null x = []
|
||||
| isPathSeparator (BS.head x) = let (root,rest) = BS.splitAt 1 x
|
||||
in root : splitter rest
|
||||
| otherwise = splitter x
|
||||
where
|
||||
splitter = filter (not . BS.null) . BS.split pathSeparator
|
||||
|
||||
|
||||
|
||||
------------------------
|
||||
-- Trailing slash functions
|
||||
|
||||
-- | Check if the last character of a 'RawFilePath' is '/'.
|
||||
--
|
||||
-- >>> hasTrailingPathSeparator "/path/"
|
||||
-- True
|
||||
-- >>> hasTrailingPathSeparator "/"
|
||||
-- True
|
||||
-- >>> hasTrailingPathSeparator "/path"
|
||||
-- False
|
||||
hasTrailingPathSeparator :: RawFilePath -> Bool
|
||||
hasTrailingPathSeparator x
|
||||
| BS.null x = False
|
||||
| otherwise = isPathSeparator $ BS.last x
|
||||
|
||||
|
||||
-- | Add a trailing path separator.
|
||||
--
|
||||
-- >>> addTrailingPathSeparator "/path"
|
||||
-- "/path/"
|
||||
-- >>> addTrailingPathSeparator "/path/"
|
||||
-- "/path/"
|
||||
-- >>> addTrailingPathSeparator "/"
|
||||
-- "/"
|
||||
addTrailingPathSeparator :: RawFilePath -> RawFilePath
|
||||
addTrailingPathSeparator x = if hasTrailingPathSeparator x
|
||||
then x
|
||||
else x `BS.snoc` pathSeparator
|
||||
|
||||
|
||||
-- | Remove a trailing path separator
|
||||
--
|
||||
-- >>> dropTrailingPathSeparator "/path/"
|
||||
-- "/path"
|
||||
-- >>> dropTrailingPathSeparator "/path////"
|
||||
-- "/path"
|
||||
-- >>> dropTrailingPathSeparator "/"
|
||||
-- "/"
|
||||
-- >>> dropTrailingPathSeparator "//"
|
||||
-- "/"
|
||||
dropTrailingPathSeparator :: RawFilePath -> RawFilePath
|
||||
dropTrailingPathSeparator x
|
||||
| x == BS.singleton pathSeparator = x
|
||||
| otherwise = if hasTrailingPathSeparator x
|
||||
then dropTrailingPathSeparator $ BS.init x
|
||||
else x
|
||||
|
||||
|
||||
|
||||
------------------------
|
||||
-- File name manipulations
|
||||
|
||||
|
||||
-- |Normalise a file.
|
||||
--
|
||||
-- >>> normalise "/file/\\test////"
|
||||
-- "/file/\\test/"
|
||||
-- >>> normalise "/file/./test"
|
||||
-- "/file/test"
|
||||
-- >>> normalise "/test/file/../bob/fred/"
|
||||
-- "/test/file/../bob/fred/"
|
||||
-- >>> normalise "../bob/fred/"
|
||||
-- "../bob/fred/"
|
||||
-- >>> normalise "./bob/fred/"
|
||||
-- "bob/fred/"
|
||||
-- >>> normalise "./bob////.fred/./...///./..///#."
|
||||
-- "bob/.fred/.../../#."
|
||||
-- >>> normalise "."
|
||||
-- "."
|
||||
-- >>> normalise "./"
|
||||
-- "./"
|
||||
-- >>> normalise "./."
|
||||
-- "./"
|
||||
-- >>> normalise "/./"
|
||||
-- "/"
|
||||
-- >>> normalise "/"
|
||||
-- "/"
|
||||
-- >>> normalise "bob/fred/."
|
||||
-- "bob/fred/"
|
||||
-- >>> normalise "//home"
|
||||
-- "/home"
|
||||
normalise :: RawFilePath -> RawFilePath
|
||||
normalise filepath =
|
||||
result `BS.append`
|
||||
(if addPathSeparator
|
||||
then BS.singleton pathSeparator
|
||||
else BS.empty)
|
||||
where
|
||||
result = let n = f filepath
|
||||
in if BS.null n
|
||||
then BS.singleton _period
|
||||
else n
|
||||
addPathSeparator = isDirPath filepath &&
|
||||
not (hasTrailingPathSeparator result)
|
||||
isDirPath xs = hasTrailingPathSeparator xs
|
||||
|| not (BS.null xs) && BS.last xs == _period
|
||||
&& hasTrailingPathSeparator (BS.init xs)
|
||||
f = joinPath . dropDots . propSep . splitDirectories
|
||||
propSep :: [ByteString] -> [ByteString]
|
||||
propSep (x:xs)
|
||||
| BS.all (== pathSeparator) x = BS.singleton pathSeparator : xs
|
||||
| otherwise = x : xs
|
||||
propSep [] = []
|
||||
dropDots :: [ByteString] -> [ByteString]
|
||||
dropDots = filter (BS.singleton _period /=)
|
||||
|
||||
|
||||
|
||||
-- | Contract a filename, based on a relative path. Note that the resulting
|
||||
-- path will never introduce @..@ paths, as the presence of symlinks
|
||||
-- means @..\/b@ may not reach @a\/b@ if it starts from @a\/c@. For a
|
||||
-- worked example see
|
||||
-- <http://neilmitchell.blogspot.co.uk/2015/10/filepaths-are-subtle-symlinks-are-hard.html this blog post>.
|
||||
--
|
||||
-- >>> makeRelative "/directory" "/directory/file.ext"
|
||||
-- "file.ext"
|
||||
-- >>> makeRelative "/Home" "/home/bob"
|
||||
-- "/home/bob"
|
||||
-- >>> makeRelative "/home/" "/home/bob/foo/bar"
|
||||
-- "bob/foo/bar"
|
||||
-- >>> makeRelative "/fred" "bob"
|
||||
-- "bob"
|
||||
-- >>> makeRelative "/file/test" "/file/test/fred"
|
||||
-- "fred"
|
||||
-- >>> makeRelative "/file/test" "/file/test/fred/"
|
||||
-- "fred/"
|
||||
-- >>> makeRelative "some/path" "some/path/a/b/c"
|
||||
-- "a/b/c"
|
||||
--
|
||||
-- prop> \p -> makeRelative p p == "."
|
||||
-- prop> \p -> makeRelative (takeDirectory p) p `equalFilePath` takeFileName p
|
||||
-- prop \x y -> equalFilePath x y || (isRelative x && makeRelative y x == x) || equalFilePath (y </> makeRelative y x) x
|
||||
makeRelative :: RawFilePath -> RawFilePath -> RawFilePath
|
||||
makeRelative root path
|
||||
| equalFilePath root path = BS.singleton _period
|
||||
| takeAbs root /= takeAbs path = path
|
||||
| otherwise = f (dropAbs root) (dropAbs path)
|
||||
where
|
||||
f x y
|
||||
| BS.null x = BS.dropWhile isPathSeparator y
|
||||
| otherwise = let (x1,x2) = g x
|
||||
(y1,y2) = g y
|
||||
in if equalFilePath x1 y1 then f x2 y2 else path
|
||||
g x = (BS.dropWhile isPathSeparator a, BS.dropWhile isPathSeparator b)
|
||||
where (a, b) = BS.break isPathSeparator $ BS.dropWhile isPathSeparator x
|
||||
dropAbs x = snd $ BS.span (== _slash) x
|
||||
takeAbs x = fst $ BS.span (== _slash) x
|
||||
|
||||
|
||||
-- |Equality of two filepaths. The filepaths are normalised
|
||||
-- and trailing path separators are dropped.
|
||||
--
|
||||
-- >>> equalFilePath "foo" "foo"
|
||||
-- True
|
||||
-- >>> equalFilePath "foo" "foo/"
|
||||
-- True
|
||||
-- >>> equalFilePath "foo" "./foo"
|
||||
-- True
|
||||
-- >>> equalFilePath "" ""
|
||||
-- True
|
||||
-- >>> equalFilePath "foo" "/foo"
|
||||
-- False
|
||||
-- >>> equalFilePath "foo" "FOO"
|
||||
-- False
|
||||
-- >>> equalFilePath "foo" "../foo"
|
||||
-- False
|
||||
--
|
||||
-- prop> \p -> equalFilePath p p
|
||||
equalFilePath :: RawFilePath -> RawFilePath -> Bool
|
||||
equalFilePath p1 p2 = f p1 == f p2
|
||||
where
|
||||
f x = dropTrailingPathSeparator $ normalise x
|
||||
|
||||
|
||||
-- | Check if a path is relative
|
||||
--
|
||||
-- prop> \path -> isRelative path /= isAbsolute path
|
||||
isRelative :: RawFilePath -> Bool
|
||||
isRelative = not . isAbsolute
|
||||
|
||||
|
||||
-- | Check if a path is absolute
|
||||
--
|
||||
-- >>> isAbsolute "/path"
|
||||
-- True
|
||||
-- >>> isAbsolute "path"
|
||||
-- False
|
||||
-- >>> isAbsolute ""
|
||||
-- False
|
||||
isAbsolute :: RawFilePath -> Bool
|
||||
isAbsolute x
|
||||
| BS.length x > 0 = isPathSeparator (BS.head x)
|
||||
| otherwise = False
|
||||
|
||||
|
||||
-- | Is a FilePath valid, i.e. could you create a file like it?
|
||||
--
|
||||
-- >>> isValid ""
|
||||
-- False
|
||||
-- >>> isValid "\0"
|
||||
-- False
|
||||
-- >>> isValid "/random_ path:*"
|
||||
-- True
|
||||
isValid :: RawFilePath -> Bool
|
||||
isValid filepath
|
||||
| BS.null filepath = False
|
||||
| _nul `BS.elem` filepath = False
|
||||
| otherwise = True
|
||||
|
||||
|
||||
-- | Take a FilePath and make it valid; does not change already valid FilePaths.
|
||||
--
|
||||
-- >>> makeValid ""
|
||||
-- "_"
|
||||
-- >>> makeValid "file\0name"
|
||||
-- "file_name"
|
||||
--
|
||||
-- prop> \p -> if isValid p then makeValid p == p else makeValid p /= p
|
||||
-- prop> \p -> isValid (makeValid p)
|
||||
makeValid :: RawFilePath -> RawFilePath
|
||||
makeValid path
|
||||
| BS.null path = BS.singleton _underscore
|
||||
| otherwise = BS.map (\x -> if x == _nul then _underscore else x) path
|
||||
|
||||
|
||||
-- | Is the given path a valid filename? This includes
|
||||
-- "." and "..".
|
||||
--
|
||||
-- >>> isFileName "lal"
|
||||
-- True
|
||||
-- >>> isFileName "."
|
||||
-- True
|
||||
-- >>> isFileName ".."
|
||||
-- True
|
||||
-- >>> isFileName ""
|
||||
-- False
|
||||
-- >>> isFileName "\0"
|
||||
-- False
|
||||
-- >>> isFileName "/random_ path:*"
|
||||
-- False
|
||||
isFileName :: RawFilePath -> Bool
|
||||
isFileName filepath =
|
||||
not (BS.singleton pathSeparator `BS.isInfixOf` filepath) &&
|
||||
not (BS.null filepath) &&
|
||||
not (_nul `BS.elem` filepath)
|
||||
|
||||
|
||||
-- | Check if the filepath has any parent directories in it.
|
||||
--
|
||||
-- >>> hasParentDir "/.."
|
||||
-- True
|
||||
-- >>> hasParentDir "foo/bar/.."
|
||||
-- True
|
||||
-- >>> hasParentDir "foo/../bar/."
|
||||
-- True
|
||||
-- >>> hasParentDir "foo/bar"
|
||||
-- False
|
||||
-- >>> hasParentDir "foo"
|
||||
-- False
|
||||
-- >>> hasParentDir ""
|
||||
-- False
|
||||
-- >>> hasParentDir ".."
|
||||
-- False
|
||||
hasParentDir :: RawFilePath -> Bool
|
||||
hasParentDir filepath =
|
||||
(pathSeparator `BS.cons` pathDoubleDot)
|
||||
`BS.isSuffixOf` filepath
|
||||
||
|
||||
(BS.singleton pathSeparator
|
||||
`BS.append` pathDoubleDot
|
||||
`BS.append` BS.singleton pathSeparator)
|
||||
`BS.isInfixOf` filepath
|
||||
||
|
||||
(pathDoubleDot `BS.append` BS.singleton pathSeparator)
|
||||
`BS.isPrefixOf` filepath
|
||||
where
|
||||
pathDoubleDot = BS.pack [_period, _period]
|
||||
|
||||
|
||||
-- | Whether the file is a hidden file.
|
||||
--
|
||||
-- >>> hiddenFile ".foo"
|
||||
-- True
|
||||
-- >>> hiddenFile "..foo.bar"
|
||||
-- True
|
||||
-- >>> hiddenFile "some/path/.bar"
|
||||
-- True
|
||||
-- >>> hiddenFile "..."
|
||||
-- True
|
||||
-- >>> hiddenFile "dod.bar"
|
||||
-- False
|
||||
-- >>> hiddenFile "."
|
||||
-- False
|
||||
-- >>> hiddenFile ".."
|
||||
-- False
|
||||
-- >>> hiddenFile ""
|
||||
-- False
|
||||
hiddenFile :: RawFilePath -> Bool
|
||||
hiddenFile fp
|
||||
| fn == BS.pack [_period, _period] = False
|
||||
| fn == BS.pack [_period] = False
|
||||
| otherwise = BS.pack [extSeparator]
|
||||
`BS.isPrefixOf` fn
|
||||
where
|
||||
fn = takeFileName fp
|
||||
|
||||
|
||||
|
||||
------------------------
|
||||
-- internal stuff
|
||||
|
||||
-- Just split the input FileName without adding/normalizing or changing
|
||||
-- anything.
|
||||
splitFileNameRaw :: RawFilePath -> (RawFilePath, RawFilePath)
|
||||
splitFileNameRaw = BS.breakEnd isPathSeparator
|
||||
|
||||
-- | Combine two paths, assuming rhs is NOT absolute.
|
||||
combineRaw :: RawFilePath -> RawFilePath -> RawFilePath
|
||||
combineRaw a b | BS.null a = b
|
||||
| BS.null b = a
|
||||
| isPathSeparator (BS.last a) = BS.append a b
|
||||
| otherwise = BS.intercalate (BS.singleton pathSeparator) [a, b]
|
||||
|
||||
7
stack.yaml
Normal file
7
stack.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
resolver: lts-12.1
|
||||
|
||||
packages:
|
||||
- '.'
|
||||
|
||||
extra-deps:
|
||||
- IfElse-0.85
|
||||
108
test/HPath/IO/AppendFileSpec.hs
Normal file
108
test/HPath/IO/AppendFileSpec.hs
Normal file
@@ -0,0 +1,108 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
|
||||
module HPath.IO.AppendFileSpec where
|
||||
|
||||
|
||||
import Test.Hspec
|
||||
import System.IO.Error
|
||||
(
|
||||
ioeGetErrorType
|
||||
)
|
||||
import GHC.IO.Exception
|
||||
(
|
||||
IOErrorType(..)
|
||||
)
|
||||
import Utils
|
||||
|
||||
|
||||
|
||||
upTmpDir :: IO ()
|
||||
upTmpDir = do
|
||||
setTmpDir "AppendFileSpec"
|
||||
createTmpDir
|
||||
|
||||
setupFiles :: IO ()
|
||||
setupFiles = do
|
||||
createRegularFile' "fileWithContent"
|
||||
createRegularFile' "fileWithoutContent"
|
||||
createSymlink' "inputFileSymL" "fileWithContent"
|
||||
createDir' "alreadyExistsD"
|
||||
createRegularFile' "noPerms"
|
||||
noPerms "noPerms"
|
||||
createDir' "noPermsD"
|
||||
createRegularFile' "noPermsD/inputFile"
|
||||
noPerms "noPermsD"
|
||||
writeFile' "fileWithContent" "BLKASL"
|
||||
|
||||
|
||||
cleanupFiles :: IO ()
|
||||
cleanupFiles = do
|
||||
deleteFile' "fileWithContent"
|
||||
deleteFile' "fileWithoutContent"
|
||||
deleteFile' "inputFileSymL"
|
||||
deleteDir' "alreadyExistsD"
|
||||
normalFilePerms "noPerms"
|
||||
deleteFile' "noPerms"
|
||||
normalDirPerms "noPermsD"
|
||||
deleteFile' "noPermsD/inputFile"
|
||||
deleteDir' "noPermsD"
|
||||
|
||||
|
||||
spec :: Spec
|
||||
spec = beforeAll_ (upTmpDir >> setupFiles) $ afterAll_ cleanupFiles $
|
||||
describe "HPath.IO.appendFile" $ do
|
||||
|
||||
-- successes --
|
||||
it "appendFile file with content, everything clear" $ do
|
||||
appendFile' "fileWithContent" "blahfaselllll"
|
||||
out <- readFile' "fileWithContent"
|
||||
out `shouldBe` "BLKASLblahfaselllll"
|
||||
|
||||
it "appendFile file with content, everything clear" $ do
|
||||
appendFile' "fileWithContent" "gagagaga"
|
||||
out <- readFile' "fileWithContent"
|
||||
out `shouldBe` "BLKASLblahfaselllllgagagaga"
|
||||
|
||||
it "appendFile file with content, everything clear" $ do
|
||||
appendFile' "fileWithContent" ""
|
||||
out <- readFile' "fileWithContent"
|
||||
out `shouldBe` "BLKASLblahfaselllllgagagaga"
|
||||
|
||||
it "appendFile file without content, everything clear" $ do
|
||||
appendFile' "fileWithoutContent" "blahfaselllll"
|
||||
out <- readFile' "fileWithoutContent"
|
||||
out `shouldBe` "blahfaselllll"
|
||||
|
||||
it "appendFile, everything clear" $ do
|
||||
appendFile' "fileWithoutContent" "gagagaga"
|
||||
out <- readFile' "fileWithoutContent"
|
||||
out `shouldBe` "blahfaselllllgagagaga"
|
||||
|
||||
it "appendFile symlink, everything clear" $ do
|
||||
appendFile' "inputFileSymL" "blahfaselllll"
|
||||
out <- readFile' "inputFileSymL"
|
||||
out `shouldBe` "BLKASLblahfaselllllgagagagablahfaselllll"
|
||||
|
||||
it "appendFile symlink, everything clear" $ do
|
||||
appendFile' "inputFileSymL" "gagagaga"
|
||||
out <- readFile' "inputFileSymL"
|
||||
out `shouldBe` "BLKASLblahfaselllllgagagagablahfaselllllgagagaga"
|
||||
|
||||
|
||||
-- posix failures --
|
||||
it "appendFile to dir, inappropriate type" $ do
|
||||
appendFile' "alreadyExistsD" ""
|
||||
`shouldThrow` (\e -> ioeGetErrorType e == InappropriateType)
|
||||
|
||||
it "appendFile, no permissions to file" $ do
|
||||
appendFile' "noPerms" ""
|
||||
`shouldThrow` (\e -> ioeGetErrorType e == PermissionDenied)
|
||||
|
||||
it "appendFile, no permissions to file" $ do
|
||||
appendFile' "noPermsD/inputFile" ""
|
||||
`shouldThrow` (\e -> ioeGetErrorType e == PermissionDenied)
|
||||
|
||||
it "appendFile, file does not exist" $ do
|
||||
appendFile' "gaga" ""
|
||||
`shouldThrow` (\e -> ioeGetErrorType e == NoSuchThing)
|
||||
78
test/HPath/IO/CanonicalizePathSpec.hs
Normal file
78
test/HPath/IO/CanonicalizePathSpec.hs
Normal file
@@ -0,0 +1,78 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module HPath.IO.CanonicalizePathSpec where
|
||||
|
||||
|
||||
import Test.Hspec
|
||||
import System.IO.Error
|
||||
(
|
||||
ioeGetErrorType
|
||||
)
|
||||
import GHC.IO.Exception
|
||||
(
|
||||
IOErrorType(..)
|
||||
)
|
||||
import Utils
|
||||
|
||||
|
||||
|
||||
|
||||
upTmpDir :: IO ()
|
||||
upTmpDir = do
|
||||
setTmpDir "CanonicalizePathSpec"
|
||||
createTmpDir
|
||||
|
||||
setupFiles :: IO ()
|
||||
setupFiles = do
|
||||
createRegularFile' "file"
|
||||
createDir' "dir"
|
||||
createSymlink' "dirSym" "dir/"
|
||||
createSymlink' "brokenSym" "nothing"
|
||||
createSymlink' "fileSym" "file"
|
||||
|
||||
cleanupFiles :: IO ()
|
||||
cleanupFiles = do
|
||||
deleteFile' "file"
|
||||
deleteDir' "dir"
|
||||
deleteFile' "dirSym"
|
||||
deleteFile' "brokenSym"
|
||||
deleteFile' "fileSym"
|
||||
|
||||
|
||||
spec :: Spec
|
||||
spec = beforeAll_ (upTmpDir >> setupFiles) $ afterAll_ cleanupFiles $
|
||||
describe "HPath.IO.canonicalizePath" $ do
|
||||
|
||||
-- successes --
|
||||
it "canonicalizePath, all fine" $ do
|
||||
path <- withTmpDir "file" return
|
||||
canonicalizePath' "file"
|
||||
`shouldReturn` path
|
||||
|
||||
it "canonicalizePath, all fine" $ do
|
||||
path <- withTmpDir "dir" return
|
||||
canonicalizePath' "dir"
|
||||
`shouldReturn` path
|
||||
|
||||
it "canonicalizePath, all fine" $ do
|
||||
path <- withTmpDir "file" return
|
||||
canonicalizePath' "fileSym"
|
||||
`shouldReturn` path
|
||||
|
||||
it "canonicalizePath, all fine" $ do
|
||||
path <- withTmpDir "dir" return
|
||||
canonicalizePath' "dirSym"
|
||||
`shouldReturn` path
|
||||
|
||||
|
||||
-- posix failures --
|
||||
it "canonicalizePath, broken symlink" $
|
||||
canonicalizePath' "brokenSym"
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == NoSuchThing)
|
||||
|
||||
it "canonicalizePath, file does not exist" $
|
||||
canonicalizePath' "nothingBlah"
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == NoSuchThing)
|
||||
|
||||
247
test/HPath/IO/CopyDirRecursiveCollectFailuresSpec.hs
Normal file
247
test/HPath/IO/CopyDirRecursiveCollectFailuresSpec.hs
Normal file
@@ -0,0 +1,247 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
|
||||
module HPath.IO.CopyDirRecursiveCollectFailuresSpec where
|
||||
|
||||
|
||||
import Test.Hspec
|
||||
import Data.List (sort)
|
||||
import HPath.IO
|
||||
import HPath.IO.Errors
|
||||
import System.IO.Error
|
||||
(
|
||||
ioeGetErrorType
|
||||
)
|
||||
import GHC.IO.Exception
|
||||
(
|
||||
IOErrorType(..)
|
||||
)
|
||||
import System.Exit
|
||||
import System.Process
|
||||
import Utils
|
||||
import qualified Data.ByteString as BS
|
||||
import Data.ByteString.UTF8 (toString)
|
||||
|
||||
|
||||
|
||||
upTmpDir :: IO ()
|
||||
upTmpDir = do
|
||||
setTmpDir "CopyDirRecursiveCollectFailuresSpec"
|
||||
createTmpDir
|
||||
|
||||
setupFiles :: IO ()
|
||||
setupFiles = do
|
||||
createRegularFile' "alreadyExists"
|
||||
createRegularFile' "wrongInput"
|
||||
createSymlink' "wrongInputSymL" "inputDir/"
|
||||
createDir' "alreadyExistsD"
|
||||
createDir' "noPerms"
|
||||
createDir' "noWritePerm"
|
||||
|
||||
createDir' "inputDir"
|
||||
createDir' "inputDir/bar"
|
||||
createDir' "inputDir/foo"
|
||||
createRegularFile' "inputDir/foo/inputFile1"
|
||||
createRegularFile' "inputDir/inputFile2"
|
||||
createRegularFile' "inputDir/bar/inputFile3"
|
||||
writeFile' "inputDir/foo/inputFile1" "SDAADSdsada"
|
||||
writeFile' "inputDir/inputFile2" "Blahfaselgagaga"
|
||||
writeFile' "inputDir/bar/inputFile3"
|
||||
"fdfdssdffsd3223sasdasdasdadasasddasdasdasasd4"
|
||||
|
||||
createDir' "inputDir1"
|
||||
createDir' "inputDir1/foo2"
|
||||
createDir' "inputDir1/foo2/foo3"
|
||||
createDir' "inputDir1/foo2/foo4"
|
||||
createRegularFile' "inputDir1/foo2/inputFile1"
|
||||
createRegularFile' "inputDir1/foo2/inputFile2"
|
||||
createRegularFile' "inputDir1/foo2/inputFile3"
|
||||
createRegularFile' "inputDir1/foo2/foo4/inputFile4"
|
||||
createRegularFile' "inputDir1/foo2/foo4/inputFile6"
|
||||
createRegularFile' "inputDir1/foo2/foo3/inputFile5"
|
||||
noPerms "inputDir1/foo2/foo3"
|
||||
|
||||
createDir' "outputDir1"
|
||||
createDir' "outputDir1/foo2"
|
||||
createDir' "outputDir1/foo2/foo4"
|
||||
createDir' "outputDir1/foo2/foo4/inputFile4"
|
||||
createRegularFile' "outputDir1/foo2/foo4/inputFile6"
|
||||
noPerms "outputDir1/foo2/foo4/inputFile4"
|
||||
noPerms "outputDir1/foo2/foo4"
|
||||
|
||||
noPerms "noPerms"
|
||||
noWritableDirPerms "noWritePerm"
|
||||
|
||||
|
||||
cleanupFiles :: IO ()
|
||||
cleanupFiles = do
|
||||
normalDirPerms "noPerms"
|
||||
normalDirPerms "noWritePerm"
|
||||
|
||||
normalDirPerms "inputDir1/foo2/foo3"
|
||||
deleteFile' "inputDir1/foo2/foo4/inputFile4"
|
||||
deleteFile' "inputDir1/foo2/foo4/inputFile6"
|
||||
deleteFile' "inputDir1/foo2/inputFile1"
|
||||
deleteFile' "inputDir1/foo2/inputFile2"
|
||||
deleteFile' "inputDir1/foo2/inputFile3"
|
||||
deleteFile' "inputDir1/foo2/foo3/inputFile5"
|
||||
deleteDir' "inputDir1/foo2/foo3"
|
||||
deleteDir' "inputDir1/foo2/foo4"
|
||||
deleteDir' "inputDir1/foo2"
|
||||
deleteDir' "inputDir1"
|
||||
|
||||
normalDirPerms "outputDir1/foo2/foo4"
|
||||
normalDirPerms "outputDir1/foo2/foo4/inputFile4"
|
||||
deleteFile' "outputDir1/foo2/foo4/inputFile6"
|
||||
deleteDir' "outputDir1/foo2/foo4/inputFile4"
|
||||
deleteDir' "outputDir1/foo2/foo4"
|
||||
deleteDir' "outputDir1/foo2"
|
||||
deleteDir' "outputDir1"
|
||||
|
||||
deleteFile' "alreadyExists"
|
||||
deleteFile' "wrongInput"
|
||||
deleteFile' "wrongInputSymL"
|
||||
deleteDir' "alreadyExistsD"
|
||||
deleteDir' "noPerms"
|
||||
deleteDir' "noWritePerm"
|
||||
deleteFile' "inputDir/foo/inputFile1"
|
||||
deleteFile' "inputDir/inputFile2"
|
||||
deleteFile' "inputDir/bar/inputFile3"
|
||||
deleteDir' "inputDir/foo"
|
||||
deleteDir' "inputDir/bar"
|
||||
deleteDir' "inputDir"
|
||||
|
||||
|
||||
|
||||
|
||||
spec :: Spec
|
||||
spec = beforeAll_ (upTmpDir >> setupFiles) $ afterAll_ cleanupFiles $
|
||||
describe "HPath.IO.copyDirRecursive" $ do
|
||||
|
||||
-- successes --
|
||||
it "copyDirRecursive (Strict, CollectFailures), all fine and compare" $ do
|
||||
tmpDir' <- getRawTmpDir
|
||||
copyDirRecursive' "inputDir"
|
||||
"outputDir"
|
||||
Strict
|
||||
CollectFailures
|
||||
(system $ "diff -r --no-dereference "
|
||||
++ toString tmpDir' ++ "inputDir" ++ " "
|
||||
++ toString tmpDir' ++ "outputDir")
|
||||
`shouldReturn` ExitSuccess
|
||||
removeDirIfExists "outputDir"
|
||||
|
||||
-- posix failures --
|
||||
it "copyDirRecursive (Strict, CollectFailures), source directory does not exist" $
|
||||
copyDirRecursive' "doesNotExist"
|
||||
"outputDir"
|
||||
Strict
|
||||
CollectFailures
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == NoSuchThing)
|
||||
|
||||
it "copyDirRecursive (Strict, CollectFailures), cannot open source dir" $
|
||||
copyDirRecursive' "noPerms/inputDir"
|
||||
"foo"
|
||||
Strict
|
||||
CollectFailures
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == PermissionDenied)
|
||||
|
||||
|
||||
-- custom failures
|
||||
it "copyDirRecursive (Overwrite, CollectFailures), various failures" $ do
|
||||
copyDirRecursive' "inputDir1/foo2"
|
||||
"outputDir1/foo2"
|
||||
Overwrite
|
||||
CollectFailures
|
||||
`shouldThrow`
|
||||
(\(RecursiveFailure ex@[_, _]) ->
|
||||
any (\(h, e) -> ioeGetErrorType e == InappropriateType
|
||||
&& isCopyFileFailed h) ex &&
|
||||
any (\(h, e) -> ioeGetErrorType e == PermissionDenied
|
||||
&& isReadContentsFailed h) ex)
|
||||
normalDirPerms "outputDir1/foo2/foo4"
|
||||
normalDirPerms "outputDir1/foo2/foo4/inputFile4"
|
||||
c <- allDirectoryContents' "outputDir1"
|
||||
tmpDir' <- getRawTmpDir
|
||||
let shouldC = (fmap (\x -> tmpDir' `BS.append` x)
|
||||
["outputDir1"
|
||||
,"outputDir1/foo2"
|
||||
,"outputDir1/foo2/inputFile1"
|
||||
,"outputDir1/foo2/inputFile2"
|
||||
,"outputDir1/foo2/inputFile3"
|
||||
,"outputDir1/foo2/foo4"
|
||||
,"outputDir1/foo2/foo4/inputFile6"
|
||||
,"outputDir1/foo2/foo4/inputFile4"])
|
||||
deleteFile' "outputDir1/foo2/inputFile1"
|
||||
deleteFile' "outputDir1/foo2/inputFile2"
|
||||
deleteFile' "outputDir1/foo2/inputFile3"
|
||||
sort c `shouldBe` sort shouldC
|
||||
|
||||
|
||||
it "copyDirRecursive (Strict, CollectFailures), no write permission on output dir" $
|
||||
copyDirRecursive' "inputDir"
|
||||
"noWritePerm/foo"
|
||||
Strict
|
||||
CollectFailures
|
||||
`shouldThrow`
|
||||
(\(RecursiveFailure [(CreateDirFailed{}, e)]) -> ioeGetErrorType e == PermissionDenied)
|
||||
|
||||
it "copyDirRecursive (Strict, CollectFailures), cannot open output dir" $
|
||||
copyDirRecursive' "inputDir"
|
||||
"noPerms/foo"
|
||||
Strict
|
||||
CollectFailures
|
||||
`shouldThrow`
|
||||
isRecursiveFailure
|
||||
|
||||
it "copyDirRecursive (Strict, CollectFailures), destination dir already exists" $
|
||||
copyDirRecursive' "inputDir"
|
||||
"alreadyExistsD"
|
||||
Strict
|
||||
CollectFailures
|
||||
`shouldThrow`
|
||||
(\(RecursiveFailure [(CreateDirFailed{}, e)]) -> ioeGetErrorType e == AlreadyExists)
|
||||
|
||||
it "copyDirRecursive (Strict, CollectFailures), destination already exists and is a file" $
|
||||
copyDirRecursive' "inputDir"
|
||||
"alreadyExists"
|
||||
Strict
|
||||
CollectFailures
|
||||
`shouldThrow`
|
||||
isRecursiveFailure
|
||||
|
||||
it "copyDirRecursive (Strict, CollectFailures), wrong input (regular file)" $
|
||||
copyDirRecursive' "wrongInput"
|
||||
"outputDir"
|
||||
Strict
|
||||
CollectFailures
|
||||
`shouldThrow`
|
||||
(\(RecursiveFailure [(ReadContentsFailed{}, e)]) -> ioeGetErrorType e == InappropriateType)
|
||||
|
||||
it "copyDirRecursive (Strict, CollectFailures), wrong input (symlink to directory)" $
|
||||
copyDirRecursive' "wrongInputSymL"
|
||||
"outputDir"
|
||||
Strict
|
||||
CollectFailures
|
||||
`shouldThrow`
|
||||
(\(RecursiveFailure [(ReadContentsFailed{}, e)]) -> ioeGetErrorType e == InvalidArgument)
|
||||
|
||||
it "copyDirRecursive (Strict, CollectFailures), destination in source" $
|
||||
copyDirRecursive' "inputDir"
|
||||
"inputDir/foo"
|
||||
Strict
|
||||
CollectFailures
|
||||
`shouldThrow`
|
||||
isDestinationInSource
|
||||
|
||||
it "copyDirRecursive (Strict, CollectFailures), destination and source same directory" $
|
||||
copyDirRecursive' "inputDir"
|
||||
"inputDir"
|
||||
Strict
|
||||
CollectFailures
|
||||
`shouldThrow`
|
||||
isSameFile
|
||||
|
||||
|
||||
202
test/HPath/IO/CopyDirRecursiveOverwriteSpec.hs
Normal file
202
test/HPath/IO/CopyDirRecursiveOverwriteSpec.hs
Normal file
@@ -0,0 +1,202 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
|
||||
module HPath.IO.CopyDirRecursiveOverwriteSpec where
|
||||
|
||||
|
||||
import Test.Hspec
|
||||
import HPath.IO
|
||||
import HPath.IO.Errors
|
||||
import System.IO.Error
|
||||
(
|
||||
ioeGetErrorType
|
||||
)
|
||||
import GHC.IO.Exception
|
||||
(
|
||||
IOErrorType(..)
|
||||
)
|
||||
import System.Exit
|
||||
import System.Process
|
||||
import Utils
|
||||
import Data.ByteString.UTF8 (toString)
|
||||
|
||||
|
||||
|
||||
upTmpDir :: IO ()
|
||||
upTmpDir = do
|
||||
setTmpDir "CopyDirRecursiveOverwriteSpec"
|
||||
createTmpDir
|
||||
|
||||
|
||||
setupFiles :: IO ()
|
||||
setupFiles = do
|
||||
createRegularFile' "alreadyExists"
|
||||
createRegularFile' "wrongInput"
|
||||
createSymlink' "wrongInputSymL" "inputDir/"
|
||||
createDir' "noPerms"
|
||||
createDir' "noWritePerm"
|
||||
|
||||
createDir' "inputDir"
|
||||
createDir' "inputDir/bar"
|
||||
createDir' "inputDir/foo"
|
||||
createRegularFile' "inputDir/foo/inputFile1"
|
||||
createRegularFile' "inputDir/inputFile2"
|
||||
createRegularFile' "inputDir/bar/inputFile3"
|
||||
writeFile' "inputDir/foo/inputFile1" "SDAADSdsada"
|
||||
writeFile' "inputDir/inputFile2" "Blahfaselgagaga"
|
||||
writeFile' "inputDir/bar/inputFile3"
|
||||
"fdfdssdffsd3223sasdasdasdadasasddasdasdasasd4"
|
||||
|
||||
createDir' "alreadyExistsD"
|
||||
createDir' "alreadyExistsD/bar"
|
||||
createDir' "alreadyExistsD/foo"
|
||||
createRegularFile' "alreadyExistsD/foo/inputFile1"
|
||||
createRegularFile' "alreadyExistsD/inputFile2"
|
||||
createRegularFile' "alreadyExistsD/bar/inputFile3"
|
||||
writeFile' "alreadyExistsD/foo/inputFile1" "DAAsada"
|
||||
writeFile' "alreadyExistsD/inputFile2" "ahfaagaga"
|
||||
writeFile' "alreadyExistsD/bar/inputFile3"
|
||||
"f3223sasdasdaasdasdasasd4"
|
||||
|
||||
noPerms "noPerms"
|
||||
noWritableDirPerms "noWritePerm"
|
||||
|
||||
|
||||
cleanupFiles :: IO ()
|
||||
cleanupFiles = do
|
||||
normalDirPerms "noPerms"
|
||||
normalDirPerms "noWritePerm"
|
||||
deleteFile' "alreadyExists"
|
||||
deleteFile' "wrongInput"
|
||||
deleteFile' "wrongInputSymL"
|
||||
deleteDir' "noPerms"
|
||||
deleteDir' "noWritePerm"
|
||||
deleteFile' "inputDir/foo/inputFile1"
|
||||
deleteFile' "inputDir/inputFile2"
|
||||
deleteFile' "inputDir/bar/inputFile3"
|
||||
deleteDir' "inputDir/foo"
|
||||
deleteDir' "inputDir/bar"
|
||||
deleteDir' "inputDir"
|
||||
deleteFile' "alreadyExistsD/foo/inputFile1"
|
||||
deleteFile' "alreadyExistsD/inputFile2"
|
||||
deleteFile' "alreadyExistsD/bar/inputFile3"
|
||||
deleteDir' "alreadyExistsD/foo"
|
||||
deleteDir' "alreadyExistsD/bar"
|
||||
deleteDir' "alreadyExistsD"
|
||||
|
||||
|
||||
|
||||
spec :: Spec
|
||||
spec = beforeAll_ (upTmpDir >> setupFiles) $ afterAll_ cleanupFiles $
|
||||
describe "HPath.IO.copyDirRecursive" $ do
|
||||
|
||||
-- successes --
|
||||
it "copyDirRecursive (Overwrite, FailEarly), all fine" $ do
|
||||
copyDirRecursive' "inputDir"
|
||||
"outputDir"
|
||||
Overwrite
|
||||
FailEarly
|
||||
removeDirIfExists "outputDir"
|
||||
|
||||
it "copyDirRecursive (Overwrite, FailEarly), all fine and compare" $ do
|
||||
tmpDir' <- getRawTmpDir
|
||||
copyDirRecursive' "inputDir"
|
||||
"outputDir"
|
||||
Overwrite
|
||||
FailEarly
|
||||
(system $ "diff -r --no-dereference "
|
||||
++ toString tmpDir' ++ "inputDir" ++ " "
|
||||
++ toString tmpDir' ++ "outputDir")
|
||||
`shouldReturn` ExitSuccess
|
||||
removeDirIfExists "outputDir"
|
||||
|
||||
it "copyDirRecursive (Overwrite, FailEarly), destination dir already exists" $ do
|
||||
tmpDir' <- getRawTmpDir
|
||||
(system $ "diff -r --no-dereference "
|
||||
++ toString tmpDir' ++ "inputDir" ++ " "
|
||||
++ toString tmpDir' ++ "alreadyExistsD")
|
||||
`shouldReturn` (ExitFailure 1)
|
||||
copyDirRecursive' "inputDir"
|
||||
"alreadyExistsD"
|
||||
Overwrite
|
||||
FailEarly
|
||||
(system $ "diff -r --no-dereference "
|
||||
++ toString tmpDir' ++ "inputDir" ++ " "
|
||||
++ toString tmpDir' ++ "alreadyExistsD")
|
||||
`shouldReturn` ExitSuccess
|
||||
removeDirIfExists "outputDir"
|
||||
|
||||
|
||||
-- posix failures --
|
||||
it "copyDirRecursive, source directory does not exist" $
|
||||
copyDirRecursive' "doesNotExist"
|
||||
"outputDir"
|
||||
Overwrite
|
||||
FailEarly
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == NoSuchThing)
|
||||
|
||||
it "copyDirRecursive, no write permission on output dir" $
|
||||
copyDirRecursive' "inputDir"
|
||||
"noWritePerm/foo"
|
||||
Overwrite
|
||||
FailEarly
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == PermissionDenied)
|
||||
|
||||
it "copyDirRecursive, cannot open output dir" $
|
||||
copyDirRecursive' "inputDir"
|
||||
"noPerms/foo"
|
||||
Overwrite
|
||||
FailEarly
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == PermissionDenied)
|
||||
|
||||
it "copyDirRecursive, cannot open source dir" $
|
||||
copyDirRecursive' "noPerms/inputDir"
|
||||
"foo"
|
||||
Overwrite
|
||||
FailEarly
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == PermissionDenied)
|
||||
|
||||
it "copyDirRecursive, destination already exists and is a file" $
|
||||
copyDirRecursive' "inputDir"
|
||||
"alreadyExists"
|
||||
Overwrite
|
||||
FailEarly
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == InappropriateType)
|
||||
|
||||
it "copyDirRecursive, wrong input (regular file)" $
|
||||
copyDirRecursive' "wrongInput"
|
||||
"outputDir"
|
||||
Overwrite
|
||||
FailEarly
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == InappropriateType)
|
||||
|
||||
it "copyDirRecursive, wrong input (symlink to directory)" $
|
||||
copyDirRecursive' "wrongInputSymL"
|
||||
"outputDir"
|
||||
Overwrite
|
||||
FailEarly
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == InvalidArgument)
|
||||
|
||||
-- custom failures
|
||||
it "copyDirRecursive (Overwrite, FailEarly), destination in source" $
|
||||
copyDirRecursive' "inputDir"
|
||||
"inputDir/foo"
|
||||
Overwrite
|
||||
FailEarly
|
||||
`shouldThrow`
|
||||
isDestinationInSource
|
||||
|
||||
it "copyDirRecursive (Overwrite, FailEarly), destination and source same directory" $
|
||||
copyDirRecursive' "inputDir"
|
||||
"inputDir"
|
||||
Overwrite
|
||||
FailEarly
|
||||
`shouldThrow`
|
||||
isSameFile
|
||||
180
test/HPath/IO/CopyDirRecursiveSpec.hs
Normal file
180
test/HPath/IO/CopyDirRecursiveSpec.hs
Normal file
@@ -0,0 +1,180 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
|
||||
module HPath.IO.CopyDirRecursiveSpec where
|
||||
|
||||
|
||||
import Test.Hspec
|
||||
import HPath.IO
|
||||
import HPath.IO.Errors
|
||||
import System.IO.Error
|
||||
(
|
||||
ioeGetErrorType
|
||||
)
|
||||
import GHC.IO.Exception
|
||||
(
|
||||
IOErrorType(..)
|
||||
)
|
||||
import System.Exit
|
||||
import System.Process
|
||||
import Utils
|
||||
import Data.ByteString.UTF8 (toString)
|
||||
|
||||
|
||||
|
||||
upTmpDir :: IO ()
|
||||
upTmpDir = do
|
||||
setTmpDir "CopyDirRecursiveSpec"
|
||||
createTmpDir
|
||||
|
||||
setupFiles :: IO ()
|
||||
setupFiles = do
|
||||
createRegularFile' "alreadyExists"
|
||||
createRegularFile' "wrongInput"
|
||||
createSymlink' "wrongInputSymL" "inputDir/"
|
||||
createDir' "alreadyExistsD"
|
||||
createDir' "noPerms"
|
||||
createDir' "noWritePerm"
|
||||
|
||||
createDir' "inputDir"
|
||||
createDir' "inputDir/bar"
|
||||
createDir' "inputDir/foo"
|
||||
createRegularFile' "inputDir/foo/inputFile1"
|
||||
createRegularFile' "inputDir/inputFile2"
|
||||
createRegularFile' "inputDir/bar/inputFile3"
|
||||
writeFile' "inputDir/foo/inputFile1" "SDAADSdsada"
|
||||
writeFile' "inputDir/inputFile2" "Blahfaselgagaga"
|
||||
writeFile' "inputDir/bar/inputFile3"
|
||||
"fdfdssdffsd3223sasdasdasdadasasddasdasdasasd4"
|
||||
|
||||
noPerms "noPerms"
|
||||
noWritableDirPerms "noWritePerm"
|
||||
|
||||
|
||||
cleanupFiles :: IO ()
|
||||
cleanupFiles = do
|
||||
normalDirPerms "noPerms"
|
||||
normalDirPerms "noWritePerm"
|
||||
deleteFile' "alreadyExists"
|
||||
deleteFile' "wrongInput"
|
||||
deleteFile' "wrongInputSymL"
|
||||
deleteDir' "alreadyExistsD"
|
||||
deleteDir' "noPerms"
|
||||
deleteDir' "noWritePerm"
|
||||
deleteFile' "inputDir/foo/inputFile1"
|
||||
deleteFile' "inputDir/inputFile2"
|
||||
deleteFile' "inputDir/bar/inputFile3"
|
||||
deleteDir' "inputDir/foo"
|
||||
deleteDir' "inputDir/bar"
|
||||
deleteDir' "inputDir"
|
||||
|
||||
|
||||
|
||||
|
||||
spec :: Spec
|
||||
spec = beforeAll_ (upTmpDir >> setupFiles) $ afterAll_ cleanupFiles $
|
||||
describe "HPath.IO.copyDirRecursive" $ do
|
||||
|
||||
-- successes --
|
||||
it "copyDirRecursive (Strict, FailEarly), all fine" $ do
|
||||
copyDirRecursive' "inputDir"
|
||||
"outputDir"
|
||||
Strict
|
||||
FailEarly
|
||||
removeDirIfExists "outputDir"
|
||||
|
||||
it "copyDirRecursive (Strict, FailEarly), all fine and compare" $ do
|
||||
tmpDir' <- getRawTmpDir
|
||||
copyDirRecursive' "inputDir"
|
||||
"outputDir"
|
||||
Strict
|
||||
FailEarly
|
||||
(system $ "diff -r --no-dereference "
|
||||
++ toString tmpDir' ++ "inputDir" ++ " "
|
||||
++ toString tmpDir' ++ "outputDir")
|
||||
`shouldReturn` ExitSuccess
|
||||
removeDirIfExists "outputDir"
|
||||
|
||||
-- posix failures --
|
||||
it "copyDirRecursive (Strict, FailEarly), source directory does not exist" $
|
||||
copyDirRecursive' "doesNotExist"
|
||||
"outputDir"
|
||||
Strict
|
||||
FailEarly
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == NoSuchThing)
|
||||
|
||||
it "copyDirRecursive (Strict, FailEarly), no write permission on output dir" $
|
||||
copyDirRecursive' "inputDir"
|
||||
"noWritePerm/foo"
|
||||
Strict
|
||||
FailEarly
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == PermissionDenied)
|
||||
|
||||
it "copyDirRecursive (Strict, FailEarly), cannot open output dir" $
|
||||
copyDirRecursive' "inputDir"
|
||||
"noPerms/foo"
|
||||
Strict
|
||||
FailEarly
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == PermissionDenied)
|
||||
|
||||
it "copyDirRecursive (Strict, FailEarly), cannot open source dir" $
|
||||
copyDirRecursive' "noPerms/inputDir"
|
||||
"foo"
|
||||
Strict
|
||||
FailEarly
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == PermissionDenied)
|
||||
|
||||
it "copyDirRecursive (Strict, FailEarly), destination dir already exists" $
|
||||
copyDirRecursive' "inputDir"
|
||||
"alreadyExistsD"
|
||||
Strict
|
||||
FailEarly
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == AlreadyExists)
|
||||
|
||||
it "copyDirRecursive (Strict, FailEarly), destination already exists and is a file" $
|
||||
copyDirRecursive' "inputDir"
|
||||
"alreadyExists"
|
||||
Strict
|
||||
FailEarly
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == AlreadyExists)
|
||||
|
||||
it "copyDirRecursive (Strict, FailEarly), wrong input (regular file)" $
|
||||
copyDirRecursive' "wrongInput"
|
||||
"outputDir"
|
||||
Strict
|
||||
FailEarly
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == InappropriateType)
|
||||
|
||||
it "copyDirRecursive (Strict, FailEarly), wrong input (symlink to directory)" $
|
||||
copyDirRecursive' "wrongInputSymL"
|
||||
"outputDir"
|
||||
Strict
|
||||
FailEarly
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == InvalidArgument)
|
||||
|
||||
-- custom failures
|
||||
it "copyDirRecursive (Strict, FailEarly), destination in source" $
|
||||
copyDirRecursive' "inputDir"
|
||||
"inputDir/foo"
|
||||
Strict
|
||||
FailEarly
|
||||
`shouldThrow`
|
||||
isDestinationInSource
|
||||
|
||||
it "copyDirRecursive (Strict, FailEarly), destination and source same directory" $
|
||||
copyDirRecursive' "inputDir"
|
||||
"inputDir"
|
||||
Strict
|
||||
FailEarly
|
||||
`shouldThrow`
|
||||
isSameFile
|
||||
|
||||
|
||||
148
test/HPath/IO/CopyFileOverwriteSpec.hs
Normal file
148
test/HPath/IO/CopyFileOverwriteSpec.hs
Normal file
@@ -0,0 +1,148 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module HPath.IO.CopyFileOverwriteSpec where
|
||||
|
||||
|
||||
import Test.Hspec
|
||||
import HPath.IO
|
||||
import HPath.IO.Errors
|
||||
import System.IO.Error
|
||||
(
|
||||
ioeGetErrorType
|
||||
)
|
||||
import GHC.IO.Exception
|
||||
(
|
||||
IOErrorType(..)
|
||||
)
|
||||
import System.Exit
|
||||
import System.Process
|
||||
import Utils
|
||||
import Data.ByteString.UTF8 (toString)
|
||||
|
||||
|
||||
|
||||
upTmpDir :: IO ()
|
||||
upTmpDir = do
|
||||
setTmpDir "CopyFileOverwriteSpec"
|
||||
createTmpDir
|
||||
|
||||
|
||||
setupFiles :: IO ()
|
||||
setupFiles = do
|
||||
createRegularFile' "inputFile"
|
||||
createRegularFile' "alreadyExists"
|
||||
createSymlink' "inputFileSymL" "inputFile"
|
||||
createDir' "alreadyExistsD"
|
||||
createDir' "noPerms"
|
||||
createRegularFile' "noPerms/inputFile"
|
||||
createDir' "outputDirNoWrite"
|
||||
createDir' "wrongInput"
|
||||
noPerms "noPerms"
|
||||
noWritableDirPerms "outputDirNoWrite"
|
||||
writeFile' "inputFile" "Blahfaselgagaga"
|
||||
writeFile' "alreadyExists" "dsaldsalkaklsdlkasksdadasl"
|
||||
|
||||
|
||||
cleanupFiles :: IO ()
|
||||
cleanupFiles = do
|
||||
normalDirPerms "noPerms"
|
||||
normalDirPerms "outputDirNoWrite"
|
||||
deleteFile' "noPerms/inputFile"
|
||||
deleteFile' "inputFile"
|
||||
deleteFile' "alreadyExists"
|
||||
deleteFile' "inputFileSymL"
|
||||
deleteDir' "alreadyExistsD"
|
||||
deleteDir' "noPerms"
|
||||
deleteDir' "outputDirNoWrite"
|
||||
deleteDir' "wrongInput"
|
||||
|
||||
|
||||
spec :: Spec
|
||||
spec = beforeAll_ (upTmpDir >> setupFiles) $ afterAll_ cleanupFiles $
|
||||
describe "HPath.IO.copyFile" $ do
|
||||
|
||||
-- successes --
|
||||
it "copyFile (Overwrite), everything clear" $ do
|
||||
copyFile' "inputFile"
|
||||
"outputFile"
|
||||
Overwrite
|
||||
removeFileIfExists "outputFile"
|
||||
|
||||
it "copyFile (Overwrite), output file already exists, all clear" $ do
|
||||
tmpDir' <- getRawTmpDir
|
||||
copyFile' "alreadyExists" "alreadyExists.bak" Strict
|
||||
copyFile' "inputFile" "alreadyExists" Overwrite
|
||||
(system $ "cmp -s " ++ toString tmpDir' ++ "inputFile" ++ " "
|
||||
++ toString tmpDir' ++ "alreadyExists")
|
||||
`shouldReturn` ExitSuccess
|
||||
removeFileIfExists "alreadyExists"
|
||||
copyFile' "alreadyExists.bak" "alreadyExists" Strict
|
||||
removeFileIfExists "alreadyExists.bak"
|
||||
|
||||
it "copyFile (Overwrite), and compare" $ do
|
||||
tmpDir' <- getRawTmpDir
|
||||
copyFile' "inputFile"
|
||||
"outputFile"
|
||||
Overwrite
|
||||
(system $ "cmp -s " ++ toString tmpDir' ++ "inputFile" ++ " "
|
||||
++ toString tmpDir' ++ "outputFile")
|
||||
`shouldReturn` ExitSuccess
|
||||
removeFileIfExists "outputFile"
|
||||
|
||||
|
||||
-- posix failures --
|
||||
it "copyFile (Overwrite), input file does not exist" $
|
||||
copyFile' "noSuchFile"
|
||||
"outputFile"
|
||||
Overwrite
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == NoSuchThing)
|
||||
|
||||
it "copyFile (Overwrite), no permission to write to output directory" $
|
||||
copyFile' "inputFile"
|
||||
"outputDirNoWrite/outputFile"
|
||||
Overwrite
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == PermissionDenied)
|
||||
|
||||
it "copyFile (Overwrite), cannot open output directory" $
|
||||
copyFile' "inputFile"
|
||||
"noPerms/outputFile"
|
||||
Overwrite
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == PermissionDenied)
|
||||
|
||||
it "copyFile (Overwrite), cannot open source directory" $
|
||||
copyFile' "noPerms/inputFile"
|
||||
"outputFile"
|
||||
Overwrite
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == PermissionDenied)
|
||||
|
||||
it "copyFile (Overwrite), wrong input type (symlink)" $
|
||||
copyFile' "inputFileSymL"
|
||||
"outputFile"
|
||||
Overwrite
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == InvalidArgument)
|
||||
|
||||
it "copyFile (Overwrite), wrong input type (directory)" $
|
||||
copyFile' "wrongInput"
|
||||
"outputFile"
|
||||
Overwrite
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == InappropriateType)
|
||||
|
||||
it "copyFile (Overwrite), output file already exists and is a dir" $
|
||||
copyFile' "inputFile"
|
||||
"alreadyExistsD"
|
||||
Overwrite
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == InappropriateType)
|
||||
|
||||
-- custom failures --
|
||||
it "copyFile (Overwrite), output and input are same file" $
|
||||
copyFile' "inputFile"
|
||||
"inputFile"
|
||||
Overwrite
|
||||
`shouldThrow` isSameFile
|
||||
143
test/HPath/IO/CopyFileSpec.hs
Normal file
143
test/HPath/IO/CopyFileSpec.hs
Normal file
@@ -0,0 +1,143 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
|
||||
module HPath.IO.CopyFileSpec where
|
||||
|
||||
|
||||
import Test.Hspec
|
||||
import HPath.IO
|
||||
import HPath.IO.Errors
|
||||
import System.IO.Error
|
||||
(
|
||||
ioeGetErrorType
|
||||
)
|
||||
import GHC.IO.Exception
|
||||
(
|
||||
IOErrorType(..)
|
||||
)
|
||||
import System.Exit
|
||||
import System.Process
|
||||
import Utils
|
||||
import Data.ByteString.UTF8 (toString)
|
||||
|
||||
|
||||
|
||||
upTmpDir :: IO ()
|
||||
upTmpDir = do
|
||||
setTmpDir "CopyFileSpec"
|
||||
createTmpDir
|
||||
|
||||
setupFiles :: IO ()
|
||||
setupFiles = do
|
||||
createRegularFile' "inputFile"
|
||||
createRegularFile' "alreadyExists"
|
||||
createSymlink' "inputFileSymL" "inputFile"
|
||||
createDir' "alreadyExistsD"
|
||||
createDir' "noPerms"
|
||||
createRegularFile' "noPerms/inputFile"
|
||||
createDir' "outputDirNoWrite"
|
||||
createDir' "wrongInput"
|
||||
noPerms "noPerms"
|
||||
noWritableDirPerms "outputDirNoWrite"
|
||||
writeFile' "inputFile" "Blahfaselgagaga"
|
||||
|
||||
|
||||
cleanupFiles :: IO ()
|
||||
cleanupFiles = do
|
||||
normalDirPerms "noPerms"
|
||||
normalDirPerms "outputDirNoWrite"
|
||||
deleteFile' "noPerms/inputFile"
|
||||
deleteFile' "inputFile"
|
||||
deleteFile' "alreadyExists"
|
||||
deleteFile' "inputFileSymL"
|
||||
deleteDir' "alreadyExistsD"
|
||||
deleteDir' "noPerms"
|
||||
deleteDir' "outputDirNoWrite"
|
||||
deleteDir' "wrongInput"
|
||||
|
||||
|
||||
spec :: Spec
|
||||
spec = beforeAll_ (upTmpDir >> setupFiles) $ afterAll_ cleanupFiles $
|
||||
describe "HPath.IO.copyFile" $ do
|
||||
|
||||
-- successes --
|
||||
it "copyFile (Strict), everything clear" $ do
|
||||
copyFile' "inputFile"
|
||||
"outputFile"
|
||||
Strict
|
||||
removeFileIfExists "outputFile"
|
||||
|
||||
it "copyFile (Strict), and compare" $ do
|
||||
tmpDir' <- getRawTmpDir
|
||||
copyFile' "inputFile"
|
||||
"outputFile"
|
||||
Strict
|
||||
(system $ "cmp -s " ++ toString tmpDir' ++ "inputFile" ++ " "
|
||||
++ toString tmpDir' ++ "outputFile")
|
||||
`shouldReturn` ExitSuccess
|
||||
removeFileIfExists "outputFile"
|
||||
|
||||
-- posix failures --
|
||||
it "copyFile (Strict), input file does not exist" $
|
||||
copyFile' "noSuchFile"
|
||||
"outputFile"
|
||||
Strict
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == NoSuchThing)
|
||||
|
||||
it "copyFile (Strict), no permission to write to output directory" $
|
||||
copyFile' "inputFile"
|
||||
"outputDirNoWrite/outputFile"
|
||||
Strict
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == PermissionDenied)
|
||||
|
||||
it "copyFile (Strict), cannot open output directory" $
|
||||
copyFile' "inputFile"
|
||||
"noPerms/outputFile"
|
||||
Strict
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == PermissionDenied)
|
||||
|
||||
it "copyFile (Strict), cannot open source directory" $
|
||||
copyFile' "noPerms/inputFile"
|
||||
"outputFile"
|
||||
Strict
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == PermissionDenied)
|
||||
|
||||
it "copyFile (Strict), wrong input type (symlink)" $
|
||||
copyFile' "inputFileSymL"
|
||||
"outputFile"
|
||||
Strict
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == InvalidArgument)
|
||||
|
||||
it "copyFile (Strict), wrong input type (directory)" $
|
||||
copyFile' "wrongInput"
|
||||
"outputFile"
|
||||
Strict
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == InappropriateType)
|
||||
|
||||
it "copyFile (Strict), output file already exists" $
|
||||
copyFile' "inputFile"
|
||||
"alreadyExists"
|
||||
Strict
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == AlreadyExists)
|
||||
|
||||
it "copyFile (Strict), output file already exists and is a dir" $
|
||||
copyFile' "inputFile"
|
||||
"alreadyExistsD"
|
||||
Strict
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == AlreadyExists)
|
||||
|
||||
-- custom failures --
|
||||
it "copyFile (Strict), output and input are same file" $
|
||||
copyFile' "inputFile"
|
||||
"inputFile"
|
||||
Strict
|
||||
`shouldThrow`
|
||||
isSameFile
|
||||
78
test/HPath/IO/CreateDirRecursiveSpec.hs
Normal file
78
test/HPath/IO/CreateDirRecursiveSpec.hs
Normal file
@@ -0,0 +1,78 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module HPath.IO.CreateDirRecursiveSpec where
|
||||
|
||||
|
||||
import Test.Hspec
|
||||
import System.IO.Error
|
||||
(
|
||||
ioeGetErrorType
|
||||
)
|
||||
import GHC.IO.Exception
|
||||
(
|
||||
IOErrorType(..)
|
||||
)
|
||||
import Utils
|
||||
|
||||
|
||||
|
||||
upTmpDir :: IO ()
|
||||
upTmpDir = do
|
||||
setTmpDir "CreateDirRecursiveSpec"
|
||||
createTmpDir
|
||||
|
||||
setupFiles :: IO ()
|
||||
setupFiles = do
|
||||
createDir' "alreadyExists"
|
||||
createRegularFile' "alreadyExistsF"
|
||||
createDir' "noPerms"
|
||||
createDir' "noWritePerms"
|
||||
noPerms "noPerms"
|
||||
noWritableDirPerms "noWritePerms"
|
||||
|
||||
cleanupFiles :: IO ()
|
||||
cleanupFiles = do
|
||||
normalDirPerms "noPerms"
|
||||
normalDirPerms "noWritePerms"
|
||||
deleteDir' "alreadyExists"
|
||||
deleteDir' "noPerms"
|
||||
deleteDir' "noWritePerms"
|
||||
deleteFile' "alreadyExistsF"
|
||||
|
||||
|
||||
spec :: Spec
|
||||
spec = beforeAll_ (upTmpDir >> setupFiles) $ afterAll_ cleanupFiles $
|
||||
describe "HPath.IO.createDirRecursive" $ do
|
||||
|
||||
-- successes --
|
||||
it "createDirRecursive, all fine" $ do
|
||||
createDirRecursive' "newDir"
|
||||
deleteDir' "newDir"
|
||||
|
||||
it "createDirRecursive, parent directories do not exist" $ do
|
||||
createDirRecursive' "some/thing/dada"
|
||||
deleteDir' "some/thing/dada"
|
||||
deleteDir' "some/thing"
|
||||
deleteDir' "some"
|
||||
|
||||
it "createDirRecursive, destination directory already exists" $
|
||||
createDirRecursive' "alreadyExists"
|
||||
|
||||
-- posix failures --
|
||||
it "createDirRecursive, destination already exists and is a file" $
|
||||
createDirRecursive' "alreadyExistsF"
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == AlreadyExists)
|
||||
|
||||
it "createDirRecursive, can't write to output directory" $
|
||||
createDirRecursive' "noWritePerms/newDir"
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == PermissionDenied)
|
||||
|
||||
it "createDirRecursive, can't open output directory" $
|
||||
createDirRecursive' "noPerms/newDir"
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == PermissionDenied)
|
||||
|
||||
|
||||
|
||||
72
test/HPath/IO/CreateDirSpec.hs
Normal file
72
test/HPath/IO/CreateDirSpec.hs
Normal file
@@ -0,0 +1,72 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module HPath.IO.CreateDirSpec where
|
||||
|
||||
|
||||
import Test.Hspec
|
||||
import System.IO.Error
|
||||
(
|
||||
ioeGetErrorType
|
||||
)
|
||||
import GHC.IO.Exception
|
||||
(
|
||||
IOErrorType(..)
|
||||
)
|
||||
import Utils
|
||||
|
||||
|
||||
|
||||
upTmpDir :: IO ()
|
||||
upTmpDir = do
|
||||
setTmpDir "CreateDirSpec"
|
||||
createTmpDir
|
||||
|
||||
setupFiles :: IO ()
|
||||
setupFiles = do
|
||||
createDir' "alreadyExists"
|
||||
createDir' "noPerms"
|
||||
createDir' "noWritePerms"
|
||||
noPerms "noPerms"
|
||||
noWritableDirPerms "noWritePerms"
|
||||
|
||||
|
||||
|
||||
cleanupFiles :: IO ()
|
||||
cleanupFiles = do
|
||||
normalDirPerms "noPerms"
|
||||
normalDirPerms "noWritePerms"
|
||||
deleteDir' "alreadyExists"
|
||||
deleteDir' "noPerms"
|
||||
deleteDir' "noWritePerms"
|
||||
|
||||
|
||||
spec :: Spec
|
||||
spec = beforeAll_ (upTmpDir >> setupFiles) $ afterAll_ cleanupFiles $
|
||||
describe "HPath.IO.createDir" $ do
|
||||
|
||||
-- successes --
|
||||
it "createDir, all fine" $ do
|
||||
createDir' "newDir"
|
||||
removeDirIfExists "newDir"
|
||||
|
||||
-- posix failures --
|
||||
it "createDir, parent directories do not exist" $
|
||||
createDir' "some/thing/dada"
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == NoSuchThing)
|
||||
|
||||
it "createDir, can't write to output directory" $
|
||||
createDir' "noWritePerms/newDir"
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == PermissionDenied)
|
||||
|
||||
it "createDir, can't open output directory" $
|
||||
createDir' "noPerms/newDir"
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == PermissionDenied)
|
||||
|
||||
it "createDir, destination directory already exists" $
|
||||
createDir' "alreadyExists"
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == AlreadyExists)
|
||||
|
||||
70
test/HPath/IO/CreateRegularFileSpec.hs
Normal file
70
test/HPath/IO/CreateRegularFileSpec.hs
Normal file
@@ -0,0 +1,70 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module HPath.IO.CreateRegularFileSpec where
|
||||
|
||||
|
||||
import Test.Hspec
|
||||
import System.IO.Error
|
||||
(
|
||||
ioeGetErrorType
|
||||
)
|
||||
import GHC.IO.Exception
|
||||
(
|
||||
IOErrorType(..)
|
||||
)
|
||||
import Utils
|
||||
|
||||
|
||||
|
||||
upTmpDir :: IO ()
|
||||
upTmpDir = do
|
||||
setTmpDir "CreateRegularFileSpec"
|
||||
createTmpDir
|
||||
|
||||
setupFiles :: IO ()
|
||||
setupFiles = do
|
||||
createRegularFile' "alreadyExists"
|
||||
createDir' "noPerms"
|
||||
createDir' "noWritePerms"
|
||||
noPerms "noPerms"
|
||||
noWritableDirPerms "noWritePerms"
|
||||
|
||||
cleanupFiles :: IO ()
|
||||
cleanupFiles = do
|
||||
normalDirPerms "noPerms"
|
||||
normalDirPerms "noWritePerms"
|
||||
deleteFile' "alreadyExists"
|
||||
deleteDir' "noPerms"
|
||||
deleteDir' "noWritePerms"
|
||||
|
||||
|
||||
spec :: Spec
|
||||
spec = beforeAll_ (upTmpDir >> setupFiles) $ afterAll_ cleanupFiles $
|
||||
describe "HPath.IO.createRegularFile" $ do
|
||||
|
||||
-- successes --
|
||||
it "createRegularFile, all fine" $ do
|
||||
createRegularFile' "newDir"
|
||||
removeFileIfExists "newDir"
|
||||
|
||||
-- posix failures --
|
||||
it "createRegularFile, parent directories do not exist" $
|
||||
createRegularFile' "some/thing/dada"
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == NoSuchThing)
|
||||
|
||||
it "createRegularFile, can't write to destination directory" $
|
||||
createRegularFile' "noWritePerms/newDir"
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == PermissionDenied)
|
||||
|
||||
it "createRegularFile, can't write to destination directory" $
|
||||
createRegularFile' "noPerms/newDir"
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == PermissionDenied)
|
||||
|
||||
it "createRegularFile, destination file already exists" $
|
||||
createRegularFile' "alreadyExists"
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == AlreadyExists)
|
||||
|
||||
71
test/HPath/IO/CreateSymlinkSpec.hs
Normal file
71
test/HPath/IO/CreateSymlinkSpec.hs
Normal file
@@ -0,0 +1,71 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module HPath.IO.CreateSymlinkSpec where
|
||||
|
||||
|
||||
import Test.Hspec
|
||||
import System.IO.Error
|
||||
(
|
||||
ioeGetErrorType
|
||||
)
|
||||
import GHC.IO.Exception
|
||||
(
|
||||
IOErrorType(..)
|
||||
)
|
||||
import Utils
|
||||
|
||||
|
||||
upTmpDir :: IO ()
|
||||
upTmpDir = do
|
||||
setTmpDir "CreateSymlinkSpec"
|
||||
createTmpDir
|
||||
|
||||
|
||||
setupFiles :: IO ()
|
||||
setupFiles = do
|
||||
createRegularFile' "alreadyExists"
|
||||
createDir' "noPerms"
|
||||
createDir' "noWritePerms"
|
||||
noPerms "noPerms"
|
||||
noWritableDirPerms "noWritePerms"
|
||||
|
||||
|
||||
cleanupFiles :: IO ()
|
||||
cleanupFiles = do
|
||||
normalDirPerms "noPerms"
|
||||
normalDirPerms "noWritePerms"
|
||||
deleteFile' "alreadyExists"
|
||||
deleteDir' "noPerms"
|
||||
deleteDir' "noWritePerms"
|
||||
|
||||
|
||||
spec :: Spec
|
||||
spec = beforeAll_ (upTmpDir >> setupFiles) $ afterAll_ cleanupFiles $
|
||||
describe "HPath.IO.createSymlink" $ do
|
||||
|
||||
-- successes --
|
||||
it "createSymlink, all fine" $ do
|
||||
createSymlink' "newSymL" "alreadyExists/"
|
||||
removeFileIfExists "newSymL"
|
||||
|
||||
-- posix failures --
|
||||
it "createSymlink, parent directories do not exist" $
|
||||
createSymlink' "some/thing/dada" "lala"
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == NoSuchThing)
|
||||
|
||||
it "createSymlink, can't write to destination directory" $
|
||||
createSymlink' "noWritePerms/newDir" "lala"
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == PermissionDenied)
|
||||
|
||||
it "createSymlink, can't write to destination directory" $
|
||||
createSymlink' "noPerms/newDir" "lala"
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == PermissionDenied)
|
||||
|
||||
it "createSymlink, destination file already exists" $
|
||||
createSymlink' "alreadyExists" "lala"
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == AlreadyExists)
|
||||
|
||||
116
test/HPath/IO/DeleteDirRecursiveSpec.hs
Normal file
116
test/HPath/IO/DeleteDirRecursiveSpec.hs
Normal file
@@ -0,0 +1,116 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module HPath.IO.DeleteDirRecursiveSpec where
|
||||
|
||||
|
||||
import Test.Hspec
|
||||
import System.IO.Error
|
||||
(
|
||||
ioeGetErrorType
|
||||
)
|
||||
import System.Posix.Files.ByteString
|
||||
(
|
||||
getSymbolicLinkStatus
|
||||
)
|
||||
import GHC.IO.Exception
|
||||
(
|
||||
IOErrorType(..)
|
||||
)
|
||||
import Utils
|
||||
|
||||
|
||||
|
||||
upTmpDir :: IO ()
|
||||
upTmpDir = do
|
||||
setTmpDir "DeleteDirRecursiveSpec"
|
||||
createTmpDir
|
||||
|
||||
|
||||
setupFiles :: IO ()
|
||||
setupFiles = do
|
||||
createRegularFile' "file"
|
||||
createDir' "dir"
|
||||
createRegularFile' "dir/.keep"
|
||||
createSymlink' "dirSym" "dir/"
|
||||
createDir' "noPerms"
|
||||
createRegularFile' "noPerms/.keep"
|
||||
createDir' "noWritable"
|
||||
createRegularFile' "noWritable/.keep"
|
||||
|
||||
|
||||
cleanupFiles :: IO ()
|
||||
cleanupFiles = do
|
||||
deleteFile' "file"
|
||||
deleteFile' "dir/.keep"
|
||||
deleteDir' "dir"
|
||||
deleteFile' "dirSym"
|
||||
deleteFile' "noPerms/.keep"
|
||||
deleteDir' "noPerms"
|
||||
deleteFile' "noWritable/.keep"
|
||||
deleteDir' "noWritable"
|
||||
|
||||
|
||||
spec :: Spec
|
||||
spec = beforeAll_ (upTmpDir >> setupFiles) $ afterAll_ cleanupFiles $
|
||||
describe "HPath.IO.deleteDirRecursive" $ do
|
||||
|
||||
-- successes --
|
||||
it "deleteDirRecursive, empty directory, all fine" $ do
|
||||
createDir' "testDir"
|
||||
deleteDirRecursive' "testDir"
|
||||
getSymbolicLinkStatus "testDir"
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == NoSuchThing)
|
||||
|
||||
it "deleteDirRecursive, empty directory with null permissions, all fine" $ do
|
||||
createDir' "noPerms/testDir"
|
||||
noPerms "noPerms/testDir"
|
||||
deleteDirRecursive' "noPerms/testDir"
|
||||
|
||||
it "deleteDirRecursive, non-empty directory, all fine" $ do
|
||||
createDir' "nonEmpty"
|
||||
createDir' "nonEmpty/dir1"
|
||||
createDir' "nonEmpty/dir2"
|
||||
createDir' "nonEmpty/dir2/dir3"
|
||||
createRegularFile' "nonEmpty/file1"
|
||||
createRegularFile' "nonEmpty/dir1/file2"
|
||||
deleteDirRecursive' "nonEmpty"
|
||||
getSymbolicLinkStatus "nonEmpty"
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == NoSuchThing)
|
||||
|
||||
-- posix failures --
|
||||
it "deleteDirRecursive, can't open parent directory" $ do
|
||||
createDir' "noPerms/foo"
|
||||
noPerms "noPerms"
|
||||
(deleteDirRecursive' "noPerms/foo")
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == PermissionDenied)
|
||||
normalDirPerms "noPerms"
|
||||
deleteDir' "noPerms/foo"
|
||||
|
||||
it "deleteDirRecursive, can't write to parent directory" $ do
|
||||
createDir' "noWritable/foo"
|
||||
noWritableDirPerms "noWritable"
|
||||
(deleteDirRecursive' "noWritable/foo")
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == PermissionDenied)
|
||||
normalDirPerms "noWritable"
|
||||
deleteDir' "noWritable/foo"
|
||||
|
||||
it "deleteDirRecursive, wrong file type (symlink to directory)" $
|
||||
deleteDirRecursive' "dirSym"
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == InappropriateType)
|
||||
|
||||
it "deleteDirRecursive, wrong file type (regular file)" $
|
||||
deleteDirRecursive' "file"
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == InappropriateType)
|
||||
|
||||
it "deleteDirRecursive, directory does not exist" $
|
||||
deleteDirRecursive' "doesNotExist"
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == NoSuchThing)
|
||||
|
||||
|
||||
114
test/HPath/IO/DeleteDirSpec.hs
Normal file
114
test/HPath/IO/DeleteDirSpec.hs
Normal file
@@ -0,0 +1,114 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module HPath.IO.DeleteDirSpec where
|
||||
|
||||
|
||||
import Test.Hspec
|
||||
import System.IO.Error
|
||||
(
|
||||
ioeGetErrorType
|
||||
)
|
||||
import System.Posix.Files.ByteString
|
||||
(
|
||||
getSymbolicLinkStatus
|
||||
)
|
||||
import GHC.IO.Exception
|
||||
(
|
||||
IOErrorType(..)
|
||||
)
|
||||
import Utils
|
||||
|
||||
|
||||
|
||||
|
||||
upTmpDir :: IO ()
|
||||
upTmpDir = do
|
||||
setTmpDir "DeleteDirSpec"
|
||||
createTmpDir
|
||||
|
||||
|
||||
setupFiles :: IO ()
|
||||
setupFiles = do
|
||||
createRegularFile' "file"
|
||||
createDir' "dir"
|
||||
createRegularFile' "dir/.keep"
|
||||
createSymlink' "dirSym" "dir/"
|
||||
createDir' "noPerms"
|
||||
createRegularFile' "noPerms/.keep"
|
||||
createDir' "noWritable"
|
||||
createRegularFile' "noWritable/.keep"
|
||||
|
||||
|
||||
cleanupFiles :: IO ()
|
||||
cleanupFiles = do
|
||||
deleteFile' "file"
|
||||
deleteFile' "dir/.keep"
|
||||
deleteDir' "dir"
|
||||
deleteFile' "dirSym"
|
||||
deleteFile' "noPerms/.keep"
|
||||
deleteDir' "noPerms"
|
||||
deleteFile' "noWritable/.keep"
|
||||
deleteDir' "noWritable"
|
||||
|
||||
|
||||
spec :: Spec
|
||||
spec = beforeAll_ (upTmpDir >> setupFiles) $ afterAll_ cleanupFiles $
|
||||
describe "HPath.IO.deleteDir" $ do
|
||||
|
||||
-- successes --
|
||||
it "deleteDir, empty directory, all fine" $ do
|
||||
createDir' "testDir"
|
||||
deleteDir' "testDir"
|
||||
getSymbolicLinkStatus "testDir"
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == NoSuchThing)
|
||||
|
||||
it "deleteDir, directory with null permissions, all fine" $ do
|
||||
createDir' "noPerms/testDir"
|
||||
noPerms "noPerms/testDir"
|
||||
deleteDir' "noPerms/testDir"
|
||||
getSymbolicLinkStatus "testDir"
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == NoSuchThing)
|
||||
|
||||
-- posix failures --
|
||||
it "deleteDir, wrong file type (symlink to directory)" $
|
||||
deleteDir' "dirSym"
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == InappropriateType)
|
||||
|
||||
it "deleteDir, wrong file type (regular file)" $
|
||||
deleteDir' "file"
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == InappropriateType)
|
||||
|
||||
it "deleteDir, directory does not exist" $
|
||||
deleteDir' "doesNotExist"
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == NoSuchThing)
|
||||
|
||||
it "deleteDir, directory not empty" $
|
||||
deleteDir' "dir"
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == UnsatisfiedConstraints)
|
||||
|
||||
it "deleteDir, can't open parent directory" $ do
|
||||
createDir' "noPerms/foo"
|
||||
noPerms "noPerms"
|
||||
(deleteDir' "noPerms/foo")
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == PermissionDenied)
|
||||
normalDirPerms "noPerms"
|
||||
deleteDir' "noPerms/foo"
|
||||
|
||||
it "deleteDir, can't write to parent directory, still fine" $ do
|
||||
createDir' "noWritable/foo"
|
||||
noWritableDirPerms "noWritable"
|
||||
(deleteDir' "noWritable/foo")
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == PermissionDenied)
|
||||
normalDirPerms "noWritable"
|
||||
deleteDir' "noWritable/foo"
|
||||
|
||||
|
||||
|
||||
84
test/HPath/IO/DeleteFileSpec.hs
Normal file
84
test/HPath/IO/DeleteFileSpec.hs
Normal file
@@ -0,0 +1,84 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module HPath.IO.DeleteFileSpec where
|
||||
|
||||
|
||||
import Test.Hspec
|
||||
import HPath.IO
|
||||
import System.IO.Error
|
||||
(
|
||||
ioeGetErrorType
|
||||
)
|
||||
import System.Posix.Files.ByteString
|
||||
(
|
||||
getSymbolicLinkStatus
|
||||
)
|
||||
import GHC.IO.Exception
|
||||
(
|
||||
IOErrorType(..)
|
||||
)
|
||||
import Utils
|
||||
|
||||
|
||||
upTmpDir :: IO ()
|
||||
upTmpDir = do
|
||||
setTmpDir "DeleteFileSpec"
|
||||
createTmpDir
|
||||
|
||||
|
||||
setupFiles :: IO ()
|
||||
setupFiles = do
|
||||
createRegularFile' "foo"
|
||||
createSymlink' "syml" "foo"
|
||||
createDir' "dir"
|
||||
createDir' "noPerms"
|
||||
noPerms "noPerms"
|
||||
|
||||
|
||||
cleanupFiles :: IO ()
|
||||
cleanupFiles = do
|
||||
normalDirPerms "noPerms"
|
||||
deleteFile' "foo"
|
||||
deleteFile' "syml"
|
||||
deleteDir' "dir"
|
||||
deleteDir' "noPerms"
|
||||
|
||||
|
||||
|
||||
spec :: Spec
|
||||
spec = beforeAll_ (upTmpDir >> setupFiles) $ afterAll_ cleanupFiles $
|
||||
describe "HPath.IO.deleteFile" $ do
|
||||
|
||||
-- successes --
|
||||
it "deleteFile, regular file, all fine" $ do
|
||||
createRegularFile' "testFile"
|
||||
deleteFile' "testFile"
|
||||
getSymbolicLinkStatus "testFile"
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == NoSuchThing)
|
||||
|
||||
it "deleteFile, symlink, all fine" $ do
|
||||
recreateSymlink' "syml"
|
||||
"testFile"
|
||||
Strict
|
||||
deleteFile' "testFile"
|
||||
getSymbolicLinkStatus "testFile"
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == NoSuchThing)
|
||||
|
||||
-- posix failures --
|
||||
it "deleteFile, wrong file type (directory)" $
|
||||
deleteFile' "dir"
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == InappropriateType)
|
||||
|
||||
it "deleteFile, file does not exist" $
|
||||
deleteFile' "doesNotExist"
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == NoSuchThing)
|
||||
|
||||
it "deleteFile, can't read directory" $
|
||||
deleteFile' "noPerms/blah"
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == PermissionDenied)
|
||||
|
||||
100
test/HPath/IO/GetDirsFilesSpec.hs
Normal file
100
test/HPath/IO/GetDirsFilesSpec.hs
Normal file
@@ -0,0 +1,100 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module HPath.IO.GetDirsFilesSpec where
|
||||
|
||||
|
||||
import Data.List
|
||||
(
|
||||
sort
|
||||
)
|
||||
import qualified HPath as P
|
||||
import HPath.IO
|
||||
import Test.Hspec
|
||||
import System.IO.Error
|
||||
(
|
||||
ioeGetErrorType
|
||||
)
|
||||
import GHC.IO.Exception
|
||||
(
|
||||
IOErrorType(..)
|
||||
)
|
||||
import Utils
|
||||
|
||||
|
||||
upTmpDir :: IO ()
|
||||
upTmpDir = do
|
||||
setTmpDir "GetDirsFilesSpec"
|
||||
createTmpDir
|
||||
|
||||
|
||||
setupFiles :: IO ()
|
||||
setupFiles = do
|
||||
createRegularFile' "file"
|
||||
createRegularFile' "Lala"
|
||||
createRegularFile' ".hidden"
|
||||
createSymlink' "syml" "Lala"
|
||||
createDir' "dir"
|
||||
createSymlink' "dirsym" "dir"
|
||||
createDir' "noPerms"
|
||||
noPerms "noPerms"
|
||||
|
||||
|
||||
cleanupFiles :: IO ()
|
||||
cleanupFiles = do
|
||||
normalDirPerms "noPerms"
|
||||
deleteFile' "file"
|
||||
deleteFile' "Lala"
|
||||
deleteFile' ".hidden"
|
||||
deleteFile' "syml"
|
||||
deleteDir' "dir"
|
||||
deleteFile' "dirsym"
|
||||
deleteDir' "noPerms"
|
||||
|
||||
|
||||
|
||||
spec :: Spec
|
||||
spec = beforeAll_ (upTmpDir >> setupFiles) $ afterAll_ cleanupFiles $
|
||||
describe "HPath.IO.getDirsFiles" $ do
|
||||
|
||||
-- successes --
|
||||
it "getDirsFiles, all fine" $
|
||||
withRawTmpDir $ \p -> do
|
||||
expectedFiles <- mapM P.parseRel [".hidden"
|
||||
,"Lala"
|
||||
,"dir"
|
||||
,"dirsym"
|
||||
,"file"
|
||||
,"noPerms"
|
||||
,"syml"]
|
||||
(fmap sort $ getDirsFiles p)
|
||||
`shouldReturn` fmap (p P.</>) expectedFiles
|
||||
|
||||
-- posix failures --
|
||||
it "getDirsFiles, nonexistent directory" $
|
||||
getDirsFiles' "nothingHere"
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == NoSuchThing)
|
||||
|
||||
it "getDirsFiles, wrong file type (file)" $
|
||||
getDirsFiles' "file"
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == InappropriateType)
|
||||
|
||||
it "getDirsFiles, wrong file type (symlink to file)" $
|
||||
getDirsFiles' "syml"
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == InvalidArgument)
|
||||
|
||||
it "getDirsFiles, wrong file type (symlink to dir)" $
|
||||
getDirsFiles' "dirsym"
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == InvalidArgument)
|
||||
|
||||
it "getDirsFiles, can't open directory" $
|
||||
getDirsFiles' "noPerms"
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == PermissionDenied)
|
||||
|
||||
|
||||
|
||||
|
||||
88
test/HPath/IO/GetFileTypeSpec.hs
Normal file
88
test/HPath/IO/GetFileTypeSpec.hs
Normal file
@@ -0,0 +1,88 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module HPath.IO.GetFileTypeSpec where
|
||||
|
||||
|
||||
import HPath.IO
|
||||
import Test.Hspec
|
||||
import System.IO.Error
|
||||
(
|
||||
ioeGetErrorType
|
||||
)
|
||||
import GHC.IO.Exception
|
||||
(
|
||||
IOErrorType(..)
|
||||
)
|
||||
import Utils
|
||||
|
||||
|
||||
|
||||
upTmpDir :: IO ()
|
||||
upTmpDir = do
|
||||
setTmpDir "GetFileTypeSpec"
|
||||
createTmpDir
|
||||
|
||||
|
||||
setupFiles :: IO ()
|
||||
setupFiles = do
|
||||
createRegularFile' "regularfile"
|
||||
createSymlink' "symlink" "regularfile"
|
||||
createSymlink' "brokenSymlink" "broken"
|
||||
createDir' "directory"
|
||||
createSymlink' "symlinkD" "directory"
|
||||
createDir' "noPerms"
|
||||
noPerms "noPerms"
|
||||
|
||||
|
||||
cleanupFiles :: IO ()
|
||||
cleanupFiles = do
|
||||
normalDirPerms "noPerms"
|
||||
deleteFile' "regularfile"
|
||||
deleteFile' "symlink"
|
||||
deleteFile' "brokenSymlink"
|
||||
deleteDir' "directory"
|
||||
deleteFile' "symlinkD"
|
||||
deleteDir' "noPerms"
|
||||
|
||||
|
||||
|
||||
spec :: Spec
|
||||
spec = beforeAll_ (upTmpDir >> setupFiles) $ afterAll_ cleanupFiles $
|
||||
describe "HPath.IO.getFileType" $ do
|
||||
|
||||
-- successes --
|
||||
it "getFileType, regular file" $
|
||||
getFileType' "regularfile"
|
||||
`shouldReturn` RegularFile
|
||||
|
||||
it "getFileType, directory" $
|
||||
getFileType' "directory"
|
||||
`shouldReturn` Directory
|
||||
|
||||
it "getFileType, directory with null permissions" $
|
||||
getFileType' "noPerms"
|
||||
`shouldReturn` Directory
|
||||
|
||||
it "getFileType, symlink to file" $
|
||||
getFileType' "symlink"
|
||||
`shouldReturn` SymbolicLink
|
||||
|
||||
it "getFileType, symlink to directory" $
|
||||
getFileType' "symlinkD"
|
||||
`shouldReturn` SymbolicLink
|
||||
|
||||
it "getFileType, broken symlink" $
|
||||
getFileType' "brokenSymlink"
|
||||
`shouldReturn` SymbolicLink
|
||||
|
||||
-- posix failures --
|
||||
it "getFileType, file does not exist" $
|
||||
getFileType' "nothingHere"
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == NoSuchThing)
|
||||
|
||||
it "getFileType, can't open directory" $
|
||||
getFileType' "noPerms/forz"
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == PermissionDenied)
|
||||
|
||||
126
test/HPath/IO/MoveFileOverwriteSpec.hs
Normal file
126
test/HPath/IO/MoveFileOverwriteSpec.hs
Normal file
@@ -0,0 +1,126 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module HPath.IO.MoveFileOverwriteSpec where
|
||||
|
||||
|
||||
import Test.Hspec
|
||||
import HPath.IO
|
||||
import HPath.IO.Errors
|
||||
import System.IO.Error
|
||||
(
|
||||
ioeGetErrorType
|
||||
)
|
||||
import GHC.IO.Exception
|
||||
(
|
||||
IOErrorType(..)
|
||||
)
|
||||
import Utils
|
||||
|
||||
|
||||
|
||||
upTmpDir :: IO ()
|
||||
upTmpDir = do
|
||||
setTmpDir "MoveFileOverwriteSpec"
|
||||
createTmpDir
|
||||
|
||||
|
||||
setupFiles :: IO ()
|
||||
setupFiles = do
|
||||
createRegularFile' "myFile"
|
||||
createSymlink' "myFileL" "myFile"
|
||||
createDir' "alreadyExistsD"
|
||||
createDir' "dir"
|
||||
createDir' "noPerms"
|
||||
createDir' "noWritePerm"
|
||||
noPerms "noPerms"
|
||||
noWritableDirPerms "noWritePerm"
|
||||
writeFile' "myFile" "Blahfaselgagaga"
|
||||
|
||||
|
||||
cleanupFiles :: IO ()
|
||||
cleanupFiles = do
|
||||
normalDirPerms "noPerms"
|
||||
normalDirPerms "noWritePerm"
|
||||
deleteFile' "myFile"
|
||||
deleteFile' "myFileL"
|
||||
deleteDir' "alreadyExistsD"
|
||||
deleteDir' "dir"
|
||||
deleteDir' "noPerms"
|
||||
deleteDir' "noWritePerm"
|
||||
|
||||
|
||||
|
||||
spec :: Spec
|
||||
spec = beforeAll_ (upTmpDir >> setupFiles) $ afterAll_ cleanupFiles $
|
||||
describe "HPath.IO.moveFile" $ do
|
||||
|
||||
-- successes --
|
||||
it "moveFile (Overwrite), all fine" $
|
||||
moveFile' "myFile"
|
||||
"movedFile"
|
||||
Overwrite
|
||||
|
||||
it "moveFile (Overwrite), all fine" $
|
||||
moveFile' "myFile"
|
||||
"dir/movedFile"
|
||||
Overwrite
|
||||
|
||||
it "moveFile (Overwrite), all fine on symlink" $
|
||||
moveFile' "myFileL"
|
||||
"movedFile"
|
||||
Overwrite
|
||||
|
||||
it "moveFile (Overwrite), all fine on directory" $
|
||||
moveFile' "dir"
|
||||
"movedFile"
|
||||
Overwrite
|
||||
|
||||
it "moveFile (Overwrite), destination file already exists" $
|
||||
moveFile' "myFile"
|
||||
"alreadyExists"
|
||||
Overwrite
|
||||
|
||||
-- posix failures --
|
||||
it "moveFile (Overwrite), source file does not exist" $
|
||||
moveFile' "fileDoesNotExist"
|
||||
"movedFile"
|
||||
Overwrite
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == NoSuchThing)
|
||||
|
||||
it "moveFile (Overwrite), can't write to destination directory" $
|
||||
moveFile' "myFile"
|
||||
"noWritePerm/movedFile"
|
||||
Overwrite
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == PermissionDenied)
|
||||
|
||||
it "moveFile (Overwrite), can't open destination directory" $
|
||||
moveFile' "myFile"
|
||||
"noPerms/movedFile"
|
||||
Overwrite
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == PermissionDenied)
|
||||
|
||||
it "moveFile (Overwrite), can't open source directory" $
|
||||
moveFile' "noPerms/myFile"
|
||||
"movedFile"
|
||||
Overwrite
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == PermissionDenied)
|
||||
|
||||
-- custom failures --
|
||||
|
||||
it "moveFile (Overwrite), move from file to dir" $
|
||||
moveFile' "myFile"
|
||||
"alreadyExistsD"
|
||||
Overwrite
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == AlreadyExists)
|
||||
|
||||
it "moveFile (Overwrite), source and dest are same file" $
|
||||
moveFile' "myFile"
|
||||
"myFile"
|
||||
Overwrite
|
||||
`shouldThrow`
|
||||
isSameFile
|
||||
129
test/HPath/IO/MoveFileSpec.hs
Normal file
129
test/HPath/IO/MoveFileSpec.hs
Normal file
@@ -0,0 +1,129 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module HPath.IO.MoveFileSpec where
|
||||
|
||||
|
||||
import Test.Hspec
|
||||
import HPath.IO
|
||||
import HPath.IO.Errors
|
||||
import System.IO.Error
|
||||
(
|
||||
ioeGetErrorType
|
||||
)
|
||||
import GHC.IO.Exception
|
||||
(
|
||||
IOErrorType(..)
|
||||
)
|
||||
import Utils
|
||||
|
||||
|
||||
|
||||
upTmpDir :: IO ()
|
||||
upTmpDir = do
|
||||
setTmpDir "MoveFileSpec"
|
||||
createTmpDir
|
||||
|
||||
|
||||
setupFiles :: IO ()
|
||||
setupFiles = do
|
||||
createRegularFile' "myFile"
|
||||
createSymlink' "myFileL" "myFile"
|
||||
createRegularFile' "alreadyExists"
|
||||
createDir' "alreadyExistsD"
|
||||
createDir' "dir"
|
||||
createDir' "noPerms"
|
||||
createDir' "noWritePerm"
|
||||
noPerms "noPerms"
|
||||
noWritableDirPerms "noWritePerm"
|
||||
writeFile' "myFile" "Blahfaselgagaga"
|
||||
|
||||
|
||||
cleanupFiles :: IO ()
|
||||
cleanupFiles = do
|
||||
normalDirPerms "noPerms"
|
||||
normalDirPerms "noWritePerm"
|
||||
deleteFile' "myFile"
|
||||
deleteFile' "myFileL"
|
||||
deleteFile' "alreadyExists"
|
||||
deleteDir' "alreadyExistsD"
|
||||
deleteDir' "dir"
|
||||
deleteDir' "noPerms"
|
||||
deleteDir' "noWritePerm"
|
||||
|
||||
|
||||
|
||||
spec :: Spec
|
||||
spec = beforeAll_ (upTmpDir >> setupFiles) $ afterAll_ cleanupFiles $
|
||||
describe "HPath.IO.moveFile" $ do
|
||||
|
||||
-- successes --
|
||||
it "moveFile (Strict), all fine" $
|
||||
moveFile' "myFile"
|
||||
"movedFile"
|
||||
Strict
|
||||
|
||||
it "moveFile (Strict), all fine" $
|
||||
moveFile' "myFile"
|
||||
"dir/movedFile"
|
||||
Strict
|
||||
|
||||
it "moveFile (Strict), all fine on symlink" $
|
||||
moveFile' "myFileL"
|
||||
"movedFile"
|
||||
Strict
|
||||
|
||||
it "moveFile (Strict), all fine on directory" $
|
||||
moveFile' "dir"
|
||||
"movedFile"
|
||||
Strict
|
||||
|
||||
-- posix failures --
|
||||
it "moveFile (Strict), source file does not exist" $
|
||||
moveFile' "fileDoesNotExist"
|
||||
"movedFile"
|
||||
Strict
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == NoSuchThing)
|
||||
|
||||
it "moveFile (Strict), can't write to destination directory" $
|
||||
moveFile' "myFile"
|
||||
"noWritePerm/movedFile"
|
||||
Strict
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == PermissionDenied)
|
||||
|
||||
it "moveFile (Strict), can't open destination directory" $
|
||||
moveFile' "myFile"
|
||||
"noPerms/movedFile"
|
||||
Strict
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == PermissionDenied)
|
||||
|
||||
it "moveFile (Strict), can't open source directory" $
|
||||
moveFile' "noPerms/myFile"
|
||||
"movedFile"
|
||||
Strict
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == PermissionDenied)
|
||||
|
||||
-- custom failures --
|
||||
it "moveFile (Strict), destination file already exists" $
|
||||
moveFile' "myFile"
|
||||
"alreadyExists"
|
||||
Strict
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == AlreadyExists)
|
||||
|
||||
it "moveFile (Strict), move from file to dir" $
|
||||
moveFile' "myFile"
|
||||
"alreadyExistsD"
|
||||
Strict
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == AlreadyExists)
|
||||
|
||||
it "moveFile (Strict), source and dest are same file" $
|
||||
moveFile' "myFile"
|
||||
"myFile"
|
||||
Strict
|
||||
`shouldThrow`
|
||||
isSameFile
|
||||
85
test/HPath/IO/ReadFileEOFSpec.hs
Normal file
85
test/HPath/IO/ReadFileEOFSpec.hs
Normal file
@@ -0,0 +1,85 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
|
||||
module HPath.IO.ReadFileEOFSpec where
|
||||
|
||||
|
||||
import Test.Hspec
|
||||
import System.IO.Error
|
||||
(
|
||||
ioeGetErrorType
|
||||
)
|
||||
import GHC.IO.Exception
|
||||
(
|
||||
IOErrorType(..)
|
||||
)
|
||||
import Utils
|
||||
|
||||
|
||||
|
||||
upTmpDir :: IO ()
|
||||
upTmpDir = do
|
||||
setTmpDir "ReadFileEOFSpec"
|
||||
createTmpDir
|
||||
|
||||
setupFiles :: IO ()
|
||||
setupFiles = do
|
||||
createRegularFile' "fileWithContent"
|
||||
createRegularFile' "fileWithoutContent"
|
||||
createSymlink' "inputFileSymL" "fileWithContent"
|
||||
createDir' "alreadyExistsD"
|
||||
createRegularFile' "noPerms"
|
||||
noPerms "noPerms"
|
||||
createDir' "noPermsD"
|
||||
createRegularFile' "noPermsD/inputFile"
|
||||
noPerms "noPermsD"
|
||||
writeFile' "fileWithContent" "Blahfaselgagaga"
|
||||
|
||||
|
||||
cleanupFiles :: IO ()
|
||||
cleanupFiles = do
|
||||
deleteFile' "fileWithContent"
|
||||
deleteFile' "fileWithoutContent"
|
||||
deleteFile' "inputFileSymL"
|
||||
deleteDir' "alreadyExistsD"
|
||||
normalFilePerms "noPerms"
|
||||
deleteFile' "noPerms"
|
||||
normalDirPerms "noPermsD"
|
||||
deleteFile' "noPermsD/inputFile"
|
||||
deleteDir' "noPermsD"
|
||||
|
||||
|
||||
spec :: Spec
|
||||
spec = beforeAll_ (upTmpDir >> setupFiles) $ afterAll_ cleanupFiles $
|
||||
describe "HPath.IO.readFileEOF" $ do
|
||||
|
||||
-- successes --
|
||||
it "readFileEOF (Strict) file with content, everything clear" $ do
|
||||
out <- readFileEOF' "fileWithContent"
|
||||
out `shouldBe` "Blahfaselgagaga"
|
||||
|
||||
it "readFileEOF (Strict) symlink, everything clear" $ do
|
||||
out <- readFileEOF' "inputFileSymL"
|
||||
out `shouldBe` "Blahfaselgagaga"
|
||||
|
||||
it "readFileEOF (Strict) empty file, everything clear" $ do
|
||||
out <- readFileEOF' "fileWithoutContent"
|
||||
out `shouldBe` ""
|
||||
|
||||
|
||||
-- posix failures --
|
||||
it "readFileEOF (Strict) directory, wrong file type" $ do
|
||||
readFileEOF' "alreadyExistsD"
|
||||
`shouldThrow` (\e -> ioeGetErrorType e == InappropriateType)
|
||||
|
||||
it "readFileEOF (Strict) file, no permissions" $ do
|
||||
readFileEOF' "noPerms"
|
||||
`shouldThrow` (\e -> ioeGetErrorType e == PermissionDenied)
|
||||
|
||||
it "readFileEOF (Strict) file, no permissions on dir" $ do
|
||||
readFileEOF' "noPermsD/inputFile"
|
||||
`shouldThrow` (\e -> ioeGetErrorType e == PermissionDenied)
|
||||
|
||||
it "readFileEOF (Strict) file, no such file" $ do
|
||||
readFileEOF' "lalala"
|
||||
`shouldThrow` (\e -> ioeGetErrorType e == NoSuchThing)
|
||||
85
test/HPath/IO/ReadFileSpec.hs
Normal file
85
test/HPath/IO/ReadFileSpec.hs
Normal file
@@ -0,0 +1,85 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
|
||||
module HPath.IO.ReadFileSpec where
|
||||
|
||||
|
||||
import Test.Hspec
|
||||
import System.IO.Error
|
||||
(
|
||||
ioeGetErrorType
|
||||
)
|
||||
import GHC.IO.Exception
|
||||
(
|
||||
IOErrorType(..)
|
||||
)
|
||||
import Utils
|
||||
|
||||
|
||||
|
||||
upTmpDir :: IO ()
|
||||
upTmpDir = do
|
||||
setTmpDir "ReadFileSpec"
|
||||
createTmpDir
|
||||
|
||||
setupFiles :: IO ()
|
||||
setupFiles = do
|
||||
createRegularFile' "fileWithContent"
|
||||
createRegularFile' "fileWithoutContent"
|
||||
createSymlink' "inputFileSymL" "fileWithContent"
|
||||
createDir' "alreadyExistsD"
|
||||
createRegularFile' "noPerms"
|
||||
noPerms "noPerms"
|
||||
createDir' "noPermsD"
|
||||
createRegularFile' "noPermsD/inputFile"
|
||||
noPerms "noPermsD"
|
||||
writeFile' "fileWithContent" "Blahfaselgagaga"
|
||||
|
||||
|
||||
cleanupFiles :: IO ()
|
||||
cleanupFiles = do
|
||||
deleteFile' "fileWithContent"
|
||||
deleteFile' "fileWithoutContent"
|
||||
deleteFile' "inputFileSymL"
|
||||
deleteDir' "alreadyExistsD"
|
||||
normalFilePerms "noPerms"
|
||||
deleteFile' "noPerms"
|
||||
normalDirPerms "noPermsD"
|
||||
deleteFile' "noPermsD/inputFile"
|
||||
deleteDir' "noPermsD"
|
||||
|
||||
|
||||
spec :: Spec
|
||||
spec = beforeAll_ (upTmpDir >> setupFiles) $ afterAll_ cleanupFiles $
|
||||
describe "HPath.IO.readFile" $ do
|
||||
|
||||
-- successes --
|
||||
it "readFile (Strict) file with content, everything clear" $ do
|
||||
out <- readFile' "fileWithContent"
|
||||
out `shouldBe` "Blahfaselgagaga"
|
||||
|
||||
it "readFile (Strict) symlink, everything clear" $ do
|
||||
out <- readFile' "inputFileSymL"
|
||||
out `shouldBe` "Blahfaselgagaga"
|
||||
|
||||
it "readFile (Strict) empty file, everything clear" $ do
|
||||
out <- readFile' "fileWithoutContent"
|
||||
out `shouldBe` ""
|
||||
|
||||
|
||||
-- posix failures --
|
||||
it "readFile (Strict) directory, wrong file type" $ do
|
||||
readFile' "alreadyExistsD"
|
||||
`shouldThrow` (\e -> ioeGetErrorType e == InappropriateType)
|
||||
|
||||
it "readFile (Strict) file, no permissions" $ do
|
||||
readFile' "noPerms"
|
||||
`shouldThrow` (\e -> ioeGetErrorType e == PermissionDenied)
|
||||
|
||||
it "readFile (Strict) file, no permissions on dir" $ do
|
||||
readFile' "noPermsD/inputFile"
|
||||
`shouldThrow` (\e -> ioeGetErrorType e == PermissionDenied)
|
||||
|
||||
it "readFile (Strict) file, no such file" $ do
|
||||
readFile' "lalala"
|
||||
`shouldThrow` (\e -> ioeGetErrorType e == NoSuchThing)
|
||||
139
test/HPath/IO/RecreateSymlinkOverwriteSpec.hs
Normal file
139
test/HPath/IO/RecreateSymlinkOverwriteSpec.hs
Normal file
@@ -0,0 +1,139 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module HPath.IO.RecreateSymlinkOverwriteSpec where
|
||||
|
||||
|
||||
-- TODO: exception if destination exists but is not a file + `OverWrite` CopyMode
|
||||
|
||||
|
||||
import Test.Hspec
|
||||
import HPath.IO
|
||||
import HPath.IO.Errors
|
||||
import System.IO.Error
|
||||
(
|
||||
ioeGetErrorType
|
||||
)
|
||||
import GHC.IO.Exception
|
||||
(
|
||||
IOErrorType(..)
|
||||
)
|
||||
import Utils
|
||||
|
||||
|
||||
upTmpDir :: IO ()
|
||||
upTmpDir = do
|
||||
setTmpDir "RecreateSymlinkOverwriteSpec"
|
||||
createTmpDir
|
||||
|
||||
|
||||
setupFiles :: IO ()
|
||||
setupFiles = do
|
||||
createRegularFile' "myFile"
|
||||
createSymlink' "myFileL" "myFile"
|
||||
createRegularFile' "alreadyExists"
|
||||
createDir' "alreadyExistsD"
|
||||
createDir' "dir"
|
||||
createDir' "noPerms"
|
||||
createDir' "noWritePerm"
|
||||
createDir' "alreadyExistsD2"
|
||||
createRegularFile' "alreadyExistsD2/lala"
|
||||
noPerms "noPerms"
|
||||
noWritableDirPerms "noWritePerm"
|
||||
writeFile' "myFile" "Blahfaselgagaga"
|
||||
|
||||
|
||||
cleanupFiles :: IO ()
|
||||
cleanupFiles = do
|
||||
normalDirPerms "noPerms"
|
||||
normalDirPerms "noWritePerm"
|
||||
deleteFile' "myFile"
|
||||
deleteFile' "myFileL"
|
||||
deleteFile' "alreadyExists"
|
||||
deleteFile' "alreadyExistsD2/lala"
|
||||
deleteDir' "alreadyExistsD"
|
||||
deleteDir' "alreadyExistsD2"
|
||||
deleteDir' "dir"
|
||||
deleteDir' "noPerms"
|
||||
deleteDir' "noWritePerm"
|
||||
|
||||
|
||||
spec :: Spec
|
||||
spec = beforeAll_ (upTmpDir >> setupFiles) $ afterAll_ cleanupFiles $
|
||||
describe "HPath.IO.recreateSymlink" $ do
|
||||
|
||||
-- successes --
|
||||
it "recreateSymLink (Overwrite), all fine" $ do
|
||||
recreateSymlink' "myFileL"
|
||||
"movedFile"
|
||||
Overwrite
|
||||
removeFileIfExists "movedFile"
|
||||
|
||||
it "recreateSymLink (Overwrite), all fine" $ do
|
||||
recreateSymlink' "myFileL"
|
||||
"dir/movedFile"
|
||||
Overwrite
|
||||
removeFileIfExists "dir/movedFile"
|
||||
|
||||
it "recreateSymLink (Overwrite), destination file already exists" $
|
||||
recreateSymlink' "myFileL"
|
||||
"alreadyExists"
|
||||
Overwrite
|
||||
|
||||
it "recreateSymLink (Overwrite), destination already exists and is an empty dir" $ do
|
||||
recreateSymlink' "myFileL"
|
||||
"alreadyExistsD"
|
||||
Overwrite
|
||||
deleteFile' "alreadyExistsD"
|
||||
createDir' "alreadyExistsD"
|
||||
|
||||
-- posix failures --
|
||||
it "recreateSymLink (Overwrite), destination already exists and is a non-empty dir" $
|
||||
recreateSymlink' "myFileL"
|
||||
"alreadyExistsD2"
|
||||
Overwrite
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == UnsatisfiedConstraints)
|
||||
|
||||
it "recreateSymLink (Overwrite), wrong input type (file)" $
|
||||
recreateSymlink' "myFile"
|
||||
"movedFile"
|
||||
Overwrite
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == InvalidArgument)
|
||||
|
||||
it "recreateSymLink (Overwrite), wrong input type (directory)" $
|
||||
recreateSymlink' "dir"
|
||||
"movedFile"
|
||||
Overwrite
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == InvalidArgument)
|
||||
|
||||
it "recreateSymLink (Overwrite), can't write to destination directory" $
|
||||
recreateSymlink' "myFileL"
|
||||
"noWritePerm/movedFile"
|
||||
Overwrite
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == PermissionDenied)
|
||||
|
||||
it "recreateSymLink (Overwrite), can't open destination directory" $
|
||||
recreateSymlink' "myFileL"
|
||||
"noPerms/movedFile"
|
||||
Overwrite
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == PermissionDenied)
|
||||
|
||||
it "recreateSymLink (Overwrite), can't open source directory" $
|
||||
recreateSymlink' "noPerms/myFileL"
|
||||
"movedFile"
|
||||
Overwrite
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == PermissionDenied)
|
||||
|
||||
-- custom failures --
|
||||
it "recreateSymLink (Overwrite), source and destination are the same file" $
|
||||
recreateSymlink' "myFileL"
|
||||
"myFileL"
|
||||
Overwrite
|
||||
`shouldThrow`
|
||||
isSameFile
|
||||
|
||||
130
test/HPath/IO/RecreateSymlinkSpec.hs
Normal file
130
test/HPath/IO/RecreateSymlinkSpec.hs
Normal file
@@ -0,0 +1,130 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module HPath.IO.RecreateSymlinkSpec where
|
||||
|
||||
|
||||
|
||||
|
||||
import Test.Hspec
|
||||
import HPath.IO
|
||||
import HPath.IO.Errors
|
||||
import System.IO.Error
|
||||
(
|
||||
ioeGetErrorType
|
||||
)
|
||||
import GHC.IO.Exception
|
||||
(
|
||||
IOErrorType(..)
|
||||
)
|
||||
import Utils
|
||||
|
||||
|
||||
|
||||
upTmpDir :: IO ()
|
||||
upTmpDir = do
|
||||
setTmpDir "RecreateSymlinkSpec"
|
||||
createTmpDir
|
||||
|
||||
|
||||
setupFiles :: IO ()
|
||||
setupFiles = do
|
||||
createRegularFile' "myFile"
|
||||
createSymlink' "myFileL" "myFile"
|
||||
createRegularFile' "alreadyExists"
|
||||
createDir' "alreadyExistsD"
|
||||
createDir' "dir"
|
||||
createDir' "noPerms"
|
||||
createDir' "noWritePerm"
|
||||
noPerms "noPerms"
|
||||
noWritableDirPerms "noWritePerm"
|
||||
writeFile' "myFile" "Blahfaselgagaga"
|
||||
|
||||
|
||||
cleanupFiles :: IO ()
|
||||
cleanupFiles = do
|
||||
normalDirPerms "noPerms"
|
||||
normalDirPerms "noWritePerm"
|
||||
deleteFile' "myFile"
|
||||
deleteFile' "myFileL"
|
||||
deleteFile' "alreadyExists"
|
||||
deleteDir' "alreadyExistsD"
|
||||
deleteDir' "dir"
|
||||
deleteDir' "noPerms"
|
||||
deleteDir' "noWritePerm"
|
||||
|
||||
|
||||
spec :: Spec
|
||||
spec = beforeAll_ (upTmpDir >> setupFiles) $ afterAll_ cleanupFiles $
|
||||
describe "HPath.IO.recreateSymlink" $ do
|
||||
|
||||
-- successes --
|
||||
it "recreateSymLink (Strict), all fine" $ do
|
||||
recreateSymlink' "myFileL"
|
||||
"movedFile"
|
||||
Strict
|
||||
removeFileIfExists "movedFile"
|
||||
|
||||
it "recreateSymLink (Strict), all fine" $ do
|
||||
recreateSymlink' "myFileL"
|
||||
"dir/movedFile"
|
||||
Strict
|
||||
removeFileIfExists "dir/movedFile"
|
||||
|
||||
-- posix failures --
|
||||
it "recreateSymLink (Strict), wrong input type (file)" $
|
||||
recreateSymlink' "myFile"
|
||||
"movedFile"
|
||||
Strict
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == InvalidArgument)
|
||||
|
||||
it "recreateSymLink (Strict), wrong input type (directory)" $
|
||||
recreateSymlink' "dir"
|
||||
"movedFile"
|
||||
Strict
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == InvalidArgument)
|
||||
|
||||
it "recreateSymLink (Strict), can't write to destination directory" $
|
||||
recreateSymlink' "myFileL"
|
||||
"noWritePerm/movedFile"
|
||||
Strict
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == PermissionDenied)
|
||||
|
||||
it "recreateSymLink (Strict), can't open destination directory" $
|
||||
recreateSymlink' "myFileL"
|
||||
"noPerms/movedFile"
|
||||
Strict
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == PermissionDenied)
|
||||
|
||||
it "recreateSymLink (Strict), can't open source directory" $
|
||||
recreateSymlink' "noPerms/myFileL"
|
||||
"movedFile"
|
||||
Strict
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == PermissionDenied)
|
||||
|
||||
it "recreateSymLink (Strict), destination file already exists" $
|
||||
recreateSymlink' "myFileL"
|
||||
"alreadyExists"
|
||||
Strict
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == AlreadyExists)
|
||||
|
||||
it "recreateSymLink (Strict), destination already exists and is a dir" $
|
||||
recreateSymlink' "myFileL"
|
||||
"alreadyExistsD"
|
||||
Strict
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == AlreadyExists)
|
||||
|
||||
-- custom failures --
|
||||
it "recreateSymLink (Strict), source and destination are the same file" $
|
||||
recreateSymlink' "myFileL"
|
||||
"myFileL"
|
||||
Strict
|
||||
`shouldThrow`
|
||||
isSameFile
|
||||
|
||||
117
test/HPath/IO/RenameFileSpec.hs
Normal file
117
test/HPath/IO/RenameFileSpec.hs
Normal file
@@ -0,0 +1,117 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module HPath.IO.RenameFileSpec where
|
||||
|
||||
|
||||
import Test.Hspec
|
||||
import HPath.IO.Errors
|
||||
import System.IO.Error
|
||||
(
|
||||
ioeGetErrorType
|
||||
)
|
||||
import GHC.IO.Exception
|
||||
(
|
||||
IOErrorType(..)
|
||||
)
|
||||
import Utils
|
||||
|
||||
|
||||
|
||||
upTmpDir :: IO ()
|
||||
upTmpDir = do
|
||||
setTmpDir "RenameFileSpec"
|
||||
createTmpDir
|
||||
|
||||
|
||||
setupFiles :: IO ()
|
||||
setupFiles = do
|
||||
createRegularFile' "myFile"
|
||||
createSymlink' "myFileL" "myFile"
|
||||
createRegularFile' "alreadyExists"
|
||||
createDir' "alreadyExistsD"
|
||||
createDir' "dir"
|
||||
createDir' "noPerms"
|
||||
createDir' "noWritePerm"
|
||||
noPerms "noPerms"
|
||||
noWritableDirPerms "noWritePerm"
|
||||
writeFile' "myFile" "Blahfaselgagaga"
|
||||
|
||||
|
||||
cleanupFiles :: IO ()
|
||||
cleanupFiles = do
|
||||
normalDirPerms "noPerms"
|
||||
normalDirPerms "noWritePerm"
|
||||
deleteFile' "myFile"
|
||||
deleteFile' "myFileL"
|
||||
deleteFile' "alreadyExists"
|
||||
deleteDir' "alreadyExistsD"
|
||||
deleteDir' "dir"
|
||||
deleteDir' "noPerms"
|
||||
deleteDir' "noWritePerm"
|
||||
|
||||
|
||||
spec :: Spec
|
||||
spec = beforeAll_ (upTmpDir >> setupFiles) $ afterAll_ cleanupFiles $
|
||||
describe "HPath.IO.renameFile" $ do
|
||||
|
||||
-- successes --
|
||||
it "renameFile, all fine" $
|
||||
renameFile' "myFile"
|
||||
"renamedFile"
|
||||
|
||||
it "renameFile, all fine" $
|
||||
renameFile' "myFile"
|
||||
"dir/renamedFile"
|
||||
|
||||
it "renameFile, all fine on symlink" $
|
||||
renameFile' "myFileL"
|
||||
"renamedFile"
|
||||
|
||||
it "renameFile, all fine on directory" $
|
||||
renameFile' "dir"
|
||||
"renamedFile"
|
||||
|
||||
-- posix failures --
|
||||
it "renameFile, source file does not exist" $
|
||||
renameFile' "fileDoesNotExist"
|
||||
"renamedFile"
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == NoSuchThing)
|
||||
|
||||
it "renameFile, can't write to output directory" $
|
||||
renameFile' "myFile"
|
||||
"noWritePerm/renamedFile"
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == PermissionDenied)
|
||||
|
||||
it "renameFile, can't open output directory" $
|
||||
renameFile' "myFile"
|
||||
"noPerms/renamedFile"
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == PermissionDenied)
|
||||
|
||||
it "renameFile, can't open source directory" $
|
||||
renameFile' "noPerms/myFile"
|
||||
"renamedFile"
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == PermissionDenied)
|
||||
|
||||
-- custom failures --
|
||||
it "renameFile, destination file already exists" $
|
||||
renameFile' "myFile"
|
||||
"alreadyExists"
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == AlreadyExists)
|
||||
|
||||
it "renameFile, move from file to dir" $
|
||||
renameFile' "myFile"
|
||||
"alreadyExistsD"
|
||||
`shouldThrow`
|
||||
(\e -> ioeGetErrorType e == AlreadyExists)
|
||||
|
||||
it "renameFile, source and dest are same file" $
|
||||
renameFile' "myFile"
|
||||
"myFile"
|
||||
`shouldThrow`
|
||||
isSameFile
|
||||
|
||||
27
test/HPath/IO/ToAbsSpec.hs
Normal file
27
test/HPath/IO/ToAbsSpec.hs
Normal file
@@ -0,0 +1,27 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
|
||||
module HPath.IO.ToAbsSpec where
|
||||
|
||||
|
||||
import Test.Hspec
|
||||
import HPath
|
||||
import HPath.IO
|
||||
|
||||
|
||||
|
||||
spec :: Spec
|
||||
spec = describe "HPath.IO.toAbs" $ do
|
||||
|
||||
-- successes --
|
||||
it "toAbs returns absolute paths unchanged" $ do
|
||||
p1 <- parseAbs "/a/b/c/d"
|
||||
to <- toAbs p1
|
||||
p1 `shouldBe` to
|
||||
|
||||
it "toAbs returns even existing absolute paths unchanged" $ do
|
||||
p1 <- parseAbs "/home"
|
||||
to <- toAbs p1
|
||||
p1 `shouldBe` to
|
||||
|
||||
|
||||
108
test/HPath/IO/WriteFileSpec.hs
Normal file
108
test/HPath/IO/WriteFileSpec.hs
Normal file
@@ -0,0 +1,108 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
|
||||
module HPath.IO.WriteFileSpec where
|
||||
|
||||
|
||||
import Test.Hspec
|
||||
import System.IO.Error
|
||||
(
|
||||
ioeGetErrorType
|
||||
)
|
||||
import GHC.IO.Exception
|
||||
(
|
||||
IOErrorType(..)
|
||||
)
|
||||
import Utils
|
||||
|
||||
|
||||
|
||||
upTmpDir :: IO ()
|
||||
upTmpDir = do
|
||||
setTmpDir "WriteFileSpec"
|
||||
createTmpDir
|
||||
|
||||
setupFiles :: IO ()
|
||||
setupFiles = do
|
||||
createRegularFile' "fileWithContent"
|
||||
createRegularFile' "fileWithoutContent"
|
||||
createSymlink' "inputFileSymL" "fileWithContent"
|
||||
createDir' "alreadyExistsD"
|
||||
createRegularFile' "noPerms"
|
||||
noPerms "noPerms"
|
||||
createDir' "noPermsD"
|
||||
createRegularFile' "noPermsD/inputFile"
|
||||
noPerms "noPermsD"
|
||||
writeFile' "fileWithContent" "BLKASL"
|
||||
|
||||
|
||||
cleanupFiles :: IO ()
|
||||
cleanupFiles = do
|
||||
deleteFile' "fileWithContent"
|
||||
deleteFile' "fileWithoutContent"
|
||||
deleteFile' "inputFileSymL"
|
||||
deleteDir' "alreadyExistsD"
|
||||
normalFilePerms "noPerms"
|
||||
deleteFile' "noPerms"
|
||||
normalDirPerms "noPermsD"
|
||||
deleteFile' "noPermsD/inputFile"
|
||||
deleteDir' "noPermsD"
|
||||
|
||||
|
||||
spec :: Spec
|
||||
spec = beforeAll_ (upTmpDir >> setupFiles) $ afterAll_ cleanupFiles $
|
||||
describe "HPath.IO.writeFile" $ do
|
||||
|
||||
-- successes --
|
||||
it "writeFile file with content, everything clear" $ do
|
||||
writeFile' "fileWithContent" "blahfaselllll"
|
||||
out <- readFile' "fileWithContent"
|
||||
out `shouldBe` "blahfaselllll"
|
||||
|
||||
it "writeFile file with content, everything clear" $ do
|
||||
writeFile' "fileWithContent" "gagagaga"
|
||||
out <- readFile' "fileWithContent"
|
||||
out `shouldBe` "gagagaga"
|
||||
|
||||
it "writeFile file with content, everything clear" $ do
|
||||
writeFile' "fileWithContent" ""
|
||||
out <- readFile' "fileWithContent"
|
||||
out `shouldBe` ""
|
||||
|
||||
it "writeFile file without content, everything clear" $ do
|
||||
writeFile' "fileWithoutContent" "blahfaselllll"
|
||||
out <- readFile' "fileWithoutContent"
|
||||
out `shouldBe` "blahfaselllll"
|
||||
|
||||
it "writeFile, everything clear" $ do
|
||||
writeFile' "fileWithoutContent" "gagagaga"
|
||||
out <- readFile' "fileWithoutContent"
|
||||
out `shouldBe` "gagagaga"
|
||||
|
||||
it "writeFile symlink, everything clear" $ do
|
||||
writeFile' "inputFileSymL" "blahfaselllll"
|
||||
out <- readFile' "inputFileSymL"
|
||||
out `shouldBe` "blahfaselllll"
|
||||
|
||||
it "writeFile symlink, everything clear" $ do
|
||||
writeFile' "inputFileSymL" "gagagaga"
|
||||
out <- readFile' "inputFileSymL"
|
||||
out `shouldBe` "gagagaga"
|
||||
|
||||
|
||||
-- posix failures --
|
||||
it "writeFile to dir, inappropriate type" $ do
|
||||
writeFile' "alreadyExistsD" ""
|
||||
`shouldThrow` (\e -> ioeGetErrorType e == InappropriateType)
|
||||
|
||||
it "writeFile, no permissions to file" $ do
|
||||
writeFile' "noPerms" ""
|
||||
`shouldThrow` (\e -> ioeGetErrorType e == PermissionDenied)
|
||||
|
||||
it "writeFile, no permissions to file" $ do
|
||||
writeFile' "noPermsD/inputFile" ""
|
||||
`shouldThrow` (\e -> ioeGetErrorType e == PermissionDenied)
|
||||
|
||||
it "writeFile, file does not exist" $ do
|
||||
writeFile' "gaga" ""
|
||||
`shouldThrow` (\e -> ioeGetErrorType e == NoSuchThing)
|
||||
233
test/Main.hs
233
test/Main.hs
@@ -1,222 +1,19 @@
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
-- | Test suite.
|
||||
|
||||
module Main where
|
||||
|
||||
import Control.Applicative
|
||||
import Control.Monad
|
||||
import Data.Maybe
|
||||
import Data.Monoid
|
||||
import Path
|
||||
import Path.Internal
|
||||
import Test.Hspec
|
||||
import Test.Hspec.Runner
|
||||
import Test.Hspec.Formatters
|
||||
import qualified Spec
|
||||
import Utils
|
||||
|
||||
|
||||
-- TODO: chardev, blockdev, namedpipe, socket
|
||||
|
||||
|
||||
-- | Test suite entry point, returns exit failure if any test fails.
|
||||
main :: IO ()
|
||||
main = hspec spec
|
||||
|
||||
-- | Test suite.
|
||||
spec :: Spec
|
||||
spec =
|
||||
do describe "Parsing: Path Abs Dir" parseAbsDirSpec
|
||||
describe "Parsing: Path Rel Dir" parseRelDirSpec
|
||||
describe "Parsing: Path Abs File" parseAbsFileSpec
|
||||
describe "Parsing: Path Rel File" parseRelFileSpec
|
||||
describe "Operations: (</>)" operationAppend
|
||||
describe "Operations: stripDir" operationStripDir
|
||||
describe "Operations: isParentOf" operationIsParentOf
|
||||
describe "Operations: parent" operationParent
|
||||
describe "Operations: filename" operationFilename
|
||||
describe "Restrictions" restrictions
|
||||
|
||||
-- | Restricting the input of any tricks.
|
||||
restrictions :: Spec
|
||||
restrictions =
|
||||
do parseFails "~/"
|
||||
parseFails "~/foo"
|
||||
parseFails "~/foo/bar"
|
||||
parseFails "../"
|
||||
parseFails ".."
|
||||
parseFails "."
|
||||
parseFails "/.."
|
||||
parseFails "/foo/../bar/"
|
||||
parseFails "/foo/bar/.."
|
||||
where parseFails x =
|
||||
it (show x ++ " should be rejected")
|
||||
(isNothing (void (parseAbsDir x) <|>
|
||||
void (parseRelDir x) <|>
|
||||
void (parseAbsFile x) <|>
|
||||
void (parseRelFile x)))
|
||||
|
||||
-- | The 'filename' operation.
|
||||
operationFilename :: Spec
|
||||
operationFilename =
|
||||
do it "filename ($(mkAbsDir parent) </> filename $(mkRelFile filename)) == $(mkRelFile filename)"
|
||||
(filename ($(mkAbsDir "/home/chris/") </>
|
||||
filename $(mkRelFile "bar.txt")) ==
|
||||
$(mkRelFile "bar.txt"))
|
||||
it "filename ($(mkRelDir parent) </> filename $(mkRelFile filename)) == $(mkRelFile filename)"
|
||||
(filename ($(mkRelDir "home/chris/") </>
|
||||
filename $(mkRelFile "bar.txt")) ==
|
||||
$(mkRelFile "bar.txt"))
|
||||
|
||||
-- | The 'parent' operation.
|
||||
operationParent :: Spec
|
||||
operationParent =
|
||||
do it "parent (parent </> child) == parent"
|
||||
(parent ($(mkAbsDir "/foo") </>
|
||||
$(mkRelDir "bar")) ==
|
||||
$(mkAbsDir "/foo"))
|
||||
it "parent \"\" == \"\""
|
||||
(parent $(mkAbsDir "/") ==
|
||||
$(mkAbsDir "/"))
|
||||
it "parent (parent \"\") == \"\""
|
||||
(parent (parent $(mkAbsDir "/")) ==
|
||||
$(mkAbsDir "/"))
|
||||
|
||||
-- | The 'isParentOf' operation.
|
||||
operationIsParentOf :: Spec
|
||||
operationIsParentOf =
|
||||
do it "isParentOf parent (parent </> child)"
|
||||
(isParentOf
|
||||
$(mkAbsDir "///bar/")
|
||||
($(mkAbsDir "///bar/") </>
|
||||
$(mkRelFile "bar/foo.txt")))
|
||||
it "isParentOf parent (parent </> child)"
|
||||
(isParentOf
|
||||
$(mkRelDir "bar/")
|
||||
($(mkRelDir "bar/") </>
|
||||
$(mkRelFile "bob/foo.txt")))
|
||||
|
||||
-- | The 'stripDir' operation.
|
||||
operationStripDir :: Spec
|
||||
operationStripDir =
|
||||
do it "stripDir parent (parent </> child) = child"
|
||||
(stripDir $(mkAbsDir "///bar/")
|
||||
($(mkAbsDir "///bar/") </>
|
||||
$(mkRelFile "bar/foo.txt")) ==
|
||||
Just $(mkRelFile "bar/foo.txt"))
|
||||
it "stripDir parent (parent </> child) = child"
|
||||
(stripDir $(mkRelDir "bar/")
|
||||
($(mkRelDir "bar/") </>
|
||||
$(mkRelFile "bob/foo.txt")) ==
|
||||
Just $(mkRelFile "bob/foo.txt"))
|
||||
it "stripDir parent parent = _|_"
|
||||
(stripDir $(mkAbsDir "/home/chris/foo")
|
||||
$(mkAbsDir "/home/chris/foo") ==
|
||||
Nothing)
|
||||
|
||||
-- | The '</>' operation.
|
||||
operationAppend :: Spec
|
||||
operationAppend =
|
||||
do it "AbsDir + RelDir = AbsDir"
|
||||
($(mkAbsDir "/home/") </>
|
||||
$(mkRelDir "chris") ==
|
||||
$(mkAbsDir "/home/chris/"))
|
||||
it "AbsDir + RelFile = AbsFile"
|
||||
($(mkAbsDir "/home/") </>
|
||||
$(mkRelFile "chris/test.txt") ==
|
||||
$(mkAbsFile "/home/chris/test.txt"))
|
||||
it "RelDir + RelDir = RelDir"
|
||||
($(mkRelDir "home/") </>
|
||||
$(mkRelDir "chris") ==
|
||||
$(mkRelDir "home/chris"))
|
||||
it "RelDir + RelFile = RelFile"
|
||||
($(mkRelDir "home/") </>
|
||||
$(mkRelFile "chris/test.txt") ==
|
||||
$(mkRelFile "home/chris/test.txt"))
|
||||
|
||||
-- | Tests for the tokenizer.
|
||||
parseAbsDirSpec :: Spec
|
||||
parseAbsDirSpec =
|
||||
do failing ""
|
||||
failing "./"
|
||||
failing "~/"
|
||||
failing "foo.txt"
|
||||
succeeding "/" (Path "/")
|
||||
succeeding "//" (Path "/")
|
||||
succeeding "///foo//bar//mu/" (Path "/foo/bar/mu/")
|
||||
succeeding "///foo//bar////mu" (Path "/foo/bar/mu/")
|
||||
succeeding "///foo//bar/.//mu" (Path "/foo/bar/mu/")
|
||||
where failing x = parserTest parseAbsDir x Nothing
|
||||
succeeding x with = parserTest parseAbsDir x (Just with)
|
||||
|
||||
-- | Tests for the tokenizer.
|
||||
parseRelDirSpec :: Spec
|
||||
parseRelDirSpec =
|
||||
do failing ""
|
||||
failing "/"
|
||||
failing "//"
|
||||
failing "~/"
|
||||
failing "/"
|
||||
failing "./"
|
||||
failing "././"
|
||||
failing "//"
|
||||
failing "///foo//bar//mu/"
|
||||
failing "///foo//bar////mu"
|
||||
failing "///foo//bar/.//mu"
|
||||
succeeding "foo.bak" (Path "foo.bak/")
|
||||
succeeding "./foo" (Path "foo/")
|
||||
succeeding "././foo" (Path "foo/")
|
||||
succeeding "./foo/./bar" (Path "foo/bar/")
|
||||
succeeding "foo//bar//mu//" (Path "foo/bar/mu/")
|
||||
succeeding "foo//bar////mu" (Path "foo/bar/mu/")
|
||||
succeeding "foo//bar/.//mu" (Path "foo/bar/mu/")
|
||||
where failing x = parserTest parseRelDir x Nothing
|
||||
succeeding x with = parserTest parseRelDir x (Just with)
|
||||
|
||||
-- | Tests for the tokenizer.
|
||||
parseAbsFileSpec :: Spec
|
||||
parseAbsFileSpec =
|
||||
do failing ""
|
||||
failing "./"
|
||||
failing "~/"
|
||||
failing "./foo.txt"
|
||||
failing "/"
|
||||
failing "//"
|
||||
failing "///foo//bar//mu/"
|
||||
succeeding "/foo.txt" (Path "/foo.txt")
|
||||
succeeding "///foo//bar////mu.txt" (Path "/foo/bar/mu.txt")
|
||||
succeeding "///foo//bar/.//mu.txt" (Path "/foo/bar/mu.txt")
|
||||
where failing x = parserTest parseAbsFile x Nothing
|
||||
succeeding x with = parserTest parseAbsFile x (Just with)
|
||||
|
||||
-- | Tests for the tokenizer.
|
||||
parseRelFileSpec :: Spec
|
||||
parseRelFileSpec =
|
||||
do failing ""
|
||||
failing "/"
|
||||
failing "//"
|
||||
failing "~/"
|
||||
failing "/"
|
||||
failing "./"
|
||||
failing "//"
|
||||
failing "///foo//bar//mu/"
|
||||
failing "///foo//bar////mu"
|
||||
failing "///foo//bar/.//mu"
|
||||
succeeding "foo.txt" (Path "foo.txt")
|
||||
succeeding "./foo.txt" (Path "foo.txt")
|
||||
succeeding "././foo.txt" (Path "foo.txt")
|
||||
succeeding "./foo/./bar.txt" (Path "foo/bar.txt")
|
||||
succeeding "foo//bar//mu.txt" (Path "foo/bar/mu.txt")
|
||||
succeeding "foo//bar////mu.txt" (Path "foo/bar/mu.txt")
|
||||
succeeding "foo//bar/.//mu.txt" (Path "foo/bar/mu.txt")
|
||||
where failing x = parserTest parseRelFile x Nothing
|
||||
succeeding x with = parserTest parseRelFile x (Just with)
|
||||
|
||||
-- | Parser test.
|
||||
parserTest :: (Show a1,Show a,Eq a1)
|
||||
=> (a -> Maybe a1) -> a -> Maybe a1 -> SpecWith ()
|
||||
parserTest parser input expected =
|
||||
it ((case expected of
|
||||
Nothing -> "Failing: "
|
||||
Just{} -> "Succeeding: ") <>
|
||||
"Parsing " <>
|
||||
show input <>
|
||||
" " <>
|
||||
case expected of
|
||||
Nothing -> "should fail."
|
||||
Just x -> "should succeed with: " <> show x)
|
||||
(actual == expected)
|
||||
where actual = parser input
|
||||
main =
|
||||
hspecWith
|
||||
defaultConfig { configFormatter = Just progress }
|
||||
$ beforeAll_ createBaseTmpDir
|
||||
$ afterAll_ deleteBaseTmpDir
|
||||
$ Spec.spec
|
||||
|
||||
2
test/Spec.hs
Normal file
2
test/Spec.hs
Normal file
@@ -0,0 +1,2 @@
|
||||
-- file test/Spec.hs
|
||||
{-# OPTIONS_GHC -F -pgmF hspec-discover -optF --module-name=Spec #-}
|
||||
307
test/Utils.hs
Normal file
307
test/Utils.hs
Normal file
@@ -0,0 +1,307 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE PackageImports #-}
|
||||
|
||||
|
||||
module Utils where
|
||||
|
||||
|
||||
import Control.Applicative
|
||||
(
|
||||
(<$>)
|
||||
)
|
||||
import Control.Monad
|
||||
(
|
||||
forM_
|
||||
, void
|
||||
)
|
||||
import Control.Monad.IfElse
|
||||
(
|
||||
whenM
|
||||
)
|
||||
import qualified Data.ByteString as BS
|
||||
import Data.IORef
|
||||
(
|
||||
newIORef
|
||||
, readIORef
|
||||
, writeIORef
|
||||
, IORef
|
||||
)
|
||||
import HPath.IO
|
||||
import HPath.IO.Errors
|
||||
import Prelude hiding (appendFile, readFile, writeFile)
|
||||
import Data.Maybe
|
||||
(
|
||||
fromJust
|
||||
)
|
||||
import qualified HPath as P
|
||||
import System.IO.Unsafe
|
||||
(
|
||||
unsafePerformIO
|
||||
)
|
||||
import qualified System.Posix.Directory.Traversals as DT
|
||||
import System.Posix.Env.ByteString
|
||||
(
|
||||
getEnv
|
||||
)
|
||||
import Data.ByteString
|
||||
(
|
||||
ByteString
|
||||
)
|
||||
import qualified Data.ByteString.Lazy as L
|
||||
import System.Posix.Files.ByteString
|
||||
(
|
||||
groupExecuteMode
|
||||
, groupReadMode
|
||||
, nullFileMode
|
||||
, otherExecuteMode
|
||||
, otherReadMode
|
||||
, ownerExecuteMode
|
||||
, ownerReadMode
|
||||
, setFileMode
|
||||
, unionFileModes
|
||||
)
|
||||
|
||||
|
||||
|
||||
baseTmpDir :: ByteString
|
||||
baseTmpDir = "test/HPath/IO/tmp/"
|
||||
|
||||
|
||||
tmpDir :: IORef ByteString
|
||||
{-# NOINLINE tmpDir #-}
|
||||
tmpDir = unsafePerformIO (newIORef baseTmpDir)
|
||||
|
||||
|
||||
|
||||
-----------------
|
||||
--[ Utilities ]--
|
||||
-----------------
|
||||
|
||||
|
||||
setTmpDir :: ByteString -> IO ()
|
||||
{-# NOINLINE setTmpDir #-}
|
||||
setTmpDir bs = writeIORef tmpDir (baseTmpDir `BS.append` bs)
|
||||
|
||||
|
||||
createTmpDir :: IO ()
|
||||
{-# NOINLINE createTmpDir #-}
|
||||
createTmpDir = do
|
||||
pwd <- fromJust <$> getEnv "PWD" >>= P.parseAbs
|
||||
tmp <- P.parseRel =<< readIORef tmpDir
|
||||
void $ createDir newDirPerms (pwd P.</> tmp)
|
||||
|
||||
|
||||
deleteTmpDir :: IO ()
|
||||
{-# NOINLINE deleteTmpDir #-}
|
||||
deleteTmpDir = do
|
||||
pwd <- fromJust <$> getEnv "PWD" >>= P.parseAbs
|
||||
tmp <- P.parseRel =<< readIORef tmpDir
|
||||
void $ deleteDir (pwd P.</> tmp)
|
||||
|
||||
|
||||
createBaseTmpDir :: IO ()
|
||||
{-# NOINLINE createBaseTmpDir #-}
|
||||
createBaseTmpDir = do
|
||||
pwd <- fromJust <$> getEnv "PWD" >>= P.parseAbs
|
||||
tmp <- P.parseRel baseTmpDir
|
||||
void $ createDir newDirPerms (pwd P.</> tmp)
|
||||
|
||||
|
||||
deleteBaseTmpDir :: IO ()
|
||||
{-# NOINLINE deleteBaseTmpDir #-}
|
||||
deleteBaseTmpDir = do
|
||||
pwd <- fromJust <$> getEnv "PWD" >>= P.parseAbs
|
||||
tmp <- P.parseRel baseTmpDir
|
||||
contents <- getDirsFiles (pwd P.</> tmp)
|
||||
forM_ contents deleteDir
|
||||
void $ deleteDir (pwd P.</> tmp)
|
||||
|
||||
|
||||
withRawTmpDir :: (P.Path P.Abs -> IO a) -> IO a
|
||||
{-# NOINLINE withRawTmpDir #-}
|
||||
withRawTmpDir f = do
|
||||
pwd <- fromJust <$> getEnv "PWD" >>= P.parseAbs
|
||||
tmp <- P.parseRel =<< readIORef tmpDir
|
||||
f (pwd P.</> tmp)
|
||||
|
||||
|
||||
getRawTmpDir :: IO ByteString
|
||||
{-# NOINLINE getRawTmpDir #-}
|
||||
getRawTmpDir = withRawTmpDir (return . flip BS.append "/" . P.fromAbs)
|
||||
|
||||
|
||||
withTmpDir :: ByteString -> (P.Path P.Abs -> IO a) -> IO a
|
||||
{-# NOINLINE withTmpDir #-}
|
||||
withTmpDir ip f = do
|
||||
pwd <- fromJust <$> getEnv "PWD" >>= P.parseAbs
|
||||
tmp <- P.parseRel =<< readIORef tmpDir
|
||||
p <- (pwd P.</> tmp P.</>) <$> P.parseRel ip
|
||||
f p
|
||||
|
||||
|
||||
withTmpDir' :: ByteString
|
||||
-> ByteString
|
||||
-> (P.Path P.Abs -> P.Path P.Abs -> IO a)
|
||||
-> IO a
|
||||
{-# NOINLINE withTmpDir' #-}
|
||||
withTmpDir' ip1 ip2 f = do
|
||||
pwd <- fromJust <$> getEnv "PWD" >>= P.parseAbs
|
||||
tmp <- P.parseRel =<< readIORef tmpDir
|
||||
p1 <- (pwd P.</> tmp P.</>) <$> P.parseRel ip1
|
||||
p2 <- (pwd P.</> tmp P.</>) <$> P.parseRel ip2
|
||||
f p1 p2
|
||||
|
||||
|
||||
removeFileIfExists :: ByteString -> IO ()
|
||||
{-# NOINLINE removeFileIfExists #-}
|
||||
removeFileIfExists bs =
|
||||
withTmpDir bs $ \p -> whenM (doesFileExist p) (deleteFile p)
|
||||
|
||||
|
||||
removeDirIfExists :: ByteString -> IO ()
|
||||
{-# NOINLINE removeDirIfExists #-}
|
||||
removeDirIfExists bs =
|
||||
withTmpDir bs $ \p -> whenM (doesDirectoryExist p) (deleteDirRecursive p)
|
||||
|
||||
|
||||
copyFile' :: ByteString -> ByteString -> CopyMode -> IO ()
|
||||
{-# NOINLINE copyFile' #-}
|
||||
copyFile' inputFileP outputFileP cm =
|
||||
withTmpDir' inputFileP outputFileP (\p1 p2 -> copyFile p1 p2 cm)
|
||||
|
||||
|
||||
copyDirRecursive' :: ByteString -> ByteString
|
||||
-> CopyMode -> RecursiveErrorMode -> IO ()
|
||||
{-# NOINLINE copyDirRecursive' #-}
|
||||
copyDirRecursive' inputDirP outputDirP cm rm =
|
||||
withTmpDir' inputDirP outputDirP (\p1 p2 -> copyDirRecursive p1 p2 cm rm)
|
||||
|
||||
|
||||
createDir' :: ByteString -> IO ()
|
||||
{-# NOINLINE createDir' #-}
|
||||
createDir' dest = withTmpDir dest (createDir newDirPerms)
|
||||
|
||||
createDirRecursive' :: ByteString -> IO ()
|
||||
{-# NOINLINE createDirRecursive' #-}
|
||||
createDirRecursive' dest = withTmpDir dest (createDirRecursive newDirPerms)
|
||||
|
||||
createRegularFile' :: ByteString -> IO ()
|
||||
{-# NOINLINE createRegularFile' #-}
|
||||
createRegularFile' dest = withTmpDir dest (createRegularFile newFilePerms)
|
||||
|
||||
|
||||
createSymlink' :: ByteString -> ByteString -> IO ()
|
||||
{-# NOINLINE createSymlink' #-}
|
||||
createSymlink' dest sympoint = withTmpDir dest
|
||||
(\x -> createSymlink x sympoint)
|
||||
|
||||
|
||||
renameFile' :: ByteString -> ByteString -> IO ()
|
||||
{-# NOINLINE renameFile' #-}
|
||||
renameFile' inputFileP outputFileP =
|
||||
withTmpDir' inputFileP outputFileP $ \i o -> do
|
||||
renameFile i o
|
||||
renameFile o i
|
||||
|
||||
|
||||
moveFile' :: ByteString -> ByteString -> CopyMode -> IO ()
|
||||
{-# NOINLINE moveFile' #-}
|
||||
moveFile' inputFileP outputFileP cm =
|
||||
withTmpDir' inputFileP outputFileP $ \i o -> do
|
||||
moveFile i o cm
|
||||
moveFile o i Strict
|
||||
|
||||
|
||||
recreateSymlink' :: ByteString -> ByteString -> CopyMode -> IO ()
|
||||
{-# NOINLINE recreateSymlink' #-}
|
||||
recreateSymlink' inputFileP outputFileP cm =
|
||||
withTmpDir' inputFileP outputFileP (\p1 p2 -> recreateSymlink p1 p2 cm)
|
||||
|
||||
|
||||
noWritableDirPerms :: ByteString -> IO ()
|
||||
{-# NOINLINE noWritableDirPerms #-}
|
||||
noWritableDirPerms path = withTmpDir path $ \p ->
|
||||
setFileMode (P.fromAbs p) perms
|
||||
where
|
||||
perms = ownerReadMode
|
||||
`unionFileModes` ownerExecuteMode
|
||||
`unionFileModes` groupReadMode
|
||||
`unionFileModes` groupExecuteMode
|
||||
`unionFileModes` otherReadMode
|
||||
`unionFileModes` otherExecuteMode
|
||||
|
||||
|
||||
noPerms :: ByteString -> IO ()
|
||||
{-# NOINLINE noPerms #-}
|
||||
noPerms path = withTmpDir path $ \p -> setFileMode (P.fromAbs p) nullFileMode
|
||||
|
||||
|
||||
normalDirPerms :: ByteString -> IO ()
|
||||
{-# NOINLINE normalDirPerms #-}
|
||||
normalDirPerms path =
|
||||
withTmpDir path $ \p -> setFileMode (P.fromAbs p) newDirPerms
|
||||
|
||||
|
||||
normalFilePerms :: ByteString -> IO ()
|
||||
{-# NOINLINE normalFilePerms #-}
|
||||
normalFilePerms path =
|
||||
withTmpDir path $ \p -> setFileMode (P.fromAbs p) newFilePerms
|
||||
|
||||
|
||||
getFileType' :: ByteString -> IO FileType
|
||||
{-# NOINLINE getFileType' #-}
|
||||
getFileType' path = withTmpDir path getFileType
|
||||
|
||||
|
||||
getDirsFiles' :: ByteString -> IO [P.Path P.Abs]
|
||||
{-# NOINLINE getDirsFiles' #-}
|
||||
getDirsFiles' path = withTmpDir path getDirsFiles
|
||||
|
||||
|
||||
deleteFile' :: ByteString -> IO ()
|
||||
{-# NOINLINE deleteFile' #-}
|
||||
deleteFile' p = withTmpDir p deleteFile
|
||||
|
||||
|
||||
deleteDir' :: ByteString -> IO ()
|
||||
{-# NOINLINE deleteDir' #-}
|
||||
deleteDir' p = withTmpDir p deleteDir
|
||||
|
||||
|
||||
deleteDirRecursive' :: ByteString -> IO ()
|
||||
{-# NOINLINE deleteDirRecursive' #-}
|
||||
deleteDirRecursive' p = withTmpDir p deleteDirRecursive
|
||||
|
||||
|
||||
canonicalizePath' :: ByteString -> IO (P.Path P.Abs)
|
||||
{-# NOINLINE canonicalizePath' #-}
|
||||
canonicalizePath' p = withTmpDir p canonicalizePath
|
||||
|
||||
|
||||
writeFile' :: ByteString -> ByteString -> IO ()
|
||||
{-# NOINLINE writeFile' #-}
|
||||
writeFile' ip bs =
|
||||
withTmpDir ip $ \p -> writeFile p bs
|
||||
|
||||
|
||||
appendFile' :: ByteString -> ByteString -> IO ()
|
||||
{-# NOINLINE appendFile' #-}
|
||||
appendFile' ip bs =
|
||||
withTmpDir ip $ \p -> appendFile p bs
|
||||
|
||||
|
||||
allDirectoryContents' :: ByteString -> IO [ByteString]
|
||||
{-# NOINLINE allDirectoryContents' #-}
|
||||
allDirectoryContents' ip =
|
||||
withTmpDir ip $ \p -> DT.allDirectoryContents' (P.fromAbs p)
|
||||
|
||||
|
||||
readFile' :: ByteString -> IO ByteString
|
||||
{-# NOINLINE readFile' #-}
|
||||
readFile' p = withTmpDir p readFile
|
||||
|
||||
|
||||
readFileEOF' :: ByteString -> IO L.ByteString
|
||||
{-# NOINLINE readFileEOF' #-}
|
||||
readFileEOF' p = withTmpDir p readFileEOF
|
||||
|
||||
52
update-gh-pages.sh
Executable file
52
update-gh-pages.sh
Executable file
@@ -0,0 +1,52 @@
|
||||
#!/bin/bash
|
||||
|
||||
SOURCE_BRANCH="master"
|
||||
TARGET_BRANCH="gh-pages"
|
||||
REPO="https://${GH_TOKEN}@github.com/hasufell/hpath"
|
||||
DOC_LOCATION="/dist/doc/html/hpath"
|
||||
|
||||
|
||||
# Pull requests and commits to other branches shouldn't try to deploy,
|
||||
# just build to verify
|
||||
if [ "$TRAVIS_PULL_REQUEST" != "false" -o "$TRAVIS_BRANCH" != "$SOURCE_BRANCH" ]; then
|
||||
echo "Skipping docs deploy."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
|
||||
cd "$HOME"
|
||||
git config --global user.email "travis@travis-ci.org"
|
||||
git config --global user.name "travis-ci"
|
||||
git clone --branch=${TARGET_BRANCH} ${REPO} ${TARGET_BRANCH} || exit 1
|
||||
|
||||
# docs
|
||||
cd ${TARGET_BRANCH} || exit 1
|
||||
echo "Removing old docs."
|
||||
rm -rf *
|
||||
echo "Adding new docs."
|
||||
cp -rf "${TRAVIS_BUILD_DIR}${DOC_LOCATION}"/* . || exit 1
|
||||
|
||||
# If there are no changes to the compiled out (e.g. this is a README update)
|
||||
# then just bail.
|
||||
if [ -z "`git diff --exit-code`" ]; then
|
||||
echo "No changes to the output on this push; exiting."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
git add -- .
|
||||
|
||||
if [[ -e ./index.html ]] ; then
|
||||
echo "Commiting docs."
|
||||
git commit -m "Lastest docs updated
|
||||
|
||||
travis build: $TRAVIS_BUILD_NUMBER
|
||||
commit: $TRAVIS_COMMIT
|
||||
auto-pushed to gh-pages"
|
||||
|
||||
git push origin $TARGET_BRANCH
|
||||
echo "Published docs to gh-pages."
|
||||
else
|
||||
echo "Error: docs are empty."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user