Line data Source code
1 : // Copyright (c) 2013-2023 Made to Order Software Corp. All Rights Reserved
2 : //
3 : // https://snapwebsites.org/project/snapdev
4 : // contact@m2osw.com
5 : //
6 : // This program is free software: you can redistribute it and/or modify
7 : // it under the terms of the GNU General Public License as published by
8 : // the Free Software Foundation, either version 3 of the License, or
9 : // (at your option) any later version.
10 : //
11 : // This program is distributed in the hope that it will be useful,
12 : // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 : // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 : // GNU General Public License for more details.
15 : //
16 : // You should have received a copy of the GNU General Public License
17 : // along with this program. If not, see <https://www.gnu.org/licenses/>.
18 : #pragma once
19 :
20 : /** \file
21 : * \brief Function to remove directories and their contents.
22 : *
23 : * This function is similar to the `rm -r <path>` command line.
24 : *
25 : * Note that if you just want to remove a directory if it is not empty,
26 : * then simply use the `rmdir()` function.
27 : */
28 :
29 : // self
30 : //
31 : #include <snapdev/glob_to_list.h>
32 :
33 :
34 : // C++
35 : //
36 : #include <algorithm>
37 : #include <vector>
38 :
39 :
40 : // C
41 : //
42 : #include <sys/stat.h>
43 : #include <sys/types.h>
44 :
45 :
46 :
47 :
48 :
49 : namespace snapdev
50 : {
51 :
52 :
53 : /** \brief Mode for removal of special files.
54 : *
55 : * When using an `rm -rf ...` you end up deleting absolutely everything
56 : * and in some cases that may not quite be what you wanted to do. The
57 : * rm_r() function uses these values to determine how to handle
58 : * special files. It also uses a flag named `keep_going` to know whether
59 : * to stop on the very first error (default) or go on and delete as much
60 : * as possible before returning.
61 : */
62 : enum class special_file_t
63 : {
64 : /** \brief Generate an error if a special file exists.
65 : *
66 : * When the `special` parameter of the rm_r() function is set to this
67 : * value, the function runs a first sweep to see whether a special file
68 : * exists. If so, it fails early with EPERM and no file gets deleted.
69 : */
70 : SPECIAL_FILE_ERROR,
71 :
72 : /** \brief Ignore special files.
73 : *
74 : * By default, the rm_r() function generates an error if it files a
75 : * special file (a file other than a regular file or a directory).
76 : *
77 : * When using this option, the special files are kept in place.
78 : */
79 : SPECIAL_FILE_IGNORE,
80 :
81 : /** \brief Remove FIFOs and sockets, keep devices.
82 : *
83 : * This value can be used to request the deletion of regular files,
84 : * directories, FIFOs and sockets. However, device files (block and
85 : * character devices) are not deleted.
86 : */
87 : SPECIAL_FILE_KEEP_DEVICES,
88 :
89 : /** \brief Remove special files.
90 : *
91 : * The rm_r() can be asked to remove absolutely everything, including
92 : * all special files.
93 : */
94 : SPECIAL_FILE_REMOVE,
95 : };
96 :
97 :
98 : /** \brief Remove a directory as with `rm -r ...`.
99 : *
100 : * This function goes through the specified directory and remove all the
101 : * files including the directory itself. If `path` includes a pattern,
102 : * then all the corresponding files are deleted.
103 : *
104 : * When the function returns -1, the errno is set to what the command used
105 : * set it at (i.e EPERM, ENOTDIR, etc.)
106 : *
107 : * The \p special parameter is used to define the behavior of the function
108 : * when a special file is found in the directory tree. Here are the
109 : * different types and how they are handled depending on the \p special
110 : * parameter:
111 : *
112 : * * Regular Files and Symbolic Links
113 : *
114 : * These are always removed unless an error occurs before that specific
115 : * file is reached.
116 : *
117 : * * Directories
118 : *
119 : * These are removed if empty after the removal of other files (or they
120 : * started empty).
121 : *
122 : * If a special file is found inside a directory and that special file does
123 : * not get removed, then the directory is silently left behind.
124 : *
125 : * * FIFO and Sockets
126 : *
127 : * When the \p special parameter is set to
128 : * special_file_t::SPECIAL_FILE_ERROR, any FIFO or Socket found in the
129 : * directory tree prevents the deletion of any file. The function returns
130 : * with -1 and errno set to EPERM.
131 : *
132 : * When the \p special parameter is set to
133 : * special_file_t::SPECIAL_FILE_IGNORE, FIFO and Socket files and their
134 : * parent directories are all kept.
135 : *
136 : * When the \p special parameter is set to
137 : * special_file_t::SPECIAL_FILE_KEEP_DEVICES or
138 : * special_file_t::SPECIAL_FILE_REMOVE, FIFO and Socket files get removed.
139 : *
140 : * * Block and Character Device Files
141 : *
142 : * When the \p special parameter is set to
143 : * special_file_t::SPECIAL_FILE_ERROR, any FIFO or Socket found in the
144 : * directory tree prevents the deletion of any file. The function returns
145 : * with -1 and errno set to EPERM.
146 : *
147 : * When the \p special parameter is set to
148 : * special_file_t::SPECIAL_FILE_IGNORE or
149 : * special_file_t::SPECIAL_FILE_KEEP_DEVICES, Block and Character Device
150 : * files and their parent directories are all kept.
151 : *
152 : * When the \p special parameter is set to
153 : * special_file_t::SPECIAL_FILE_REMOVE, Block and Character Device files get
154 : * removed.
155 : *
156 : * The \p keep_going flag can be used to request the function to delete as
157 : * many files as possible even if errors are encountered.
158 : *
159 : * \warning
160 : * As mentioned above, the function may return 0 even though the resulting
161 : * path does not get deleted. This happens when special files are found and
162 : * kept behind.
163 : *
164 : * \param[in] pattern The path to remove. It can include a glob-like pattern.
165 : * \param[in] special How to handle special files.
166 : * \param[in] keep_going Try to delete as much as possible.
167 : *
168 : * \return 0 if the directory gets removed;
169 : * -1 if an error occurs (i.e. permissions denied).
170 : */
171 8 : inline int rm_r(
172 : std::string const & pattern
173 : , special_file_t special = special_file_t::SPECIAL_FILE_ERROR
174 : , bool keep_going = false)
175 : {
176 8 : snapdev::glob_to_list<std::vector<std::string>> glob;
177 8 : int e(0);
178 8 : std::string::size_type pos(0);
179 8 : struct stat st;
180 :
181 8 : std::string root;
182 8 : std::string extra;
183 8 : if(stat(pattern.c_str(), &st) == 0)
184 : {
185 6 : if(S_ISDIR(st.st_mode))
186 : {
187 6 : extra = "/*";
188 6 : root = glob.get_real_path(pattern);
189 : }
190 : }
191 :
192 : // get a list of the existing files
193 : //
194 8 : if(glob.read_path<
195 : snapdev::glob_to_list_flag_t::GLOB_FLAG_IGNORE_ERRORS,
196 : snapdev::glob_to_list_flag_t::GLOB_FLAG_BRACE,
197 : snapdev::glob_to_list_flag_t::GLOB_FLAG_PERIOD,
198 : snapdev::glob_to_list_flag_t::GLOB_FLAG_TILDE,
199 : snapdev::glob_to_list_flag_t::GLOB_FLAG_RECURSIVE,
200 8 : snapdev::glob_to_list_flag_t::GLOB_FLAG_EMPTY>(pattern + extra))
201 : {
202 : // also delete the root if it was not a pattern
203 : //
204 8 : if(!root.empty())
205 : {
206 6 : glob.push_back(root);
207 : }
208 :
209 : // sort the filenames, longest first since that represents the
210 : // deepest file in a given directory and that is the order in
211 : // which we need to delete all the files/directories
212 : //
213 8 : std::sort(
214 : glob.begin()
215 : , glob.end()
216 283 : , [](std::string const & lhs, std::string const & rhs)
217 : {
218 283 : return lhs.size() > rhs.size();
219 : });
220 :
221 : // do a first sweep to determine whether any special files exist
222 : // and if so return with -1 and errno = EPERM
223 : //
224 8 : if(special == special_file_t::SPECIAL_FILE_ERROR
225 5 : && !keep_going)
226 : {
227 19 : for(auto const & filename : glob)
228 : {
229 15 : if(stat(filename.c_str(), &st) == 0)
230 : {
231 15 : switch(st.st_mode & S_IFMT)
232 : {
233 14 : case S_IFDIR:
234 : case S_IFREG:
235 : case S_IFLNK:
236 14 : break;
237 :
238 1 : default: // devices, fifos, sockets, and unknown
239 1 : errno = EPERM;
240 1 : return -1;
241 :
242 : }
243 : }
244 0 : else if(errno != ENOENT)
245 : {
246 : // except for missing files stop on errors
247 : //
248 0 : return -1;
249 : }
250 : }
251 : }
252 :
253 : // now process the removal
254 : //
255 : // WARNING: I use an index because I may remove parent folders
256 : // and thus would mess up an `auto`
257 : //
258 52 : for(std::size_t i(0); i < glob.size(); ++i)
259 : {
260 45 : std::string filename(glob[i]);
261 45 : if(stat(filename.c_str(), &st) == 0)
262 : {
263 45 : int r(0);
264 45 : bool keep_parents(false);
265 45 : switch(st.st_mode & S_IFMT)
266 : {
267 9 : case S_IFDIR:
268 9 : r = rmdir(filename.c_str());
269 9 : break;
270 :
271 20 : case S_IFREG:
272 : case S_IFLNK:
273 20 : r = unlink(filename.c_str());
274 20 : break;
275 :
276 16 : case S_IFIFO:
277 : case S_IFSOCK:
278 16 : if(special == special_file_t::SPECIAL_FILE_KEEP_DEVICES
279 8 : || special == special_file_t::SPECIAL_FILE_REMOVE)
280 : {
281 8 : r = unlink(filename.c_str());
282 : }
283 : else
284 : {
285 8 : keep_parents = true;
286 :
287 8 : if(special == special_file_t::SPECIAL_FILE_ERROR
288 0 : && e == 0)
289 : {
290 0 : e = EPERM;
291 : }
292 : }
293 16 : break;
294 :
295 0 : case S_IFBLK:
296 : case S_IFCHR:
297 0 : if(special == special_file_t::SPECIAL_FILE_REMOVE)
298 : {
299 0 : r = unlink(filename.c_str());
300 : }
301 : else
302 : {
303 0 : keep_parents = true;
304 :
305 0 : if(special == special_file_t::SPECIAL_FILE_ERROR
306 0 : && e == 0)
307 : {
308 0 : e = EPERM;
309 : }
310 : }
311 0 : break;
312 :
313 0 : default:
314 : // unknown file type, do not mess with it
315 : //
316 0 : errno = EPERM;
317 0 : return -1;
318 :
319 : }
320 45 : if(r != 0)
321 : {
322 0 : if(!keep_going)
323 : {
324 0 : return -1;
325 : }
326 0 : if(e != 0)
327 : {
328 0 : e = errno;
329 : }
330 : }
331 45 : if(keep_parents)
332 : {
333 : // remove the parent directories since we won't be able
334 : // to delete them since we're "legally" keeping some
335 : // files in the child most directory
336 : //
337 : for(;;)
338 : {
339 94 : pos = filename.rfind('/');
340 94 : if(pos == std::string::npos)
341 : {
342 8 : break;
343 : }
344 86 : filename = filename.substr(0, pos);
345 1066 : for(std::size_t j(i + 1); j < glob.size(); ++j)
346 : {
347 984 : if(glob[j] == filename)
348 : {
349 4 : glob.erase(glob.begin() + j);
350 4 : break;
351 : }
352 : }
353 86 : }
354 : }
355 : }
356 0 : else if(errno != ENOENT)
357 : {
358 : // the stat() failed with an error other than:
359 : // "file does not exist"
360 : //
361 0 : if(!keep_going)
362 : {
363 0 : return -1;
364 : }
365 0 : if(e == 0)
366 : {
367 0 : e = errno;
368 : }
369 : }
370 45 : }
371 : }
372 : // else -- the directories/files do not exist, success
373 :
374 7 : if(e == 0)
375 : {
376 7 : return 0;
377 : }
378 :
379 : // case where keep_going is true and an error occurred
380 : //
381 0 : errno = e;
382 :
383 0 : return -1;
384 8 : }
385 :
386 :
387 :
388 : } // snap namespacedev
389 : // vim: ts=4 sw=4 et
|