0 | module Control.RIO.File
  1 |
  2 | import public Control.RIO.App
  3 | import public Data.FilePath.File
  4 | import public Data.DPair
  5 | import public Data.List
  6 | import Data.List1
  7 | import Data.IORef
  8 |
  9 | import System.Directory
 10 | import System.File
 11 |
 12 | %default total
 13 |
 14 | --------------------------------------------------------------------------------
 15 | --          Error Type
 16 | --------------------------------------------------------------------------------
 17 |
 18 | public export
 19 | data FileErr : Type where
 20 |   ReadErr       : (path : File t) -> (error : FileError) -> FileErr
 21 |
 22 |   WriteErr      : (path : File t) -> (error : FileError) -> FileErr
 23 |
 24 |   DeleteErr     : (path : Path t) -> (error : FileError) -> FileErr
 25 |
 26 |   LimitExceeded : (path : File t) -> (limit : Bits32) -> FileErr
 27 |
 28 |   CurDir        : FileErr
 29 |
 30 |   ChangeDir     : Path t -> FileErr
 31 |
 32 |   ListDir       : Path t -> FileError -> FileErr
 33 |
 34 |   MkDir         : Path t -> FileError -> FileErr
 35 |
 36 | export
 37 | printErr : FileErr -> String
 38 | printErr (ReadErr p err) =
 39 |   "Error when reading from \"\{p}\": \{show err}"
 40 | printErr (WriteErr p err) =
 41 |   "Error when writing to \"\{p}\": \{show err}"
 42 | printErr (DeleteErr p err) =
 43 |   "Error when deleteing \"\{p}\": \{show err}"
 44 | printErr (LimitExceeded p _) =
 45 |   "Error when reading \"\{p}\": File size limit exceeded."
 46 | printErr CurDir = "Failed to get current directory"
 47 | printErr (ChangeDir p) = "Failed to change to directory \"\{p}\""
 48 | printErr (ListDir p err) =
 49 |   "Error when reading directory \"\{p}\": \{show err}."
 50 | printErr (MkDir p err) =
 51 |   "Error when creating directory \"\{p}\": \{show err}."
 52 |
 53 | --------------------------------------------------------------------------------
 54 | --          Record
 55 | --------------------------------------------------------------------------------
 56 |
 57 | ||| Record representing a simple file system, where we can
 58 | ||| read from and write to files.
 59 | public export
 60 | record FS where
 61 |   constructor MkFS
 62 |   ||| Writes the string to the file in question
 63 |   write_  : (0 t : _) -> File t -> String -> IO (Either FileErr ())
 64 |
 65 |   ||| Appends the string to the file in question
 66 |   append_ : (0 t : _) -> File t -> String -> IO (Either FileErr ())
 67 |
 68 |   ||| Deletes the given file.
 69 |   removeFile_ : (0 t : _) -> File t -> IO (Either FileErr ())
 70 |
 71 |   ||| Deletes the given directory.
 72 |   removeDir_ : (0 t : _) -> Path t -> IO (Either FileErr ())
 73 |
 74 |   ||| Checks if the given file exists in the file system
 75 |   exists_ : (0 t : _) -> Path t -> IO Bool
 76 |
 77 |   ||| This tries to read a file in whole, so we have to limit
 78 |   ||| the acceptable file size, otherwise this might overflow
 79 |   ||| the computer's memory if presented with an infinite stream
 80 |   ||| such as `/dev/zero`
 81 |   read_   : (0 t : _) -> File t -> Bits32 -> IO (Either FileErr String)
 82 |
 83 |   ||| Prints the current working directory.
 84 |   curDir_ : IO (Either FileErr (Path Abs))
 85 |
 86 |   ||| Change to the given directory
 87 |   changeDir_ : (0 t : _) -> Path t -> IO (Either FileErr ())
 88 |
 89 |   ||| List entries in a directory (without `.` and `..`)
 90 |   listDir_ : (0 t : _) -> Path t -> IO (Either FileErr (List Body))
 91 |
 92 |   ||| Creates the given directory
 93 |   mkDir_ : (0 t : _) -> Path t -> IO (Either FileErr ())
 94 |
 95 | --------------------------------------------------------------------------------
 96 | --          Interface
 97 | --------------------------------------------------------------------------------
 98 |
 99 | ||| True if the given file or directory exists in the file system
100 | export
101 | exists : FS => Path t -> RIO x Bool
102 | exists path = liftIO (exists_ %search t path)
103 |
104 | ||| True if the given file exists in the file system
105 | export %inline
106 | fileExists : FS => File t -> RIO x Bool
107 | fileExists = exists . toPath
108 |
109 | ||| True if the given file or directory does not exist in the file system
110 | export %inline
111 | missing : FS => Path t -> RIO x Bool
112 | missing = map not . exists
113 |
114 | ||| True if the given file does not exist in the file system
115 | export %inline
116 | fileMissing : FS => File t -> RIO x Bool
117 | fileMissing = missing . toPath
118 |
119 | ||| Writes the given string to a file.
120 | export
121 | write : FS => Has FileErr xs => File t -> String -> App xs ()
122 | write path str = injectIO (write_ %search t path str)
123 |
124 | ||| Writes the given string to a file.
125 | export
126 | append : FS => Has FileErr xs => File t -> String -> App xs ()
127 | append path str = injectIO (append_ %search t path str)
128 |
129 | ||| Reads a string from a file, the size of which must not
130 | ||| exceed the given number of bytes.
131 | export
132 | read : FS => Has FileErr xs => File t -> Bits32 -> App xs String
133 | read path limit = injectIO (read_ %search t path limit)
134 |
135 | ||| Delete a file from the file system.
136 | export
137 | removeFile : FS => Has FileErr xs => File t -> App xs ()
138 | removeFile path = injectIO (removeFile_ %search t path)
139 |
140 | ||| Delete a file from the file system.
141 | export
142 | removeDir : FS => Has FileErr xs => Path t -> App xs ()
143 | removeDir path = injectIO (removeDir_ %search t path)
144 |
145 | ||| Returns the current directory's path.
146 | export
147 | curDir : FS => Has FileErr xs => App xs (Path Abs)
148 | curDir = injectIO $ curDir_ %search
149 |
150 | ||| Changes the working directory
151 | export
152 | chgDir : FS => Has FileErr xs => (dir : Path t) -> App xs ()
153 | chgDir dir = injectIO (changeDir_ %search t dir)
154 |
155 | ||| Runs an action in the given directory, changing back
156 | ||| to the current directory afterwards.
157 | export
158 | inDir :
159 |      {auto _ : FS}
160 |   -> {auto _ : Has FileErr xs}
161 |   -> (dir : Path t)
162 |   -> (act : App xs a)
163 |   -> App xs a
164 | inDir dir act = do
165 |   cur <- curDir
166 |   finally (chgDir cur) (chgDir dir >> act)
167 |
168 | ||| List entries in a directory (without `.` and `..`)
169 | export
170 | listDir : FS => Has FileErr xs => Path t -> App xs (List Body)
171 | listDir dir = injectIO (listDir_ %search t dir)
172 |
173 | ||| Creates the given directory
174 | export
175 | mkDir : FS => Has FileErr xs => Path t -> App xs ()
176 | mkDir dir = injectIO (mkDir_ %search t dir)
177 |
178 | ||| Creates the given directory (including parent directories)
179 | export
180 | mkDirP : FS => Has FileErr xs => Path t -> App xs ()
181 | mkDirP dir = go (parentDirs dir) >> mkDir dir
182 |
183 |   where
184 |     go : List (Path t) -> App xs ()
185 |     go []        = pure ()
186 |     go (x :: xs) = when !(missing x) $ go xs >> mkDir x
187 |
188 | ||| Creates the parent directory of the given file
189 | export %inline
190 | mkParentDir : FS => Has FileErr xs => File t -> App xs ()
191 | mkParentDir = mkDirP . parent
192 |
193 | --------------------------------------------------------------------------------
194 | --          Default FS
195 | --------------------------------------------------------------------------------
196 |
197 | writeImpl : (0 t : _) -> File t -> String -> IO (Either FileErr ())
198 | writeImpl _ fp s = mapFst (WriteErr fp) <$> writeFile "\{fp}" s
199 |
200 | appendImpl : (0 t : _) -> File t -> String -> IO (Either FileErr ())
201 | appendImpl _ fp s = mapFst (WriteErr fp) <$> appendFile "\{fp}" s
202 |
203 | removeFileImpl : (0 t : _) -> File t -> IO (Either FileErr ())
204 | removeFileImpl _ fp = mapFst (DeleteErr $ toPath fp) <$> removeFile "\{fp}"
205 |
206 | removeDirImpl : (0 t : _) -> Path t -> IO (Either FileErr ())
207 | removeDirImpl _ fp = Right <$> removeDir "\{fp}"
208 |
209 | readImpl : (0 t : _) -> File t -> Bits32 -> IO (Either FileErr String)
210 | readImpl _ fp limit = do
211 |   Right s <- withFile "\{fp}" Read pure fileSize
212 |     | Left err => pure (Left $ ReadErr fp err)
213 |   case s <= 0 of
214 |     True => pure (Right "")
215 |     False => case s <= cast limit of
216 |       True  => assert_total $ do
217 |         Left err <- readFile "\{fp}" | Right s => pure (Right s)
218 |         pure (Left $ ReadErr fp err)
219 |
220 |       False => pure (Left $ LimitExceeded fp limit)
221 |
222 | curDirImpl : IO (Either FileErr (Path Abs))
223 | curDirImpl = do
224 |   Just s <- currentDir | Nothing => pure (Left CurDir)
225 |   case fromString {ty = FilePath} s of
226 |     (FP p@(PAbs {})) => pure (Right p)
227 |     _                => pure (Left CurDir)
228 |
229 | changeDirImpl : (0 t : _) -> Path t -> IO (Either FileErr ())
230 | changeDirImpl _ dir = do
231 |   True <- changeDir "\{dir}" | False => pure (Left $ ChangeDir dir)
232 |   pure $ Right ()
233 |
234 | listDirImpl : (0 t : _) -> Path t -> IO (Either FileErr $ List Body)
235 | listDirImpl _ dir = bimap (ListDir dir) (mapMaybe parse) <$> listDir "\{dir}"
236 |
237 | mkDirImpl : (0 t : _) -> Path t -> IO (Either FileErr ())
238 | mkDirImpl _ dir = mapFst (MkDir dir) <$> createDir "\{dir}"
239 |
240 | ||| A computer's local file system
241 | export
242 | local : FS
243 | local =
244 |   MkFS
245 |     { write_      = writeImpl
246 |     , append_     = appendImpl
247 |     , removeFile_ = removeFileImpl
248 |     , removeDir_  = removeDirImpl
249 |     , exists_     = \_,fp => exists "\{fp}"
250 |     , read_       = readImpl
251 |     , curDir_     = curDirImpl
252 |     , changeDir_  = changeDirImpl
253 |     , listDir_    = listDirImpl
254 |     , mkDir_      = mkDirImpl
255 |     }
256 |