GnuCash c935c2f+
Loading...
Searching...
No Matches
gnc-filepath-utils.cpp
1/********************************************************************\
2 * gnc-filepath-utils.c -- file path resolution utility *
3 * *
4 * This program is free software; you can redistribute it and/or *
5 * modify it under the terms of the GNU General Public License as *
6 * published by the Free Software Foundation; either version 2 of *
7 * the License, or (at your option) any later version. *
8 * *
9 * This program is distributed in the hope that it will be useful, *
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
12 * GNU General Public License for more details. *
13 * *
14 * You should have received a copy of the GNU General Public License*
15 * along with this program; if not, contact: *
16 * *
17 * Free Software Foundation Voice: +1-617-542-5942 *
18 * 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652 *
19 * Boston, MA 02110-1301, USA gnu@gnu.org *
20\********************************************************************/
21
22/*
23 * @file gnc-filepath-utils.c
24 * @brief file path resolution utilities
25 * @author Copyright (c) 1998-2004 Linas Vepstas <linas@linas.org>
26 * @author Copyright (c) 2000 Dave Peticolas
27 */
28
29#include <glib.h>
30#include <glib/gi18n.h>
31#include <glib/gprintf.h>
32#include <glib/gstdio.h>
33
34#include <config.h>
35
36#include <platform.h>
37#if PLATFORM(WINDOWS)
38#include <windows.h>
39#include <Shlobj.h>
40#endif
41
42
43#include <stdlib.h>
44#include <stdio.h>
45#include <string.h>
46#include <sys/types.h>
47#include <sys/stat.h>
48#ifdef HAVE_UNISTD_H
49# include <unistd.h>
50#endif
51#include <errno.h>
52
53#include "gnc-path.h"
54#include "gnc-filepath-utils.h"
55
56#if defined (_MSC_VER) || defined (G_OS_WIN32)
57#include <glib/gwin32.h>
58#ifndef PATH_MAX
59#define PATH_MAX MAXPATHLEN
60#endif
61#endif
62#ifdef MAC_INTEGRATION
63#include <Foundation/Foundation.h>
64#endif
65
66#include "gnc-locale-utils.hpp"
67#include <boost/filesystem.hpp>
68#include <boost/locale.hpp>
69#include <ctre.hpp>
70#include <iostream>
71
72/* Below cvt and bfs_locale should be used with boost::filesystem::path (bfs)
73 * objects created alter in this source file. The rationale is as follows:
74 * - a bfs object has an internal, locale and platform dependent
75 * representation of a file system path
76 * - glib's internal representation is always utf8
77 * - when creating a bfs object, we should pass a cvt to convert from
78 * utf8 to the object's internal representation
79 * - if we later want to use the bfs object's internal representation
80 * in a glib context we should imbue the locale below so that
81 * bfs will use it to convert back to utf8
82 * - if the bfs object's internal representation will never be used
83 * in a glib context, imbuing is not needed (although probably more
84 * future proof)
85 * - also note creating a bfs based on another one will inherit the
86 * locale from the source path. So in that case there's not need
87 * to imbue it again.
88 */
89#if PLATFORM(WINDOWS)
90#include <codecvt>
91using codecvt = std::codecvt_utf8<wchar_t, 0x10FFFF, std::little_endian>;
92using string = std::wstring;
93#else
94/* See https://stackoverflow.com/questions/41744559/is-this-a-bug-of-gcc */
95template<class I, class E, class S>
96struct codecvt_r : std::codecvt<I, E, S>
97{
98 ~codecvt_r() {}
99};
101using string = std::string;
102#endif
103static codecvt cvt;
104static std::locale bfs_locale(std::locale(), new codecvt);
105
106namespace bfs = boost::filesystem;
107namespace bst = boost::system;
108namespace bl = boost::locale;
109
116static gchar *
117check_path_return_if_valid(gchar *path)
118{
119 if (g_file_test(path, G_FILE_TEST_IS_REGULAR))
120 {
121 return path;
122 }
123 g_free (path);
124 return NULL;
125}
126
154gchar *
155gnc_resolve_file_path (const gchar * filefrag)
156{
157 gchar *fullpath = NULL, *tmp_path = NULL;
158
159 /* seriously invalid */
160 if (!filefrag)
161 {
162 g_critical("filefrag is NULL");
163 return NULL;
164 }
165
166 /* ---------------------------------------------------- */
167 /* OK, now we try to find or build an absolute file path */
168
169 /* check for an absolute file path */
170 if (g_path_is_absolute(filefrag))
171 return g_strdup (filefrag);
172
173 /* Look in the current working directory */
174 tmp_path = g_get_current_dir();
175 fullpath = g_build_filename(tmp_path, filefrag, (gchar *)NULL);
176 g_free(tmp_path);
177 fullpath = check_path_return_if_valid(fullpath);
178 if (fullpath != NULL)
179 return fullpath;
180
181 /* Look in the data dir (e.g. $PREFIX/share/gnucash) */
182 tmp_path = gnc_path_get_pkgdatadir();
183 fullpath = g_build_filename(tmp_path, filefrag, (gchar *)NULL);
184 g_free(tmp_path);
185 fullpath = check_path_return_if_valid(fullpath);
186 if (fullpath != NULL)
187 return fullpath;
188
189 /* Look in the config dir (e.g. $PREFIX/share/gnucash/accounts) */
190 tmp_path = gnc_path_get_accountsdir();
191 fullpath = g_build_filename(tmp_path, filefrag, (gchar *)NULL);
192 g_free(tmp_path);
193 fullpath = check_path_return_if_valid(fullpath);
194 if (fullpath != NULL)
195 return fullpath;
196
197 /* Look in the users config dir (e.g. $HOME/.gnucash/data) */
198 fullpath = g_strdup(gnc_build_data_path(filefrag));
199 if (g_file_test(fullpath, G_FILE_TEST_IS_REGULAR))
200 return fullpath;
201
202 /* OK, it's not there. Note that it needs to be created and pass it
203 * back anyway */
204 g_warning("create new file %s", fullpath);
205 return fullpath;
206
207}
208
209gchar *gnc_file_path_relative_part (const gchar *prefix, const gchar *path)
210{
211 std::string p{path};
212 if (p.find(prefix) == 0)
213 {
214 auto str = p.substr(strlen(prefix));
215 return g_strdup(str.c_str());
216 }
217 return g_strdup(path);
218}
219
220/* Searches for a file fragment paths set via GNC_DOC_PATH environment
221 * variable. If this variable is not set, fall back to search in
222 * - a html directory in the local user's gnucash settings directory
223 * (typically $HOME/.gnucash/html)
224 * - the gnucash documentation directory
225 * (typically /usr/share/doc/gnucash)
226 * - the gnucash data directory
227 * (typically /usr/share/gnucash)
228 * It searches in this order.
229 *
230 * This is used by gnc_path_find_localized_file to search for
231 * localized versions of files if they exist.
232 */
233static gchar *
234gnc_path_find_localized_html_file_internal (const gchar * file_name)
235{
236 gchar *full_path = NULL;
237 int i;
238 const gchar *env_doc_path = g_getenv("GNC_DOC_PATH");
239 const gchar *default_dirs[] =
240 {
241 gnc_build_userdata_path ("html"),
242 gnc_path_get_pkgdocdir (),
243 gnc_path_get_pkgdatadir (),
244 NULL
245 };
246 gchar **dirs;
247
248 if (!file_name || *file_name == '\0')
249 return NULL;
250
251 /* Allow search path override via GNC_DOC_PATH environment variable */
252 if (env_doc_path)
253 dirs = g_strsplit (env_doc_path, G_SEARCHPATH_SEPARATOR_S, -1);
254 else
255 dirs = (gchar **)default_dirs;
256
257 for (i = 0; dirs[i]; i++)
258 {
259 full_path = g_build_filename (dirs[i], file_name, (gchar *)NULL);
260 g_debug ("Checking for existence of %s", full_path);
261 full_path = check_path_return_if_valid (full_path);
262 if (full_path != NULL)
263 return full_path;
264 }
265
266 return NULL;
267}
268
303gchar *
304gnc_path_find_localized_html_file (const gchar *file_name)
305{
306 gchar *loc_file_name = NULL;
307 gchar *full_path = NULL;
308 const gchar * const *lang;
309
310 if (!file_name || *file_name == '\0')
311 return NULL;
312
313 /* An absolute path is returned unmodified. */
314 if (g_path_is_absolute (file_name))
315 return g_strdup (file_name);
316
317 /* First try to find the file in any of the localized directories
318 * the user has set up on his system
319 */
320 for (lang = g_get_language_names (); *lang; lang++)
321 {
322 loc_file_name = g_build_filename (*lang, file_name, (gchar *)NULL);
323 full_path = gnc_path_find_localized_html_file_internal (loc_file_name);
324 g_free (loc_file_name);
325 if (full_path != NULL)
326 return full_path;
327 }
328
329 /* If not found in a localized directory, try to find the file
330 * in any of the base directories
331 */
332 return gnc_path_find_localized_html_file_internal (file_name);
333
334}
335
336/* ====================================================================== */
337static auto gnc_userdata_home = bfs::path();
338static auto gnc_userconfig_home = bfs::path();
339static auto build_dir = bfs::path();
340/*Provide static-stored strings for gnc_userdata_home and
341 * gnc_userconfig_home to ensure that the cstrings don't go out of
342 * scope when gnc_userdata_dir and gnc_userconfig_dir return them.
343 */
344static std::string gnc_userdata_home_str;
345static std::string gnc_userconfig_home_str;
346
347static bool dir_is_descendant (const bfs::path& path, const bfs::path& base)
348{
349 auto test_path = path;
350 if (bfs::exists (path))
351 test_path = bfs::canonical (path);
352 auto test_base = base;
353 if (bfs::exists (base))
354 test_base = bfs::canonical (base);
355
356 auto is_descendant = (test_path.string() == test_base.string());
357 while (!test_path.empty() && !is_descendant)
358 {
359 test_path = test_path.parent_path();
360 is_descendant = (test_path.string() == test_base.string());
361 }
362 return is_descendant;
363}
364
370static bool
371gnc_validate_directory (const bfs::path &dirname)
372{
373 if (dirname.empty())
374 return false;
375
376 auto create_dirs = true;
377 if (build_dir.empty() || !dir_is_descendant (dirname, build_dir))
378 {
379 /* Gnucash won't create a home directory
380 * if it doesn't exist yet. So if the directory to create
381 * is a descendant of the homedir, we can't create it either.
382 * This check is conditioned on do_homedir_check because
383 * we need to overrule it during build (when guile interferes)
384 * and testing.
385 */
386 bfs::path home_dir(g_get_home_dir(), cvt);
387 home_dir.imbue(bfs_locale);
388 auto homedir_exists = bfs::exists(home_dir);
389 auto is_descendant = dir_is_descendant (dirname, home_dir);
390 if (!homedir_exists && is_descendant)
391 create_dirs = false;
392 }
393
394 /* Create directories if they don't exist yet and we can
395 *
396 * Note this will do nothing if the directory and its
397 * parents already exist, but will fail if the path
398 * points to a file or a softlink. So it serves as a test
399 * for that as well.
400 */
401 if (create_dirs)
402 bfs::create_directories(dirname);
403 else
404 throw (bfs::filesystem_error (
405 std::string (dirname.string() +
406 " is a descendant of a non-existing home directory. As " +
407 PACKAGE_NAME +
408 " will never create a home directory this path can't be used"),
409 dirname, bst::error_code(bst::errc::permission_denied, bst::generic_category())));
410
411 auto d = bfs::directory_entry (dirname);
412 auto perms = d.status().permissions();
413
414 /* On Windows only write permission will be checked.
415 * So strictly speaking we'd need two error messages here depending
416 * on the platform. For simplicity this detail is glossed over though. */
417#if PLATFORM(WINDOWS)
418 auto check_perms = bfs::owner_read | bfs::owner_write;
419#else
420 auto check_perms = bfs::owner_all;
421#endif
422 if ((perms & check_perms) != check_perms)
423 throw (bfs::filesystem_error(
424 std::string("Insufficient permissions, at least write and access permissions required: ")
425 + dirname.string(), dirname,
426 bst::error_code(bst::errc::permission_denied, bst::generic_category())));
427
428 return true;
429}
430
431/* Will attempt to copy all files and directories from src to dest
432 * Returns true if successful or false if not */
433static bool
434copy_recursive(const bfs::path& src, const bfs::path& dest)
435{
436 if (!bfs::exists(src))
437 return false;
438
439 // Don't copy on self
440 if (src.compare(dest) == 0)
441 return false;
442
443 auto old_str = src.string();
444 auto old_len = old_str.size();
445 try
446 {
447 /* Note: the for(auto elem : iterator) construct fails
448 * on travis (g++ 4.8.x, boost 1.54) so I'm using
449 * a traditional for loop here */
450 for(auto direntry = bfs::recursive_directory_iterator(src);
451 direntry != bfs::recursive_directory_iterator(); ++direntry)
452 {
453#ifdef G_OS_WIN32
454 string cur_str = direntry->path().wstring();
455#else
456 string cur_str = direntry->path().string();
457#endif
458 auto cur_len = cur_str.size();
459 string rel_str(cur_str, old_len, cur_len - old_len);
460 bfs::path relpath(rel_str, cvt);
461 auto newpath = bfs::absolute (relpath.relative_path(), dest);
462 newpath.imbue(bfs_locale);
463 bfs::copy(direntry->path(), newpath);
464 }
465 }
466 catch(const bfs::filesystem_error& ex)
467 {
468 g_warning("An error occurred while trying to migrate the user configation from\n%s to\n%s"
469 "(Error: %s)",
470 src.string().c_str(), gnc_userdata_home_str.c_str(),
471 ex.what());
472 return false;
473 }
474
475 return true;
476}
477
478#ifdef G_OS_WIN32
479/* g_get_user_data_dir defaults to CSIDL_LOCAL_APPDATA, but
480 * the roaming profile makes more sense.
481 * So this function is a copy of glib's internal get_special_folder
482 * and minimally adjusted to fetch CSIDL_APPDATA
483 */
484static bfs::path
485get_user_data_dir ()
486{
487 wchar_t path[MAX_PATH+1];
488 HRESULT hr;
489 LPITEMIDLIST pidl = NULL;
490
491 hr = SHGetSpecialFolderLocation (NULL, CSIDL_APPDATA, &pidl);
492 if (hr == S_OK)
493 {
494 [[maybe_unused]] auto b = SHGetPathFromIDListW (pidl, path);
495 CoTaskMemFree (pidl);
496 }
497 bfs::path retval(path, cvt);
498 retval.imbue(bfs_locale);
499 return retval;
500}
501#elif defined MAC_INTEGRATION
502static bfs::path
503get_user_data_dir()
504{
505 NSFileManager*fm = [NSFileManager defaultManager];
506 NSArray* appSupportDir = [fm URLsForDirectory:NSApplicationSupportDirectory
507 inDomains:NSUserDomainMask];
508 NSString *dirPath = nullptr;
509 if ([appSupportDir count] > 0)
510 {
511 NSURL* dirUrl = [appSupportDir objectAtIndex:0];
512 dirPath = [dirUrl path];
513 }
514 return [dirPath UTF8String];
515}
516#else
517static bfs::path
518get_user_data_dir()
519{
520 return g_get_user_data_dir();
521}
522#endif
523
524/* Returns an absolute path to the user's data directory.
525 * Note the default path depends on the platform.
526 * - Windows: CSIDL_APPDATA
527 * - OS X: $HOME/Application Support
528 * - Linux: $XDG_DATA_HOME (or the default $HOME/.local/share)
529 */
530static bfs::path
531get_userdata_home(void)
532{
533 auto try_tmp_dir = true;
534 auto userdata_home = get_user_data_dir();
535
536 /* g_get_user_data_dir doesn't check whether the path exists nor attempts to
537 * create it. So while it may return an actual path we may not be able to use it.
538 * Let's check that now */
539 if (!userdata_home.empty())
540 {
541 try
542 {
543 gnc_validate_directory(userdata_home); // May throw
544 try_tmp_dir = false;
545 }
546 catch (const bfs::filesystem_error& ex)
547 {
548 auto path_string = userdata_home.string();
549 g_warning("%s is not a suitable base directory for the user data. "
550 "Trying temporary directory instead.\n(Error: %s)",
551 path_string.c_str(), ex.what());
552 }
553 }
554
555 /* The path we got is not usable, so fall back to a path in TMP_DIR.
556 Hopefully we can always write there. */
557 if (try_tmp_dir)
558 {
559 bfs::path newpath(g_get_tmp_dir (), cvt);
560 userdata_home = newpath / g_get_user_name ();
561 userdata_home.imbue(bfs_locale);
562 }
563 g_assert(!userdata_home.empty());
564
565 return userdata_home;
566}
567
568/* Returns an absolute path to the user's config directory.
569 * Note the default path depends on the platform.
570 * - Windows: CSIDL_APPDATA
571 * - OS X: $HOME/Application Support
572 * - Linux: $XDG_CONFIG_HOME (or the default $HOME/.config)
573 */
574static bfs::path
575get_userconfig_home(void)
576{
577 /* On Windows and Macs the data directory is used, for Linux
578 $HOME/.config is used */
579#if defined (G_OS_WIN32) || defined (MAC_INTEGRATION)
580 return get_user_data_dir();
581#else
582 return g_get_user_config_dir();
583#endif
584}
585
586static std::string migrate_gnc_datahome()
587{
588 // Specify location of dictionaries
589 bfs::path old_dir(g_get_home_dir(), cvt);
590 old_dir /= ".gnucash";
591
592 std::stringstream migration_msg;
593 migration_msg.imbue(gnc_get_boost_locale());
594
595 /* Step 1: copy directory $HOME/.gnucash to $GNC_DATA_HOME */
596 auto full_copy = copy_recursive (old_dir, gnc_userdata_home);
597
598 /* Step 2: move user editable config files from GNC_DATA_HOME to GNC_CONFIG_HOME
599 These files are:
600 - log.conf
601 - the most recent of "config-2.0.user", "config-1.8.user", "config-1.6.user",
602 "config.user"
603 Note: we'll also rename config.user to config-user.scm to make it more clear
604 this file is meant for custom scm code to load at run time */
605 auto failed = std::vector<std::string>{};
606 auto succeeded = std::vector<std::string>{};
607
608 /* Move log.conf
609 * Note on OS X/Quarz and Windows GNC_DATA_HOME and GNC_CONFIG_HOME are the same, so this will do nothing */
610 auto oldlogpath = gnc_userdata_home / "log.conf";
611 auto newlogpath = gnc_userconfig_home / "log.conf";
612 try
613 {
614 if (bfs::exists (oldlogpath) && gnc_validate_directory (gnc_userconfig_home) &&
615 (oldlogpath != newlogpath))
616 {
617 bfs::rename (oldlogpath, newlogpath);
618 succeeded.emplace_back ("log.conf");
619 }
620 }
621 catch (const bfs::filesystem_error& ex)
622 {
623 failed.emplace_back ("log.conf");
624 }
625
626 /* Move/rename the most relevant config*.user file. The relevance comes from
627 * the order in which these files were searched for at gnucash startup.
628 * Make note of other config*.user files found to inform the user they are now ignored */
629 auto user_config_files = std::vector<std::string>
630 {
631 "config-2.0.user", "config-1.8.user",
632 "config-1.6.user", "config.user"
633 };
634 auto conf_exist_vec = std::vector<std::string> {};
635 auto renamed_config = std::string();
636 for (auto conf_file : user_config_files)
637 {
638 auto oldconfpath = gnc_userdata_home / conf_file;
639 try
640 {
641 if (bfs::exists (oldconfpath) && gnc_validate_directory (gnc_userconfig_home))
642 {
643 // Only migrate the most relevant of the config*.user files
644 if (renamed_config.empty())
645 {
646 /* Translators: this string refers to a file name that gets renamed */
647 renamed_config = conf_file + " (" + _("Renamed to:") + " config-user.scm)";
648 auto newconfpath = gnc_userconfig_home / "config-user.scm";
649 bfs::rename (oldconfpath, newconfpath);
650 }
651 else
652 {
653 /* We want to report the obsolete file to the user */
654 conf_exist_vec.emplace_back (conf_file);
655 if (full_copy)
656 /* As we copied from .gnucash, just delete the obsolete file as well. It's still
657 * present in .gnucash if the user wants to recover it */
658 bfs::remove (oldconfpath);
659 }
660 }
661 }
662 catch (const bfs::filesystem_error& ex)
663 {
664 failed.emplace_back (conf_file);
665 }
666 }
667 if (!renamed_config.empty())
668 succeeded.emplace_back (renamed_config);
669
670 /* Step 3: inform the user of additional changes */
671 if (full_copy || !succeeded.empty() || !conf_exist_vec.empty() || !failed.empty())
672 migration_msg << _("Notice") << std::endl << std::endl;
673
674 if (full_copy)
675 {
676 migration_msg
677 << _("Your gnucash metadata has been migrated.") << std::endl << std::endl
678 /* Translators: this refers to a directory name. */
679 << _("Old location:") << " " << old_dir.string() << std::endl
680 /* Translators: this refers to a directory name. */
681 << _("New location:") << " " << gnc_userdata_home.string() << std::endl << std::endl
682 // Translators {1} will be replaced with the package name (typically Gnucash) at runtime
683 << bl::format (std::string{_("If you no longer intend to run {1} 2.6.x or older on this system you can safely remove the old directory.")})
684 % PACKAGE_NAME;
685 }
686
687 if (full_copy &&
688 (!succeeded.empty() || !conf_exist_vec.empty() || !failed.empty()))
689 migration_msg << std::endl << std::endl
690 << _("In addition:");
691
692 if (!succeeded.empty())
693 {
694 migration_msg << std::endl << std::endl;
695 if (full_copy)
696 migration_msg << bl::format (std::string{ngettext("The following file has been copied to {1} instead:",
697 "The following files have been copied to {1} instead:",
698 succeeded.size())}) % gnc_userconfig_home.string().c_str();
699 else
700 migration_msg << bl::format (std::string{_("The following file in {1} has been renamed:")})
701 % gnc_userconfig_home.string().c_str();
702
703 migration_msg << std::endl;
704 for (const auto& success_file : succeeded)
705 migration_msg << "- " << success_file << std::endl;
706 }
707 if (!conf_exist_vec.empty())
708 {
709 migration_msg << "\n\n"
710 << ngettext("The following file has become obsolete and will be ignored:",
711 "The following files have become obsolete and will be ignored:",
712 conf_exist_vec.size())
713 << std::endl;
714 for (const auto& obs_file : conf_exist_vec)
715 migration_msg << "- " << obs_file << std::endl;
716 }
717 if (!failed.empty())
718 {
719 migration_msg << std::endl << std::endl
720 << bl::format (std::string{ngettext("The following file could not be moved to {1}:",
721 "The following files could not be moved to {1}:",
722 failed.size())}) % gnc_userconfig_home.string().c_str()
723 << std::endl;
724 for (const auto& failed_file : failed)
725 migration_msg << "- " << failed_file << std::endl;
726 }
727
728 return migration_msg.str ();
729}
730
731
732
733#if defined G_OS_WIN32 ||defined MAC_INTEGRATION
734constexpr auto path_package = PACKAGE_NAME;
735#else
736constexpr auto path_package = PROJECT_NAME;
737#endif
738
739// Initialize the user's config directory for gnucash
740// creating it if it doesn't exist yet.
741static void
742gnc_file_path_init_config_home (void)
743{
744 auto have_valid_userconfig_home = false;
745
746 /* If this code is run while building/testing, use a fake GNC_CONFIG_HOME
747 * in the base of the build directory. This is to deal with all kinds of
748 * issues when the build environment is not a complete environment (like
749 * it could be missing a valid home directory). */
750 auto env_build_dir = g_getenv ("GNC_BUILDDIR");
751 bfs::path new_dir(env_build_dir ? env_build_dir : "", cvt);
752 new_dir.imbue(bfs_locale);
753 build_dir = std::move(new_dir);
754 auto running_uninstalled = (g_getenv ("GNC_UNINSTALLED") != NULL);
755 if (running_uninstalled && !build_dir.empty())
756 {
757 gnc_userconfig_home = build_dir / "gnc_config_home";
758 try
759 {
760 gnc_validate_directory (gnc_userconfig_home); // May throw
761 have_valid_userconfig_home = true;
762 }
763 catch (const bfs::filesystem_error& ex)
764 {
765 auto path_string = gnc_userconfig_home.string();
766 g_warning("%s (due to run during at build time) is not a suitable directory for user configuration files. "
767 "Trying another directory instead.\n(Error: %s)",
768 path_string.c_str(), ex.what());
769 }
770 }
771
772 if (!have_valid_userconfig_home)
773 {
774 /* If environment variable GNC_CONFIG_HOME is set, try whether
775 * it points at a valid directory. */
776 auto gnc_userconfig_home_env = g_getenv ("GNC_CONFIG_HOME");
777 if (gnc_userconfig_home_env)
778 {
779 bfs::path newdir(gnc_userconfig_home_env, cvt);
780 newdir.imbue(bfs_locale);
781 gnc_userconfig_home = std::move(newdir);
782 try
783 {
784 gnc_validate_directory (gnc_userconfig_home); // May throw
785 have_valid_userconfig_home = true;
786 }
787 catch (const bfs::filesystem_error& ex)
788 {
789 auto path_string = gnc_userconfig_home.string();
790 g_warning("%s (from environment variable 'GNC_CONFIG_HOME') is not a suitable directory for user configuration files. "
791 "Trying the default instead.\n(Error: %s)",
792 path_string.c_str(), ex.what());
793 }
794 }
795 }
796
797 if (!have_valid_userconfig_home)
798 {
799 /* Determine platform dependent default userconfig_home_path
800 * and check whether it's valid */
801 auto userconfig_home = get_userconfig_home();
802 gnc_userconfig_home = userconfig_home / path_package;
803 try
804 {
805 gnc_validate_directory (gnc_userconfig_home);
806 }
807 catch (const bfs::filesystem_error& ex)
808 {
809 g_warning ("User configuration directory doesn't exist, yet could not be created. Proceed with caution.\n"
810 "(Error: %s)", ex.what());
811 }
812 }
813 gnc_userconfig_home_str = gnc_userconfig_home.string();
814}
815
816// Initialize the user's config directory for gnucash
817// creating it if it didn't exist yet.
818// The function will return true if the directory already
819// existed or false if it had to be created
820static bool
821gnc_file_path_init_data_home (void)
822{
823 // Initialize the user's data directory for gnucash
824 auto gnc_userdata_home_exists = false;
825 auto have_valid_userdata_home = false;
826
827 /* If this code is run while building/testing, use a fake GNC_DATA_HOME
828 * in the base of the build directory. This is to deal with all kinds of
829 * issues when the build environment is not a complete environment (like
830 * it could be missing a valid home directory). */
831 auto env_build_dir = g_getenv ("GNC_BUILDDIR");
832 bfs::path new_dir(env_build_dir ? env_build_dir : "", cvt);
833 new_dir.imbue(bfs_locale);
834 build_dir = std::move(new_dir);
835 auto running_uninstalled = (g_getenv ("GNC_UNINSTALLED") != NULL);
836 if (running_uninstalled && !build_dir.empty())
837 {
838 gnc_userdata_home = build_dir / "gnc_data_home";
839 try
840 {
841 gnc_validate_directory (gnc_userdata_home); // May throw
842 have_valid_userdata_home = true;
843 gnc_userdata_home_exists = true; // To prevent possible migration further down
844 }
845 catch (const bfs::filesystem_error& ex)
846 {
847 auto path_string = gnc_userdata_home.string();
848 g_warning("%s (due to run during at build time) is not a suitable directory for user data. "
849 "Trying another directory instead.\n(Error: %s)",
850 path_string.c_str(), ex.what());
851 }
852 }
853
854 if (!have_valid_userdata_home)
855 {
856 /* If environment variable GNC_DATA_HOME is set, try whether
857 * it points at a valid directory. */
858 auto gnc_userdata_home_env = g_getenv ("GNC_DATA_HOME");
859 if (gnc_userdata_home_env)
860 {
861 bfs::path newdir(gnc_userdata_home_env, cvt);
862 newdir.imbue(bfs_locale);
863 gnc_userdata_home = std::move(newdir);
864 try
865 {
866 gnc_userdata_home_exists = bfs::exists (gnc_userdata_home);
867 gnc_validate_directory (gnc_userdata_home); // May throw
868 have_valid_userdata_home = true;
869 }
870 catch (const bfs::filesystem_error& ex)
871 {
872 auto path_string = gnc_userdata_home.string();
873 g_warning("%s (from environment variable 'GNC_DATA_HOME') is not a suitable directory for user data. "
874 "Trying the default instead.\n(Error: %s)",
875 path_string.c_str(), ex.what());
876 }
877 }
878 }
879
880 if (!have_valid_userdata_home)
881 {
882 /* Determine platform dependent default userdata_home_path
883 * and check whether it's valid */
884 auto userdata_home = get_userdata_home();
885 gnc_userdata_home = userdata_home / path_package;
886 try
887 {
888 gnc_userdata_home_exists = bfs::exists (gnc_userdata_home);
889 gnc_validate_directory (gnc_userdata_home);
890 }
891 catch (const bfs::filesystem_error& ex)
892 {
893 g_warning ("User data directory doesn't exist, yet could not be created. Proceed with caution.\n"
894 "(Error: %s)", ex.what());
895 }
896 }
897 gnc_userdata_home_str = gnc_userdata_home.string();
898 return gnc_userdata_home_exists;
899}
900
901// Initialize the user's config and data directory for gnucash
902// This function will also create these directories if they didn't
903// exist yet.
904// In addition it will trigger a migration if the user's data home
905// didn't exist but the now obsolete GNC_DOT_DIR ($HOME/.gnucash)
906// does.
907// Finally it well ensure a number of default required directories
908// will be created if they don't exist yet.
909char *
910gnc_filepath_init (void)
911{
912 gnc_userconfig_home = get_userconfig_home() / path_package;
913 gnc_userconfig_home_str = gnc_userconfig_home.string();
914
915 gnc_file_path_init_config_home ();
916 auto gnc_userdata_home_exists = gnc_file_path_init_data_home ();
917
918 /* Run migration code before creating the default directories
919 If migrating, these default directories are copied instead of created. */
920 auto migration_notice = std::string ();
921 if (!gnc_userdata_home_exists)
922 migration_notice = migrate_gnc_datahome();
923
924 /* Try to create the standard subdirectories for gnucash' user data */
925 try
926 {
927 gnc_validate_directory (gnc_userdata_home / "books");
928 gnc_validate_directory (gnc_userdata_home / "checks");
929 gnc_validate_directory (gnc_userdata_home / "translog");
930 }
931 catch (const bfs::filesystem_error& ex)
932 {
933 g_warning ("Default user data subdirectories don't exist, yet could not be created. Proceed with caution.\n"
934 "(Error: %s)", ex.what());
935 }
936
937 return migration_notice.empty() ? NULL : g_strdup (migration_notice.c_str());
938}
939
959/* Note Don't create missing directories automatically
960 * here and in the next function except if the
961 * target directory is the temporary directory. This
962 * should be done properly at a higher level (in the gui
963 * code most likely) very early in application startup.
964 * This call is just a fallback to prevent the code from
965 * crashing because no directories were configured. This
966 * weird construct is set up because compiling our guile
967 * scripts also triggers this code and that's not the
968 * right moment to start creating the necessary directories.
969 * FIXME A better approach would be to have the gnc_userdata_home
970 * verification/creation be part of the application code instead
971 * of libgnucash. If libgnucash needs access to this directory
972 * libgnucash will need some kind of initialization routine
973 * that the application can call to set (among others) the proper
974 * gnc_uderdata_home for libgnucash. The only other aspect to
975 * consider here is how to handle this in the bindings (if they
976 * need it).
977 */
978const gchar *
979gnc_userdata_dir (void)
980{
981 if (gnc_userdata_home.empty())
982 gnc_filepath_init();
983 return g_strdup(gnc_userdata_home_str.c_str());
984}
985
999const gchar *
1000gnc_userconfig_dir (void)
1001{
1002 if (gnc_userdata_home.empty())
1003 gnc_filepath_init();
1004
1005 return gnc_userconfig_home_str.c_str();
1006}
1007
1008static const bfs::path&
1009gnc_userdata_dir_as_path (void)
1010{
1011 if (gnc_userdata_home.empty())
1012 /* Don't create missing directories automatically except
1013 * if the target directory is the temporary directory. This
1014 * should be done properly at a higher level (in the gui
1015 * code most likely) very early in application startup.
1016 * This call is just a fallback to prevent the code from
1017 * crashing because no directories were configured. */
1018 gnc_filepath_init();
1019
1020 return gnc_userdata_home;
1021}
1022
1023static const bfs::path&
1024gnc_userconfig_dir_as_path (void)
1025{
1026 if (gnc_userdata_home.empty())
1027 /* Don't create missing directories automatically except
1028 * if the target directory is the temporary directory. This
1029 * should be done properly at a higher level (in the gui
1030 * code most likely) very early in application startup.
1031 * This call is just a fallback to prevent the code from
1032 * crashing because no directories were configured. */
1033 gnc_filepath_init();
1034
1035 return gnc_userconfig_home;
1036}
1037
1038gchar *gnc_file_path_absolute (const gchar *prefix, const gchar *relative)
1039{
1040 bfs::path path_relative (relative);
1041 path_relative.imbue (bfs_locale);
1042 bfs::path path_absolute;
1043 bfs::path path_head;
1044
1045 if (prefix == nullptr)
1046 {
1047 const gchar *doc_dir = g_get_user_special_dir (G_USER_DIRECTORY_DOCUMENTS);
1048 if (doc_dir == nullptr)
1049 path_head = bfs::path (gnc_userdata_dir ()); // running as root maybe
1050 else
1051 path_head = bfs::path (doc_dir);
1052
1053 path_head.imbue (bfs_locale);
1054 path_absolute = absolute (path_relative, path_head);
1055 }
1056 else
1057 {
1058 bfs::path path_head (prefix);
1059 path_head.imbue (bfs_locale);
1060 path_absolute = absolute (path_relative, path_head);
1061 }
1062 path_absolute.imbue (bfs_locale);
1063
1064 return g_strdup (path_absolute.string().c_str());
1065}
1066
1076gchar *
1077gnc_build_userdata_path (const gchar *filename)
1078{
1079 return g_strdup((gnc_userdata_dir_as_path() / filename).string().c_str());
1080}
1081
1091gchar *
1092gnc_build_userconfig_path (const gchar *filename)
1093{
1094 return g_strdup((gnc_userconfig_dir_as_path() / filename).string().c_str());
1095}
1096
1097/* Test whether c is a valid character for a win32 file name.
1098 * If so return false, otherwise return true.
1099 */
1100static bool
1101is_invalid_char (char c)
1102{
1103 return (c == '/') || ( c == ':');
1104}
1105
1106static bfs::path
1107gnc_build_userdata_subdir_path (const gchar *subdir, const gchar *filename)
1108{
1109 auto fn = std::string(filename);
1110
1111 std::replace_if (fn.begin(), fn.end(), is_invalid_char, '_');
1112 auto result = (gnc_userdata_dir_as_path() / subdir) / fn;
1113 return result;
1114}
1115
1125gchar *
1126gnc_build_book_path (const gchar *filename)
1127{
1128 auto path = gnc_build_userdata_subdir_path("books", filename).string();
1129 return g_strdup(path.c_str());
1130}
1131
1141gchar *
1142gnc_build_translog_path (const gchar *filename)
1143{
1144 auto path = gnc_build_userdata_subdir_path("translog", filename).string();
1145 return g_strdup(path.c_str());
1146}
1147
1157gchar *
1158gnc_build_data_path (const gchar *filename)
1159{
1160 auto path = gnc_build_userdata_subdir_path("data", filename).string();
1161 return g_strdup(path.c_str());
1162}
1163
1173gchar *
1174gnc_build_scm_path (const gchar *filename)
1175{
1176 gchar *scmdir = gnc_path_get_scmdir ();
1177 gchar *result = g_build_filename (scmdir, filename, (gchar *)NULL);
1178 g_free (scmdir);
1179 return result;
1180}
1181
1191gchar *
1192gnc_build_report_path (const gchar *filename)
1193{
1194 gchar *rptdir = gnc_path_get_reportdir ();
1195 gchar *result = g_build_filename (rptdir, filename, (gchar *)NULL);
1196 g_free (rptdir);
1197 return result;
1198}
1199
1209gchar *
1210gnc_build_reports_path (const gchar *dirname)
1211{
1212 gchar *rptsdir = gnc_path_get_reportsdir ();
1213 gchar *result = g_build_filename (rptsdir, dirname, (gchar *)NULL);
1214 g_free (rptsdir);
1215 return result;
1216}
1217
1227gchar *
1228gnc_build_stdreports_path (const gchar *filename)
1229{
1230 gchar *stdrptdir = gnc_path_get_stdreportsdir ();
1231 gchar *result = g_build_filename (stdrptdir, filename, (gchar *)NULL);
1232 g_free (stdrptdir);
1233 return result;
1234}
1235
1236static gchar *
1237gnc_filepath_locate_file (const gchar *default_path, const gchar *name)
1238{
1239 gchar *fullname;
1240
1241 g_return_val_if_fail (name != NULL, NULL);
1242
1243 if (g_path_is_absolute (name))
1244 fullname = g_strdup (name);
1245 else if (default_path)
1246 fullname = g_build_filename (default_path, name, nullptr);
1247 else
1248 fullname = gnc_resolve_file_path (name);
1249
1250 if (!g_file_test (fullname, G_FILE_TEST_IS_REGULAR))
1251 {
1252 g_warning ("Could not locate file %s", name);
1253 g_free (fullname);
1254 return NULL;
1255 }
1256
1257 return fullname;
1258}
1259
1260gchar *
1261gnc_filepath_locate_data_file (const gchar *name)
1262{
1263 gchar *pkgdatadir = gnc_path_get_pkgdatadir ();
1264 gchar *result = gnc_filepath_locate_file (pkgdatadir, name);
1265 g_free (pkgdatadir);
1266 return result;
1267}
1268
1269gchar *
1270gnc_filepath_locate_pixmap (const gchar *name)
1271{
1272 gchar *default_path;
1273 gchar *fullname;
1274 gchar* pkgdatadir = gnc_path_get_pkgdatadir ();
1275
1276 default_path = g_build_filename (pkgdatadir, "pixmaps", nullptr);
1277 g_free(pkgdatadir);
1278 fullname = gnc_filepath_locate_file (default_path, name);
1279 g_free(default_path);
1280
1281 return fullname;
1282}
1283
1284gchar *
1285gnc_filepath_locate_ui_file (const gchar *name)
1286{
1287 gchar *default_path;
1288 gchar *fullname;
1289 gchar* pkgdatadir = gnc_path_get_pkgdatadir ();
1290
1291 default_path = g_build_filename (pkgdatadir, "ui", nullptr);
1292 g_free(pkgdatadir);
1293 fullname = gnc_filepath_locate_file (default_path, name);
1294 g_free(default_path);
1295
1296 return fullname;
1297}
1298
1299gchar *
1300gnc_filepath_locate_doc_file (const gchar *name)
1301{
1302 gchar *docdir = gnc_path_get_pkgdocdir ();
1303 gchar *result = gnc_filepath_locate_file (docdir, name);
1304 g_free (docdir);
1305 return result;
1306}
1307
1308std::vector<EnvPaths>
1309gnc_list_all_paths ()
1310{
1311 if (gnc_userdata_home.empty())
1312 gnc_filepath_init ();
1313
1314 return {
1315 { "GNC_DATA_HOME", gnc_userdata_home_str.c_str(), true},
1316 { "GNC_CONFIG_HOME", gnc_userconfig_home_str.c_str(), true },
1317 { "GNC_BIN", g_getenv ("GNC_BIN"), false },
1318 { "GNC_LIB", g_getenv ("GNC_LIB"), false },
1319 { "GNC_CONF", g_getenv ("GNC_CONF"), false },
1320 { "GNC_DATA", g_getenv ("GNC_DATA"), false },
1321 };
1322}
1323
1324static constexpr ctll::fixed_string
1325backup_regex (".*[.](?:xac|gnucash)[.][0-9]{14}[.](?:xac|gnucash)$");
1326
1327gboolean gnc_filename_is_backup (const char *filename)
1328{
1329 g_return_val_if_fail (filename, FALSE);
1330 return ctre::match<backup_regex>(filename);
1331}
1332
1333static constexpr ctll::fixed_string
1334datafile_regex (".*[.](?:xac|gnucash)$");
1335
1336gboolean gnc_filename_is_datafile (const char *filename)
1337{
1338 g_return_val_if_fail (filename, FALSE);
1339 return !gnc_filename_is_backup (filename) && ctre::match<datafile_regex>(filename);
1340}
1341
1342std::ofstream
1343gnc_open_filestream(const char* path)
1344{
1345 bfs::path bfs_path(path, cvt);
1346 bfs_path.imbue(bfs_locale);
1347 return std::ofstream(bfs_path.c_str());
1348}
1349/* =============================== END OF FILE ========================== */
File path resolution utility functions.