;;;; The file surface beyond slurp and barf: file-exists?, file-size, ;;;; delete-file, rename-file and make-directory. ;;;; ;;;; The split down the middle of that list is the whole design and this file ;;;; is arranged to show it. The two that ask a *question* — is it there, how ;;;; big is it — answer a value, because absence is a reply and not a fault; ;;;; they are prelude functions over one declare and the compiler knows ;;;; nothing about them. The three that *change* the filesystem answer () and ;;;; signal FileError with the two restarts slurp and barf already establish, ;;;; because each of their failures is one a handler can act on: make the ;;;; parent directory and retry, or supply another path. ;;;; ;;;; Everything is made and removed inside this program, so it leaves the ;;;; directory as it found it — checked at the end rather than assumed. ;; Handlers cannot see the locals of the function that established them, so the ;; observations are globals, as in slurp.flan. (defvar seen i64) (defvar last-reason i32) (defvar last-op i32) (defn main [] i32 ;; ── The questions ───────────────────────────────────────────────── (println (file-exists? "programs/assets/a.txt")) ; true (println (file-exists? "programs/assets/nope")) ; false ;; A directory resolves, which is what the name asks and not "is a regular ;; file" — a caller wanting the narrower question is asking a second one. (println (file-exists? "programs/assets")) ; true (match (file-size "programs/assets/a.txt") (Some n) (println n) ; 13 None (println "missing")) ;; None folds every reason into one answer, which is the trade a question ;; with no restart on it makes. (match (file-size "programs/assets/nope") (Some n) (println n) None (println "none")) ;; ── make-directory, rename-file, delete-file ────────────────────── (make-directory "files-tmp") (println (file-exists? "files-tmp")) ; true (barf "files-tmp/one.txt" (bytes "0123456789")) (match (file-size "files-tmp/one.txt") (Some n) (println n) ; 10 None (println "missing")) (rename-file "files-tmp/one.txt" "files-tmp/two.txt") (println (file-exists? "files-tmp/one.txt")) ; false (println (file-exists? "files-tmp/two.txt")) ; true (delete-file "files-tmp/two.txt") (println (file-exists? "files-tmp/two.txt")) ; false ;; ── retry, after the handler made the parent ────────────────────── ;; The restart this family exists for. Writing into a directory that is not ;; there is ENOENT, which arrives as `missing`; the handler makes the ;; directory and takes `retry`, and the second attempt succeeds. Nothing in ;; the failing code knows any of that happened. (handler-bind [(FileError [c] (set seen (+ seen 1)) (set last-reason (.reason c)) (set last-op (.op c)) (make-directory "files-tmp/sub") (invoke-restart 'retry))] (barf "files-tmp/sub/deep.txt" (bytes "deep"))) (println seen) ; 1 (println (= last-reason file-missing)) ; true (println (= last-op file-op-write)) ; true (println (file-exists? "files-tmp/sub/deep.txt")) ; true ;; ── use-value, on a delete ──────────────────────────────────────── ;; The same restart slurp's read offers, on an operation that writes: the ;; handler names a path that is there and the delete resumes against it. (set seen 0) (handler-bind [(FileError [c] (set seen (+ seen 1)) (set last-op (.op c)) (invoke-restart 'use-value "files-tmp/sub/deep.txt"))] (delete-file "files-tmp/sub/not-there.txt")) (println seen) ; 1 (println (= last-op file-op-delete)) ; true (println (file-exists? "files-tmp/sub/deep.txt")) ; false ;; ── A non-empty directory does not delete ───────────────────────── ;; remove() is unlink or rmdir depending on what the path names, so an empty ;; directory goes by the same call a file does — and a full one does not, ;; which is deliberate: a recursive delete is a loop the caller writes and ;; sees. Here the handler declines to answer, which is what an unhandled ;; condition would do, so it counts and lets the program carry on by ;; supplying the child path instead. (set seen 0) (handler-bind [(FileError [c] (set seen (+ seen 1)) (set last-op (.op c)) (invoke-restart 'use-value "files-tmp/sub"))] (delete-file "files-tmp")) (println seen) ; 1 (println (= last-op file-op-delete)) ; true ;; And now it is empty, so it goes. (delete-file "files-tmp") (println (file-exists? "files-tmp")) ; false 0)