Skip to content

Commit 2efd05d

Browse files
committed
Add create_files
1 parent 0448e71 commit 2efd05d

File tree

4 files changed

+161
-0
lines changed

4 files changed

+161
-0
lines changed

src/ParallelProcessingTools.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import ThreadPinning
1515

1616
using Parameters: @with_kw
1717

18+
include("fileio.jl")
1819
include("threadsafe.jl")
1920
include("threadlocal.jl")
2021
include("onthreads.jl")

src/fileio.jl

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
# This file is a part of ParallelProcessingTools.jl, licensed under the MIT License (MIT).
2+
3+
4+
"""
5+
ParallelProcessingTools.tmp_filename(fname::AbstractString)
6+
7+
Returns a temporary filename, based on `fname`, in the same directory.
8+
9+
Does *not* create the temporary file.
10+
"""
11+
function tmp_filename(fname::AbstractString)
12+
d, fn, ext = _split_dir_fn_ext(fname)
13+
tag = _rand_fname_tag()
14+
joinpath(d, "$(fn)_$(tag)$(ext)")
15+
end
16+
17+
function _split_dir_fn_ext(fname::AbstractString)
18+
d = dirname(fname)
19+
f = basename(fname)
20+
ext_startpos = findfirst('.', f)
21+
fn, ext = isnothing(ext_startpos) ? (f, "") : (f[1:ext_startpos-1], f[ext_startpos:end])
22+
return d, fn, ext
23+
end
24+
25+
_rand_fname_tag() = String(rand(b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", 8))
26+
27+
28+
"""
29+
function create_files(
30+
body, filenames::AbstractString...;
31+
create_dirs::Bool = true, overwrite::Bool = true, delete_on_error::Bool=true
32+
)
33+
34+
Creates `filenames` in an atomic fashion.
35+
36+
Creates temporary files in the same directories as `filenames`, then
37+
calls `body(temporary_filenames...)`. If `body` returns successfully,
38+
the files `temporary_filenames` are renamed to `filenames`. If `body` throws
39+
an exception, the temporary files are either deleted (if `delete_on_error` is
40+
`true`) or left in place (e.g. for debugging purposes).
41+
42+
If `create_dirs` is `true`, directories are created if necessary.
43+
44+
If all of files already exist and `overwrite` is `false`, takes no action
45+
(or, if the file is created by other code running in parallel, while `body` is
46+
running, does not overwrite it).
47+
48+
Throws an error if only some of the files exist and `overwrite` is `false`.
49+
50+
Returns `nothing`.
51+
52+
Example:
53+
54+
```julia
55+
create_files("foo.txt", "bar.txt") do foo, bar
56+
write(foo, "Hello")
57+
write(bar, "World")
58+
end
59+
```
60+
"""
61+
function create_files(
62+
body, filenames::AbstractString...;
63+
create_dirs::Bool = true, overwrite::Bool = true, delete_on_error::Bool=true
64+
)
65+
tmp_filenames = String[]
66+
completed_filenames = String[]
67+
68+
pre_existing = isfile.(filenames)
69+
if any(pre_existing)
70+
if all(pre_existing)
71+
if !overwrite
72+
@info "Files $filenames already exist, nothing to do."
73+
return nothing
74+
end
75+
else
76+
!overwrite && throw(ErrorException("Only some of $filenames exist but not allowed to overwrite"))
77+
end
78+
end
79+
80+
dirs = dirname.(filenames)
81+
for dir in dirs
82+
if !isdir(dir) && create_dirs
83+
mkpath(dir)
84+
@info "Created directory $dir."
85+
end
86+
end
87+
88+
try
89+
for fname in filenames
90+
tmp_fname = tmp_filename(fname)
91+
@assert !isfile(tmp_fname)
92+
push!(tmp_filenames, tmp_fname)
93+
end
94+
95+
body(tmp_filenames...)
96+
97+
post_body_existing = isfile.(filenames)
98+
if any(post_body_existing)
99+
if all(post_body_existing)
100+
if !overwrite
101+
@info "Files $filenames already exist, won't replace."
102+
return nothing
103+
end
104+
else
105+
!overwrite && throw(ErrorException("Only some of $filenames exist but not allowed to replace files"))
106+
end
107+
end
108+
109+
try
110+
for (tmp_fname, fname) in zip(tmp_filenames, filenames)
111+
mv(tmp_fname, fname; force=true)
112+
@assert isfile(fname)
113+
push!(completed_filenames, fname)
114+
end
115+
@info "Successfully created files $filenames."
116+
catch
117+
if !isempty(completed_filenames)
118+
@error "Failed to rename some temporary files to final filenames, removing $completed_filenames"
119+
for fname in completed_filenames
120+
rm(fname; force=true)
121+
end
122+
end
123+
rethrow()
124+
end
125+
126+
@assert all(fn -> !isfile(fn), tmp_filenames)
127+
finally
128+
if delete_on_error
129+
for tmp_fname in tmp_filenames
130+
isfile(tmp_fname) && rm(tmp_fname; force=true);
131+
end
132+
end
133+
end
134+
135+
return nothing
136+
end
137+
export create_files

test/runtests.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ Test.@testset "Package ParallelProcessingTools" begin
66
@info "Testing with $(Base.Threads.nthreads()) Julia threads."
77

88
include("test_aqua.jl")
9+
include("test_fileio.jl")
910
include("test_threadsafe.jl")
1011
include("test_threadlocal.jl")
1112
include("test_workpartition.jl")

test/test_fileio.jl

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# This file is a part of ParallelProcessingTools.jl, licensed under the MIT License (MIT).
2+
3+
using Test
4+
using ParallelProcessingTools
5+
6+
7+
@testset "fileio" begin
8+
mktempdir() do dir
9+
data1 = "Hello"
10+
data2 = "World"
11+
12+
fn1 = joinpath(dir, "hello.txt")
13+
fn2 = joinpath(dir, "world.txt")
14+
15+
create_files(fn1, fn2) do fn1, fn2
16+
write(fn1, data1)
17+
write(fn2, data2)
18+
end
19+
20+
@test read(fn1, String) == data1 && read(fn2, String) == data2
21+
end
22+
end

0 commit comments

Comments
 (0)