feat: create temporary directories (#6148)

This PR adds a primitive for creating temporary directories, akin to the
existing functionality for creating temporary files.
This commit is contained in:
David Thrane Christiansen 2024-11-22 13:24:32 +01:00 committed by GitHub
parent a19ff61e15
commit 1126407d9b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 179 additions and 19 deletions

View file

@ -462,6 +462,16 @@ Note that it is the caller's job to remove the file after use.
-/
@[extern "lean_io_create_tempfile"] opaque createTempFile : IO (Handle × FilePath)
/--
Creates a temporary directory in the most secure manner possible. There are no race conditions in the
directorys creation. The directory is readable and writable only by the creating user ID.
Returns the new directory's path.
It is the caller's job to remove the directory after use.
-/
@[extern "lean_io_create_tempdir"] opaque createTempDir : IO FilePath
end FS
@[extern "lean_io_getenv"] opaque getEnv (var : @& String) : BaseIO (Option String)
@ -474,17 +484,6 @@ namespace FS
def withFile (fn : FilePath) (mode : Mode) (f : Handle → IO α) : IO α :=
Handle.mk fn mode >>= f
/--
Like `createTempFile` but also takes care of removing the file after usage.
-/
def withTempFile [Monad m] [MonadFinally m] [MonadLiftT IO m] (f : Handle → FilePath → m α) :
m α := do
let (handle, path) ← createTempFile
try
f handle path
finally
removeFile path
def Handle.putStrLn (h : Handle) (s : String) : IO Unit :=
h.putStr (s.push '\n')
@ -675,8 +674,10 @@ def appDir : IO FilePath := do
| throw <| IO.userError s!"System.IO.appDir: unexpected filename '{p}'"
FS.realPath p
namespace FS
/-- Create given path and all missing parents as directories. -/
partial def FS.createDirAll (p : FilePath) : IO Unit := do
partial def createDirAll (p : FilePath) : IO Unit := do
if ← p.isDir then
return ()
if let some parent := p.parent then
@ -693,7 +694,7 @@ partial def FS.createDirAll (p : FilePath) : IO Unit := do
/--
Fully remove given directory by deleting all contained files and directories in an unspecified order.
Fails if any contained entry cannot be deleted or was newly created during execution. -/
partial def FS.removeDirAll (p : FilePath) : IO Unit := do
partial def removeDirAll (p : FilePath) : IO Unit := do
for ent in (← p.readDir) do
if (← ent.path.isDir : Bool) then
removeDirAll ent.path
@ -701,6 +702,32 @@ partial def FS.removeDirAll (p : FilePath) : IO Unit := do
removeFile ent.path
removeDir p
/--
Like `createTempFile`, but also takes care of removing the file after usage.
-/
def withTempFile [Monad m] [MonadFinally m] [MonadLiftT IO m] (f : Handle → FilePath → m α) :
m α := do
let (handle, path) ← createTempFile
try
f handle path
finally
removeFile path
/--
Like `createTempDir`, but also takes care of removing the directory after usage.
All files in the directory are recursively deleted, regardless of how or when they were created.
-/
def withTempDir [Monad m] [MonadFinally m] [MonadLiftT IO m] (f : FilePath → m α) :
m α := do
let path ← createTempDir
try
f path
finally
removeDirAll path
end FS
namespace Process
/-- Returns the current working directory of the calling process. -/

View file

@ -1140,6 +1140,7 @@ extern "C" LEAN_EXPORT obj_res lean_io_create_tempfile(lean_object * /* w */) {
strcat(path, file_pattern);
uv_fs_t req;
// Differences from lean_io_create_tempdir start here
ret = uv_fs_mkstemp(NULL, &req, path, NULL);
if (ret < 0) {
// If mkstemp throws an error we cannot rely on path to contain a proper file name.
@ -1151,6 +1152,48 @@ extern "C" LEAN_EXPORT obj_res lean_io_create_tempfile(lean_object * /* w */) {
}
}
/* createTempDir : IO FilePath */
extern "C" LEAN_EXPORT obj_res lean_io_create_tempdir(lean_object * /* w */) {
char path[PATH_MAX];
size_t base_len = PATH_MAX;
int ret = uv_os_tmpdir(path, &base_len);
if (ret < 0) {
return io_result_mk_error(decode_uv_error(ret, nullptr));
} else if (base_len == 0) {
return lean_io_result_mk_error(decode_uv_error(UV_ENOENT, mk_string("")));
}
#if defined(LEAN_WINDOWS)
// On Windows `GetTempPathW` always returns a path ending in \, but libuv removes it.
// https://learn.microsoft.com/en-us/windows/win32/fileio/creating-and-using-a-temporary-file
if (path[base_len - 1] != '\\') {
lean_always_assert(PATH_MAX >= base_len + 1 + 1);
strcat(path, "\\");
}
#else
// No guarantee that we have a trailing / in TMPDIR.
if (path[base_len - 1] != '/') {
lean_always_assert(PATH_MAX >= base_len + 1 + 1);
strcat(path, "/");
}
#endif
const char* file_pattern = "tmp.XXXXXXXX";
const size_t file_pattern_size = strlen(file_pattern);
lean_always_assert(PATH_MAX >= strlen(path) + file_pattern_size + 1);
strcat(path, file_pattern);
uv_fs_t req;
// Differences from lean_io_create_tempfile start here
ret = uv_fs_mkdtemp(NULL, &req, path, NULL);
if (ret < 0) {
// If mkdtemp throws an error we cannot rely on path to contain a proper file name.
return io_result_mk_error(decode_uv_error(ret, nullptr));
} else {
return lean_io_result_mk_ok(mk_string(req.path));
}
}
extern "C" LEAN_EXPORT obj_res lean_io_remove_file(b_obj_arg fname, obj_arg) {
if (std::remove(string_cstr(fname)) == 0) {
return io_result_mk_ok(box(0));

View file

@ -1,10 +1,100 @@
/-!
# Temporary Files
These tests check that temporary files and directories can be created and used.
-/
/--
Tests temporary file creation.
-/
def test : IO Unit := do
let (handle, path) ← IO.FS.createTempFile
let toWrite := "Hello World"
handle.putStr toWrite
let handle2 ← IO.FS.Handle.mk path .read
let content ← handle2.getLine
assert! (content == toWrite)
IO.FS.removeFile path
try
let toWrite := "Hello World"
handle.putStr toWrite
handle.flush
let handle2 ← IO.FS.Handle.mk path .read
let content ← handle2.getLine
assert! (content == toWrite)
finally
IO.FS.removeFile path
#eval test
/--
Tests temporary file helper.
-/
def testWithFile : IO Unit := do
let pathRef ← IO.mkRef none
IO.FS.withTempFile fun handle path => do
pathRef.set (some path)
assert! (← path.pathExists)
let toWrite := "Hello World"
handle.putStr toWrite
handle.flush
let handle2 ← IO.FS.Handle.mk path .read
let content ← handle2.getLine
assert! (content == toWrite)
match (← pathRef.get) with
| none => assert! false
| some p => assert! (! (← p.pathExists))
#eval testWithFile
/--
Tests temporary directory creation and ensures that files can be created in it.
-/
def testDir : IO Unit := do
let path ← IO.FS.createTempDir
try
assert! (← path.isDir)
let fileList ← path.readDir
assert! (fileList.isEmpty)
let toWrite := "Hello World"
for i in [0:3] do
IO.FS.withFile (path / s!"{i}.txt") .write fun h => do
h.putStr toWrite
h.putStr (toString i)
for i in [0:3] do
IO.FS.withFile (path / s!"{i}.txt") .read fun h => do
let content ← h.getLine
assert! (content == toWrite ++ toString i)
let fileList := ((← path.readDir).map (·.fileName)).qsortOrd
assert! (fileList == #["0.txt", "1.txt", "2.txt"])
finally
IO.FS.removeDirAll path
#eval testDir
/--
Tests temporary directory helper.
-/
def testWithDir : IO Unit := do
let pathRef ← IO.mkRef none
IO.FS.withTempDir fun path => do
pathRef.set (some path)
assert! (← path.isDir)
let fileList ← path.readDir
assert! (fileList.isEmpty)
let toWrite := "Hello World"
for i in [0:3] do
IO.FS.withFile (path / s!"{i}.txt") .write fun h => do
h.putStr toWrite
h.putStr (toString i)
for i in [0:3] do
IO.FS.withFile (path / s!"{i}.txt") .read fun h => do
let content ← h.getLine
assert! (content == toWrite ++ toString i)
let fileList := ((← path.readDir).map (·.fileName)).qsortOrd
assert! (fileList == #["0.txt", "1.txt", "2.txt"])
match (← pathRef.get) with
| none => assert! false
| some p => assert! (! (← p.pathExists))
#eval testWithDir