1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
//! The atomic file replace, its first step and its last
//!
//! Stage a fresh file beside the real one, write it, `fsync` it, `rename`
//! it over the target, then `fsync` the directory the rename landed in. A
//! reader sees the whole old file or the whole new one, never a fragment.
//! [`create_staging_file`] is the first step, [`sync_dir`] the last, and
//! the middle stays with each store, which maps its failures onto its own
//! error type.
//!
//! [`sync_dir`] makes the *rename* durable, which the temp file's own
//! `fsync` does not. On unix, and there only where the filesystem
//! implements the flush: it is a no-op on Windows, and it answers `Ok` to
//! the `EINVAL` some FUSE and network mounts return instead of flushing.
use Path;
// `$SHEP_HOME` is already `0700`, but `shep.toml` and `dogs.toml` hold
// webhook URLs with a bearer token in the path, and a `tar` or `cp -p` of
// them carries this mode somewhere no directory mode follows.
/// Mode a file under `$SHEP_HOME` is created with: owner read/write,
/// nobody else.
///
/// # Platforms
///
/// Unix only. On Windows a file inherits the ACL of the directory it lands
/// in.
pub const OWNER_ONLY_FILE_MODE: u32 = 0o600;
// No mode parameter, because a caller that wanted a looser one would be
// wrong: every file here holds a credential or an `env` value.
//
// The unique middle is not tidiness. Two writers sharing a fixed
// `<path>.tmp` had one `rename` consume the other's staging file.
//
// `tempfile_in` joins a separator onto `parent`, so `../evil` escapes the
// one directory this is contracted to stay inside.
/// Creates the staging file a store is rewritten through, in `parent` so
/// the later `rename` stays within one filesystem.
///
/// `prefix` and `suffix` bracket a unique middle `tempfile` picks, and
/// neither may contain a path separator. The file is created
/// [`OWNER_ONLY_FILE_MODE`] on unix at the `open` itself, never by a later
/// `chmod`, so it is never briefly wider. It carries no mode on Windows.
///
/// The caller writes it, `sync_all`s it, and `persist`s it over the real
/// file.
///
/// # Errors
/// - [`std::io::ErrorKind::InvalidInput`] when `prefix` or `suffix`
/// contains `/` or `\`, both refused on both platforms.
/// - Otherwise `parent` is missing or unwritable, or `tempfile` ran out of
/// attempts at a unique name.
// The two flushes answer different questions and it is easy to buy one
// believing you bought both. `sync_all` on the staging file flushes its
// CONTENTS; the entry the rename creates is a change to the parent
// DIRECTORY, which sits in the page cache until that directory is flushed
// too. Lose power in between and the data survives with nothing pointing
// at it. A crash is not that case: a completed `rename(2)` is visible to
// every later process whether or not anything was flushed, so it takes an
// unclean shutdown (power cut, kernel panic, hypervisor reset) to undo one,
// and what comes back is then the old file or the new one, never a
// fragment. The muster roll needs the difference most, its whole job being
// read back after a reboot.
//
// Why the two arms differ, which is not a caller's question (IR-31).
//
// UNIX. `fsync` on a directory descriptor is the portable way to flush the
// entry a rename created, and `EINVAL` is tolerated because POSIX lets
// `fsync` answer it when the implementation has no synchronized I/O to
// perform for the file it was handed. Some FUSE and network mounts answer
// exactly that for a directory, and reporting it as a failed write would
// break writes that do land, on hosts where they land today. Every other
// error propagates: a helper that swallowed `EIO` could never tell a
// caller that the durability it asked for did not happen.
//
// WINDOWS. There is no call to make. `File::open` on a directory fails
// outright unless the handle carries `FILE_FLAG_BACKUP_SEMANTICS`, which
// `std` does not pass, so the unix arm would not even compile into
// something runnable. NTFS journals metadata operations, which keeps the
// filesystem CONSISTENT across a crash, and that is a weaker promise than
// the unix arm makes: `MoveFileEx` without `MOVEFILE_WRITE_THROUGH` does
// not force the rename out, so a power cut can still lose it. Closing that
// would mean reaching past `std` for a directory handle, or a
// write-through rename in place of `NamedTempFile::persist`. Neither is
// free and neither is done here, so the honest position is that the
// guarantee below is a unix one.
/// Flushes `dir`'s own metadata, making renames into it durable.
///
/// Call it after the `rename` that installs a staged file, not before: it
/// is the directory entry created by that rename that needs to reach the
/// disk. Callers that skip it keep the atomicity guarantee (a reader sees
/// the old file or the new one, never a fragment) and lose only the
/// durability one, and only to a power cut.
///
/// # Platforms
///
/// Unix only. On Windows this is a no-op that answers `Ok` without
/// touching `dir`, so a rename there is as durable as NTFS makes it and no
/// more. Callers get the same API on both and a weaker guarantee on one.
///
/// # Errors
///
/// - [`std::io::Error`] when `dir` could not be opened, or when flushing
/// it failed for a reason the filesystem could act on. `EINVAL` is not
/// one of them: it reads as "this filesystem has no such step" and the
/// write stands. Never returns an error on Windows.