xref: /haiku/src/apps/haikudepot/util/StorageUtils.cpp (revision 1978089f7cec856677e46204e992c7273d70b9af)
1 /*
2  * Copyright 2017-2023, Andrew Lindesay <apl@lindesay.co.nz>.
3  * All rights reserved. Distributed under the terms of the MIT License.
4  */
5 
6 #include "StorageUtils.h"
7 
8 #include <errno.h>
9 #include <stdlib.h>
10 #include <vector>
11 #include <algorithm>
12 
13 #include <Directory.h>
14 #include <File.h>
15 #include <FindDirectory.h>
16 #include <Entry.h>
17 #include <String.h>
18 
19 #include "HaikuDepotConstants.h"
20 #include "Logger.h"
21 
22 #define FILE_TO_STRING_BUFFER_LEN 64
23 
24 
25 static bool sAreWorkingFilesAvailable = true;
26 
27 
28 class PathWithLastAccessTimestamp {
29 public:
30 	PathWithLastAccessTimestamp(const BPath path, uint64 lastAccessMillisSinceEpoch)
31 		:
32 		fPath(path),
33 		fLastAccessMillisSinceEpoch(lastAccessMillisSinceEpoch)
34 	{
35 	}
36 
37 	~PathWithLastAccessTimestamp()
38 	{
39 	}
40 
41 	const BPath& Path() const
42 	{
43 		return fPath;
44 	}
45 
46 	int64 LastAccessMillisSinceEpoch() const
47 	{
48 		return fLastAccessMillisSinceEpoch;
49 	}
50 
51 	BString String() const
52 	{
53 		BString result;
54 		result.SetToFormat("%s; @ %" B_PRIu64, fPath.Leaf(), fLastAccessMillisSinceEpoch);
55 		return result;
56 	}
57 
58 	bool operator<(const PathWithLastAccessTimestamp& other) const
59     {
60     	return fLastAccessMillisSinceEpoch < other.fLastAccessMillisSinceEpoch &&
61     		strcmp(fPath.Path(), other.fPath.Path()) < 0;
62     }
63 
64 private:
65 	BPath fPath;
66 	uint64 fLastAccessMillisSinceEpoch;
67 };
68 
69 
70 /*static*/ bool
71 StorageUtils::AreWorkingFilesAvailable()
72 {
73 	return sAreWorkingFilesAvailable;
74 }
75 
76 
77 /*static*/ void
78 StorageUtils::SetWorkingFilesUnavailable()
79 {
80 	sAreWorkingFilesAvailable = false;
81 }
82 
83 
84 /* This method will append the contents of the file at the supplied path to the
85  * string provided.
86  */
87 
88 /*static*/ status_t
89 StorageUtils::AppendToString(const BPath& path, BString& result)
90 {
91 	BFile file(path.Path(), O_RDONLY);
92 	uint8_t buffer[FILE_TO_STRING_BUFFER_LEN];
93 	size_t buffer_read;
94 
95 	while((buffer_read = file.Read(buffer, FILE_TO_STRING_BUFFER_LEN)) > 0)
96 		result.Append((char *) buffer, buffer_read);
97 
98 	return (status_t) buffer_read;
99 }
100 
101 
102 /*static*/ status_t
103 StorageUtils::AppendToFile(const BString& input, const BPath& path)
104 {
105 	BFile file(path.Path(), O_WRONLY | O_CREAT | O_APPEND);
106 	const char* cstr = input.String();
107 	size_t cstrLen = strlen(cstr);
108 	return file.WriteExactly(cstr, cstrLen);
109 }
110 
111 
112 /*static*/ status_t
113 StorageUtils::RemoveWorkingDirectoryContents()
114 {
115 	BPath path;
116 	status_t result = B_OK;
117 
118 	if (result == B_OK)
119 		result = find_directory(B_USER_CACHE_DIRECTORY, &path);
120 	if (result == B_OK)
121 		result = path.Append(CACHE_DIRECTORY_APP);
122 
123 	bool exists;
124 	bool isDirectory;
125 
126 	if (result == B_OK)
127 		result = ExistsObject(path, &exists, &isDirectory, NULL);
128 
129 	if (result == B_OK && exists && !isDirectory) {
130 		HDERROR("the working directory at [%s] is not a directory",
131 			path.Path());
132 		result = B_ERROR;
133 	}
134 
135 	if (result == B_OK && exists)
136 		result = RemoveDirectoryContents(path);
137 
138 	return result;
139 }
140 
141 
142 /* This method will traverse the directory structure and will remove all of the
143  * files that are present in the directories as well as the directories
144  * themselves.
145  */
146 
147 status_t
148 StorageUtils::RemoveDirectoryContents(BPath& path)
149 {
150 	BDirectory directory(path.Path());
151 	BEntry directoryEntry;
152 	status_t result = B_OK;
153 
154 	while (result == B_OK &&
155 		directory.GetNextEntry(&directoryEntry) != B_ENTRY_NOT_FOUND) {
156 
157 		bool exists = false;
158 		bool isDirectory = false;
159 		BPath directoryEntryPath;
160 
161 		result = directoryEntry.GetPath(&directoryEntryPath);
162 
163 		if (result == B_OK) {
164 			result = ExistsObject(directoryEntryPath, &exists, &isDirectory,
165 				NULL);
166 		}
167 
168 		if (result == B_OK) {
169 			if (isDirectory)
170 				RemoveDirectoryContents(directoryEntryPath);
171 
172 			if (remove(directoryEntryPath.Path()) == 0) {
173 				HDDEBUG("did delete contents under [%s]",
174 					directoryEntryPath.Path());
175 			} else {
176 				HDERROR("unable to delete [%s]", directoryEntryPath.Path());
177 				result = B_ERROR;
178 			}
179 		}
180 
181 	}
182 
183 	return result;
184 }
185 
186 
187 /*! This function will delete all of the files in a directory except for the most
188 	recent `countLatestRetained` files.
189 */
190 
191 /*static*/ status_t
192 StorageUtils::RemoveDirectoryContentsRetainingLatestFiles(BPath& path, uint32 countLatestRetained)
193 {
194 	std::vector<PathWithLastAccessTimestamp> pathAndTimestampses;
195 	BDirectory directory(path.Path());
196 	BEntry directoryEntry;
197 	status_t result = B_OK;
198 	struct stat s;
199 
200 	while (result == B_OK &&
201 		directory.GetNextEntry(&directoryEntry) != B_ENTRY_NOT_FOUND) {
202 		BPath directoryEntryPath;
203 		result = directoryEntry.GetPath(&directoryEntryPath);
204 
205 		if (result == B_OK) {
206 			if (-1 == stat(directoryEntryPath.Path(), &s))
207 				result = B_ERROR;
208 		}
209 
210 		if (result == B_OK) {
211 			pathAndTimestampses.push_back(PathWithLastAccessTimestamp(
212 				directoryEntryPath,
213 				(static_cast<uint64>(s.st_atim.tv_sec) * 1000) + (static_cast<uint64>(s.st_atim.tv_nsec) / 1000)));
214 		}
215 	}
216 
217 	if (pathAndTimestampses.size() > countLatestRetained) {
218 
219 		// sort the list with the oldest files first (smallest fLastAccessMillisSinceEpoch)
220 		std::sort(pathAndTimestampses.begin(), pathAndTimestampses.end());
221 
222 		std::vector<PathWithLastAccessTimestamp>::iterator it;
223 
224 		if (Logger::IsTraceEnabled()) {
225 			for (it = pathAndTimestampses.begin(); it != pathAndTimestampses.end(); it++) {
226 				PathWithLastAccessTimestamp pathAndTimestamp = *it;
227 				HDTRACE("delete candidate [%s]", pathAndTimestamp.String().String());
228 			}
229 		}
230 
231 		for (it = pathAndTimestampses.begin(); it != pathAndTimestampses.end() - countLatestRetained; it++) {
232 			PathWithLastAccessTimestamp pathAndTimestamp = *it;
233 			const char* pathStr = pathAndTimestamp.Path().Path();
234 
235 			if (remove(pathStr) == 0)
236 				HDDEBUG("did delete [%s]", pathStr);
237 			else {
238 				HDERROR("unable to delete [%s]", pathStr);
239 				result = B_ERROR;
240 			}
241 		}
242 	}
243 
244 	return result;
245 }
246 
247 
248 /* This method checks to see if a file object exists at the path specified.  If
249  * something does exist then the value of the 'exists' pointer is set to true.
250  * If the object is a directory then this value is also set to true.
251  */
252 
253 status_t
254 StorageUtils::ExistsObject(const BPath& path,
255 	bool* exists,
256 	bool* isDirectory,
257 	off_t* size)
258 {
259 	struct stat s;
260 
261 	if (exists != NULL)
262 		*exists = false;
263 
264 	if (isDirectory != NULL)
265 		*isDirectory = false;
266 
267 	if (size != NULL)
268 		*size = 0;
269 
270 	if (-1 == stat(path.Path(), &s)) {
271 		if (ENOENT != errno)
272 			 return B_ERROR;
273 	} else {
274 		if (exists != NULL)
275 			*exists = true;
276 
277 		if (isDirectory != NULL)
278 			*isDirectory = S_ISDIR(s.st_mode);
279 
280 		if (size != NULL)
281 			*size = s.st_size;
282 	}
283 
284 	return B_OK;
285 }
286 
287 
288 /*! This method will check that it is possible to write to the specified file.
289     This may create the file, write some data to it and then read that data
290     back again to be sure.  This can be used as an effective safety measure as
291     the application starts up in order to ensure that the storage systems are
292     in place for the application to startup.
293 
294     It is assumed here that the directory containing the test file exists.
295 */
296 
297 /*static*/ status_t
298 StorageUtils::CheckCanWriteTo(const BPath& path)
299 {
300 	status_t result = B_OK;
301 	bool exists = false;
302 	uint8 buffer[16];
303 
304 	// create some random latin letters into the buffer to write.
305 	for (int i = 0; i < 16; i++)
306 		buffer[i] = 65 + (abs(rand()) % 26);
307 
308 	if (result == B_OK)
309 		result = ExistsObject(path, &exists, NULL, NULL);
310 
311 	if (result == B_OK && exists) {
312 		HDTRACE("an object exists at the candidate path "
313 			"[%s] - it will be deleted", path.Path());
314 
315 		if (remove(path.Path()) == 0) {
316 			HDTRACE("did delete the candidate file [%s]", path.Path());
317 		} else {
318 			HDERROR("unable to delete the candidate file [%s]", path.Path());
319 			result = B_ERROR;
320 		}
321 	}
322 
323 	if (result == B_OK) {
324 		BFile file(path.Path(), O_WRONLY | O_CREAT);
325 		if (file.Write(buffer, 16) != 16) {
326 			HDERROR("unable to write test data to candidate file [%s]",
327 				path.Path());
328 			result = B_ERROR;
329 		}
330 	}
331 
332 	if (result == B_OK) {
333 		BFile file(path.Path(), O_RDONLY);
334 		uint8 readBuffer[16];
335 		if (file.Read(readBuffer, 16) != 16) {
336 			HDERROR("unable to read test data from candidate file [%s]",
337 				path.Path());
338 			result = B_ERROR;
339 		}
340 
341 		for (int i = 0; result == B_OK && i < 16; i++) {
342 			if (readBuffer[i] != buffer[i]) {
343 				HDERROR("mismatched read..write check on candidate file [%s]",
344 					path.Path());
345 				result = B_ERROR;
346 			}
347 		}
348 	}
349 
350 	return result;
351 }
352 
353 
354 /*! As the application runs it will need to store some files into the local
355     disk system.  This method, given a leafname, will write into the supplied
356     path variable, a final path where this leafname should be stored.
357 */
358 
359 /*static*/ status_t
360 StorageUtils::LocalWorkingFilesPath(const BString leaf, BPath& path,
361 	bool failOnCreateDirectory)
362 {
363 	BPath resultPath;
364 	status_t result = B_OK;
365 
366 	if (result == B_OK)
367 		result = find_directory(B_USER_CACHE_DIRECTORY, &resultPath);
368 
369 	if (result == B_OK)
370 		result = resultPath.Append(CACHE_DIRECTORY_APP);
371 
372 	if (result == B_OK) {
373 		if (failOnCreateDirectory)
374 			result = create_directory(resultPath.Path(), 0777);
375 		else
376 			create_directory(resultPath.Path(), 0777);
377 	}
378 
379 	if (result == B_OK)
380 		result = resultPath.Append(leaf);
381 
382 	if (result == B_OK)
383 		path.SetTo(resultPath.Path());
384 	else {
385 		path.Unset();
386 		HDERROR("unable to find the user cache file for "
387 			"[%s] data; %s", leaf.String(), strerror(result));
388 	}
389 
390 	return result;
391 }
392 
393 
394 /*static*/ status_t
395 StorageUtils::LocalWorkingDirectoryPath(const BString leaf, BPath& path,
396 	bool failOnCreateDirectory)
397 {
398 	BPath resultPath;
399 	status_t result = B_OK;
400 
401 	if (result == B_OK)
402 		result = find_directory(B_USER_CACHE_DIRECTORY, &resultPath);
403 
404 	if (result == B_OK)
405 		result = resultPath.Append(CACHE_DIRECTORY_APP);
406 
407 	if (result == B_OK)
408 		result = resultPath.Append(leaf);
409 
410 	if (result == B_OK) {
411 		if (failOnCreateDirectory)
412 			result = create_directory(resultPath.Path(), 0777);
413 		else
414 			create_directory(resultPath.Path(), 0777);
415 	}
416 
417 	if (result == B_OK)
418 		path.SetTo(resultPath.Path());
419 	else {
420 		path.Unset();
421 		HDERROR("unable to find the user cache directory for "
422 			"[%s] data; %s", leaf.String(), strerror(result));
423 	}
424 
425 	return result;
426 }
427 
428 
429 /*static*/ status_t
430 StorageUtils::SwapExtensionOnPath(BPath& path, const char* extension)
431 {
432 	BPath parent;
433 	status_t result = path.GetParent(&parent);
434 	if (result == B_OK) {
435 		path.SetTo(parent.Path(),
436 			SwapExtensionOnPathComponent(path.Leaf(), extension).String());
437 	}
438 	return result;
439 }
440 
441 
442 /*static*/ BString
443 StorageUtils::SwapExtensionOnPathComponent(const char* pathComponent,
444 	const char* extension)
445 {
446 	BString result(pathComponent);
447 	int32 lastDot = result.FindLast(".");
448 	if (lastDot != B_ERROR) {
449 		result.Truncate(lastDot);
450 	}
451 	result.Append(".");
452 	result.Append(extension);
453 	return result;
454 }
455