/* * This program source code file is part of KiCad, a free EDA CAD application. * * Copyright (C) 2020 Jon Evans * Copyright (C) 2020 KiCad Developers, see AUTHORS.txt for contributors. * * This program is free software: you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the * Free Software Foundation, either version 3 of the License, or (at your * option) any later version. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program. If not, see . */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /// Project settings path will be + this #define PROJECT_BACKUPS_DIR_SUFFIX wxT( "-backups" ) SETTINGS_MANAGER::SETTINGS_MANAGER( bool aHeadless ) : m_headless( aHeadless ), m_kiway( nullptr ), m_common_settings( nullptr ), m_migration_source(), m_migrateLibraryTables( true ) { // Check if the settings directory already exists, and if not, perform a migration if possible if( !MigrateIfNeeded() ) { m_ok = false; return; } m_ok = true; // create the common settings shared by all applications. Not loaded immediately m_common_settings = static_cast( RegisterSettings( new COMMON_SETTINGS, false ) ); loadAllColorSettings(); } SETTINGS_MANAGER::~SETTINGS_MANAGER() { m_settings.clear(); m_color_settings.clear(); m_projects.clear(); } JSON_SETTINGS* SETTINGS_MANAGER::RegisterSettings( JSON_SETTINGS* aSettings, bool aLoadNow ) { std::unique_ptr ptr( aSettings ); ptr->SetManager( this ); wxLogTrace( traceSettings, "Registered new settings object <%s>", ptr->GetFullFilename() ); if( aLoadNow ) ptr->LoadFromFile( GetPathForSettingsFile( ptr.get() ) ); m_settings.push_back( std::move( ptr ) ); return m_settings.back().get(); } void SETTINGS_MANAGER::Load() { // TODO(JE) We should check for dirty settings here and write them if so, because // Load() could be called late in the application lifecycle for( auto&& settings : m_settings ) settings->LoadFromFile( GetPathForSettingsFile( settings.get() ) ); } void SETTINGS_MANAGER::Load( JSON_SETTINGS* aSettings ) { auto it = std::find_if( m_settings.begin(), m_settings.end(), [&aSettings]( const std::unique_ptr& aPtr ) { return aPtr.get() == aSettings; } ); if( it != m_settings.end() ) ( *it )->LoadFromFile( GetPathForSettingsFile( it->get() ) ); } void SETTINGS_MANAGER::Save() { for( auto&& settings : m_settings ) { // Never automatically save color settings, caller should use SaveColorSettings if( dynamic_cast( settings.get() ) ) continue; settings->SaveToFile( GetPathForSettingsFile( settings.get() ) ); } } void SETTINGS_MANAGER::Save( JSON_SETTINGS* aSettings ) { auto it = std::find_if( m_settings.begin(), m_settings.end(), [&aSettings]( const std::unique_ptr& aPtr ) { return aPtr.get() == aSettings; } ); if( it != m_settings.end() ) { wxLogTrace( traceSettings, "Saving %s", ( *it )->GetFullFilename() ); ( *it )->SaveToFile( GetPathForSettingsFile( it->get() ) ); } } void SETTINGS_MANAGER::FlushAndRelease( JSON_SETTINGS* aSettings, bool aSave ) { auto it = std::find_if( m_settings.begin(), m_settings.end(), [&aSettings]( const std::unique_ptr& aPtr ) { return aPtr.get() == aSettings; } ); if( it != m_settings.end() ) { wxLogTrace( traceSettings, "Flush and release %s", ( *it )->GetFullFilename() ); if( aSave ) ( *it )->SaveToFile( GetPathForSettingsFile( it->get() ) ); m_settings.erase( it ); } } COLOR_SETTINGS* SETTINGS_MANAGER::GetColorSettings( const wxString& aName ) { if( m_color_settings.count( aName ) ) return m_color_settings.at( aName ); COLOR_SETTINGS* ret = nullptr; if( !aName.empty() ) ret = loadColorSettingsByName( aName ); // This had better work if( !ret ) ret = m_color_settings.at( "user" ); return ret; } COLOR_SETTINGS* SETTINGS_MANAGER::loadColorSettingsByName( const wxString& aName ) { wxLogTrace( traceSettings, "Attempting to load color theme %s", aName ); wxFileName fn( GetColorSettingsPath(), aName, "json" ); if( !fn.IsOk() || !fn.Exists() ) { wxLogTrace( traceSettings, "Theme file %s.json not found, falling back to user", aName ); return nullptr; } auto cs = static_cast( RegisterSettings( new COLOR_SETTINGS( aName.ToStdString() ) ) ); if( cs->GetFilename() != aName.ToStdString() ) { wxLogTrace( traceSettings, "Warning: stored filename is actually %s, ", cs->GetFilename() ); } m_color_settings[aName] = cs; return cs; } class COLOR_SETTINGS_LOADER : public wxDirTraverser { private: std::function m_action; public: explicit COLOR_SETTINGS_LOADER( std::function aAction ) : m_action( std::move( aAction ) ) { } wxDirTraverseResult OnFile( const wxString& aFilePath ) override { wxFileName file( aFilePath ); if( file.GetExt() != "json" ) return wxDIR_CONTINUE; if( file.GetName() == "user" ) return wxDIR_CONTINUE; m_action( file.GetName() ); return wxDIR_CONTINUE; } wxDirTraverseResult OnDir( const wxString& dirPath ) override { return wxDIR_IGNORE; } }; void SETTINGS_MANAGER::registerColorSettings( const wxString& aFilename ) { if( m_color_settings.count( aFilename ) ) return; m_color_settings[aFilename] = static_cast( RegisterSettings( new COLOR_SETTINGS( aFilename ) ) ); } COLOR_SETTINGS* SETTINGS_MANAGER::AddNewColorSettings( const wxString& aFilename ) { wxString filename = aFilename; if( filename.EndsWith( wxT( ".json" ) ) ) filename = filename.BeforeLast( '.' ); registerColorSettings( filename ); return m_color_settings[filename]; } void SETTINGS_MANAGER::loadAllColorSettings() { // Create the default color settings registerColorSettings( "user" ); // Search for and load any other settings COLOR_SETTINGS_LOADER loader( [&]( const wxString& aFilename ) { registerColorSettings( aFilename ); } ); wxDir colors_dir( GetColorSettingsPath() ); if( colors_dir.IsOpened() ) colors_dir.Traverse( loader ); } void SETTINGS_MANAGER::ReloadColorSettings() { m_color_settings.clear(); loadAllColorSettings(); } void SETTINGS_MANAGER::SaveColorSettings( COLOR_SETTINGS* aSettings, const std::string& aNamespace ) { // The passed settings should already be managed wxASSERT( std::find_if( m_color_settings.begin(), m_color_settings.end(), [aSettings] ( const std::pair& el ) { return el.second->GetFilename() == aSettings->GetFilename(); } ) != m_color_settings.end() ); nlohmann::json::json_pointer ptr = JSON_SETTINGS::PointerFromString( aNamespace ); if( !aSettings->Store() ) { wxLogTrace( traceSettings, "Color scheme %s not modified; skipping save", aSettings->GetFilename(), aNamespace ); return; } wxASSERT( aSettings->contains( ptr ) ); wxLogTrace( traceSettings, "Saving color scheme %s, preserving %s", aSettings->GetFilename(), aNamespace ); nlohmann::json backup = aSettings->at( ptr ); wxString path = GetColorSettingsPath(); aSettings->LoadFromFile( path ); ( *aSettings )[ptr].update( backup ); aSettings->Load(); aSettings->SaveToFile( path, true ); } wxString SETTINGS_MANAGER::GetPathForSettingsFile( JSON_SETTINGS* aSettings ) { wxASSERT( aSettings ); switch( aSettings->GetLocation() ) { case SETTINGS_LOC::USER: return GetUserSettingsPath(); case SETTINGS_LOC::PROJECT: return Prj().GetProjectPath(); case SETTINGS_LOC::COLORS: return GetColorSettingsPath(); case SETTINGS_LOC::NONE: return ""; default: wxASSERT_MSG( false, "Unknown settings location!" ); } return ""; } class MIGRATION_TRAVERSER : public wxDirTraverser { private: wxString m_src; wxString m_dest; wxString m_errors; bool m_migrateTables; public: MIGRATION_TRAVERSER( const wxString& aSrcDir, const wxString& aDestDir, bool aMigrateTables ) : m_src( aSrcDir ), m_dest( aDestDir ), m_migrateTables( aMigrateTables ) { } wxString GetErrors() { return m_errors; } wxDirTraverseResult OnFile( const wxString& aSrcFilePath ) override { wxFileName file( aSrcFilePath ); if( !m_migrateTables && ( file.GetName() == wxT( "sym-lib-table" ) || file.GetName() == wxT( "fp-lib-table" ) ) ) { return wxDIR_CONTINUE; } wxString path = file.GetPath(); path.Replace( m_src, m_dest, false ); file.SetPath( path ); wxLogTrace( traceSettings, "Copying %s to %s", aSrcFilePath, file.GetFullPath() ); // For now, just copy everything KiCopyFile( aSrcFilePath, file.GetFullPath(), m_errors ); return wxDIR_CONTINUE; } wxDirTraverseResult OnDir( const wxString& dirPath ) override { wxFileName dir( dirPath ); // Whitelist of directories to migrate if( dir.GetName() == "colors" || dir.GetName() == "3d" ) { wxString path = dir.GetPath(); path.Replace( m_src, m_dest, false ); dir.SetPath( path ); wxMkdir( dir.GetFullPath() ); return wxDIR_CONTINUE; } else { return wxDIR_IGNORE; } } }; bool SETTINGS_MANAGER::MigrateIfNeeded() { if( m_headless ) { wxLogTrace( traceSettings, "Settings migration not checked; running headless" ); return false; } wxFileName path( GetUserSettingsPath(), "" ); wxLogTrace( traceSettings, "Using settings path %s", path.GetFullPath() ); if( path.DirExists() ) { wxFileName common = path; common.SetName( "kicad_common" ); common.SetExt( "json" ); if( common.Exists() ) { wxLogTrace( traceSettings, "Path exists and has a kicad_common, continuing!" ); return true; } } // Now we have an empty path, let's figure out what to put in it DIALOG_MIGRATE_SETTINGS dlg( this ); if( dlg.ShowModal() != wxID_OK ) { wxLogTrace( traceSettings, "Migration dialog canceled; exiting" ); return false; } if( !path.DirExists() ) { wxLogTrace( traceSettings, "Path didn't exist; creating it" ); path.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ); } if( m_migration_source.IsEmpty() ) { wxLogTrace( traceSettings, "No migration source given; starting with defaults" ); return true; } MIGRATION_TRAVERSER traverser( m_migration_source, path.GetFullPath(), m_migrateLibraryTables ); wxDir source_dir( m_migration_source ); source_dir.Traverse( traverser ); if( !traverser.GetErrors().empty() ) DisplayErrorMessage( nullptr, traverser.GetErrors() ); return true; } bool SETTINGS_MANAGER::GetPreviousVersionPaths( std::vector* aPaths ) { wxASSERT( aPaths ); aPaths->clear(); wxDir dir; std::vector base_paths; base_paths.emplace_back( wxFileName( calculateUserSettingsPath( false ), "" ) ); // If the env override is set, also check the default paths if( wxGetEnv( wxT( "KICAD_CONFIG_HOME" ), nullptr ) ) base_paths.emplace_back( wxFileName( calculateUserSettingsPath( false, false ), "" ) ); wxString subdir; std::string mine = GetSettingsVersion(); auto check_dir = [&] ( const wxString& aSubDir ) { // Only older versions are valid for migration if( compareVersions( aSubDir.ToStdString(), mine ) <= 0 ) { wxString sub_path = dir.GetNameWithSep() + aSubDir; if( IsSettingsPathValid( sub_path ) ) { aPaths->push_back( sub_path ); wxLogTrace( traceSettings, "GetPreviousVersionName: %s is valid", sub_path ); } } }; for( auto base_path : base_paths ) { if( !dir.Open( base_path.GetFullPath() ) ) { wxLogTrace( traceSettings, "GetPreviousVersionName: could not open base path %s", base_path.GetFullPath() ); continue; } wxLogTrace( traceSettings, "GetPreviousVersionName: checking base path %s", base_path.GetFullPath() ); if( dir.GetFirst( &subdir, wxEmptyString, wxDIR_DIRS ) ) { if( subdir != mine ) check_dir( subdir ); while( dir.GetNext( &subdir ) ) { if( subdir != mine ) check_dir( subdir ); } } // If we didn't find one yet, check for legacy settings without a version directory if( IsSettingsPathValid( dir.GetNameWithSep() ) ) { wxLogTrace( traceSettings, "GetPreviousVersionName: root path %s is valid", dir.GetName() ); aPaths->push_back( dir.GetName() ); } } return aPaths->size() > 0; } bool SETTINGS_MANAGER::IsSettingsPathValid( const wxString& aPath ) { wxFileName test( aPath, "kicad_common" ); return test.Exists(); } wxString SETTINGS_MANAGER::GetColorSettingsPath() { wxFileName path; path.AssignDir( GetUserSettingsPath() ); path.AppendDir( "colors" ); return path.GetPath(); } wxString SETTINGS_MANAGER::GetUserSettingsPath() { static wxString user_settings_path; if( user_settings_path.empty() ) user_settings_path = calculateUserSettingsPath(); return user_settings_path; } wxString SETTINGS_MANAGER::calculateUserSettingsPath( bool aIncludeVer, bool aUseEnv ) { wxFileName cfgpath; // http://docs.wxwidgets.org/3.0/classwx_standard_paths.html#a7c7cf595d94d29147360d031647476b0 cfgpath.AssignDir( wxStandardPaths::Get().GetUserConfigDir() ); // GetUserConfigDir() does not default to ~/.config which is the current standard // configuration file location on Linux. This has been fixed in later versions of wxWidgets. #if !defined( __WXMSW__ ) && !defined( __WXMAC__ ) wxArrayString dirs = cfgpath.GetDirs(); if( dirs.Last() != ".config" ) cfgpath.AppendDir( ".config" ); #endif wxString envstr; // This shouldn't cause any issues on Windows or MacOS. if( wxGetEnv( wxT( "XDG_CONFIG_HOME" ), &envstr ) && !envstr.IsEmpty() ) { // Override the assignment above with XDG_CONFIG_HOME cfgpath.AssignDir( envstr ); } cfgpath.AppendDir( TO_STR( KICAD_CONFIG_DIR ) ); // Use KICAD_CONFIG_HOME to allow the user to force a specific configuration path. if( aUseEnv && wxGetEnv( wxT( "KICAD_CONFIG_HOME" ), &envstr ) && !envstr.IsEmpty() ) { // Override the assignment above with KICAD_CONFIG_HOME cfgpath.AssignDir( envstr ); } if( aIncludeVer ) cfgpath.AppendDir( GetSettingsVersion() ); return cfgpath.GetPath(); } std::string SETTINGS_MANAGER::GetSettingsVersion() { // CMake computes the major.minor string for us. return GetMajorMinorVersion().ToStdString(); } int SETTINGS_MANAGER::compareVersions( const std::string& aFirst, const std::string& aSecond ) { int a_maj = 0; int a_min = 0; int b_maj = 0; int b_min = 0; if( !extractVersion( aFirst, &a_maj, &a_min ) || !extractVersion( aSecond, &b_maj, &b_min ) ) { wxLogTrace( traceSettings, "compareSettingsVersions: bad input (%s, %s)", aFirst, aSecond ); return -1; } if( a_maj < b_maj ) { return -1; } else if( a_maj > b_maj ) { return 1; } else { if( a_min < b_min ) { return -1; } else if( a_min > b_min ) { return 1; } else { return 0; } } } bool SETTINGS_MANAGER::extractVersion( const std::string& aVersionString, int* aMajor, int* aMinor ) { std::regex re_version( "(\\d+)\\.(\\d+)" ); std::smatch match; if( std::regex_match( aVersionString, match, re_version ) ) { try { *aMajor = std::stoi( match[1].str() ); *aMinor = std::stoi( match[2].str() ); } catch( ... ) { return false; } return true; } return false; } bool SETTINGS_MANAGER::LoadProject( const wxString& aFullPath, bool aSetActive ) { // Normalize path to new format even if migrating from a legacy file wxFileName path( aFullPath ); if( path.GetExt() == LegacyProjectFileExtension ) path.SetExt( ProjectFileExtension ); wxString fullPath = path.GetFullPath(); // If already loaded, we are all set. This might be called more than once over a project's // lifetime in case the project is first loaded by the KiCad manager and then eeschema or // pcbnew try to load it again when they are launched. if( m_projects.count( fullPath ) ) return true; // No MDI yet if( aSetActive && !m_projects.empty() ) { PROJECT* oldProject = m_projects.begin()->second; unloadProjectFile( oldProject, true ); m_projects.erase( m_projects.begin() ); } wxLogTrace( traceSettings, "Load project %s", fullPath ); std::unique_ptr project = std::make_unique(); project->setProjectFullName( fullPath ); bool success = loadProjectFile( *project ); m_projects_list.push_back( std::move( project ) ); m_projects[fullPath] = m_projects_list.back().get(); wxString fn( path.GetName() ); PROJECT_LOCAL_SETTINGS* settings = static_cast( RegisterSettings( new PROJECT_LOCAL_SETTINGS( m_projects[fullPath], fn ) ) ); m_projects[fullPath]->setLocalSettings( settings ); if( m_kiway ) m_kiway->ProjectChanged(); return success; } bool SETTINGS_MANAGER::UnloadProject( PROJECT* aProject, bool aSave ) { if( !aProject || !m_projects.count( aProject->GetProjectFullName() ) ) return false; if( !unloadProjectFile( aProject, aSave ) ) return false; wxString projectPath = aProject->GetProjectFullName(); wxLogTrace( traceSettings, "Unload project %s", projectPath ); PROJECT* toRemove = m_projects.at( projectPath ); auto it = std::find_if( m_projects_list.begin(), m_projects_list.end(), [&]( const std::unique_ptr& ptr ) { return ptr.get() == toRemove; } ); wxASSERT( it != m_projects_list.end() ); m_projects_list.erase( it ); m_projects.erase( projectPath ); // Immediately reload a null project; this is required until the rest of the application // is refactored to not assume that Prj() always works if( m_projects.empty() ) LoadProject( "" ); // Remove the reference in the environment to the previous project wxSetEnv( PROJECT_VAR_NAME, "" ); if( m_kiway ) m_kiway->ProjectChanged(); return true; } PROJECT& SETTINGS_MANAGER::Prj() const { // No MDI yet: First project in the list is the active project return *m_projects.begin()->second; } bool SETTINGS_MANAGER::IsProjectOpen() const { return !m_projects.empty(); } PROJECT* SETTINGS_MANAGER::GetProject( const wxString& aFullPath ) const { if( m_projects.count( aFullPath ) ) return m_projects.at( aFullPath ); return nullptr; } std::vector SETTINGS_MANAGER::GetOpenProjects() const { std::vector ret; for( const std::pair& pair : m_projects ) ret.emplace_back( pair.first ); return ret; } bool SETTINGS_MANAGER::SaveProject( const wxString& aFullPath ) { wxString path = aFullPath; if( path.empty() ) path = Prj().GetProjectFullName(); // TODO: refactor for MDI if( Prj().IsReadOnly() ) return false; if( !m_project_files.count( path ) ) return false; PROJECT_FILE* project = m_project_files.at( path ); wxString projectPath = GetPathForSettingsFile( project ); project->SaveToFile( projectPath ); Prj().GetLocalSettings().SaveToFile( projectPath ); return true; } void SETTINGS_MANAGER::SaveProjectAs( const wxString& aFullPath ) { wxString oldName = Prj().GetProjectFullName(); // Changing this will cause UnloadProject to not save over the "old" project when loading below Prj().setProjectFullName( aFullPath ); wxFileName fn( aFullPath ); PROJECT_FILE* project = m_project_files.at( oldName ); project->SetFilename( fn.GetName() ); project->SaveToFile( fn.GetPath() ); Prj().GetLocalSettings().SetFilename( fn.GetName() ); Prj().GetLocalSettings().SaveToFile( fn.GetPath() ); m_project_files[fn.GetFullPath()] = project; m_project_files.erase( oldName ); m_projects[fn.GetFullPath()] = m_projects[oldName]; m_projects.erase( oldName ); } bool SETTINGS_MANAGER::loadProjectFile( PROJECT& aProject ) { wxFileName fullFn( aProject.GetProjectFullName() ); wxString fn( fullFn.GetName() ); PROJECT_FILE* file = static_cast( RegisterSettings( new PROJECT_FILE( fn ), false ) ); m_project_files[aProject.GetProjectFullName()] = file; aProject.setProjectFile( file ); file->SetProject( &aProject ); wxString path( fullFn.GetPath() ); return file->LoadFromFile( path ); } bool SETTINGS_MANAGER::unloadProjectFile( PROJECT* aProject, bool aSave ) { if( !aProject ) return false; wxString name = aProject->GetProjectFullName(); if( !m_project_files.count( name ) ) return false; PROJECT_FILE* file = m_project_files[name]; auto it = std::find_if( m_settings.begin(), m_settings.end(), [&file]( const std::unique_ptr& aPtr ) { return aPtr.get() == file; } ); if( it != m_settings.end() ) { wxString projectPath = GetPathForSettingsFile( it->get() ); FlushAndRelease( &aProject->GetLocalSettings(), aSave ); if( aSave ) ( *it )->SaveToFile( projectPath ); m_settings.erase( it ); } m_project_files.erase( name ); return true; } wxString SETTINGS_MANAGER::GetProjectBackupsPath() const { return Prj().GetProjectPath() + Prj().GetProjectName() + PROJECT_BACKUPS_DIR_SUFFIX; } wxString SETTINGS_MANAGER::backupDateTimeFormat = wxT( "%Y-%m-%d_%H%M%S" ); bool SETTINGS_MANAGER::BackupProject( REPORTER& aReporter ) const { wxDateTime timestamp = wxDateTime::Now(); wxString fileName = wxString::Format( wxT( "%s-%s" ), Prj().GetProjectName(), timestamp.Format( backupDateTimeFormat ) ); wxFileName target; target.SetPath( GetProjectBackupsPath() ); target.SetName( fileName ); target.SetExt( ArchiveFileExtension ); wxDir dir( target.GetPath() ); if( !target.DirExists() && !wxMkdir( target.GetPath() ) ) { wxLogTrace( traceSettings, "Could not create project backup path %s", target.GetPath() ); return false; } if( !target.IsDirWritable() ) { wxLogTrace( traceSettings, "Backup directory %s is not writeable", target.GetPath() ); return false; } wxLogTrace( traceSettings, "Backing up project to %s", target.GetPath() ); PROJECT_ARCHIVER archiver; return archiver.Archive( Prj().GetProjectPath(), target.GetFullPath(), aReporter ); } class VECTOR_INSERT_TRAVERSER : public wxDirTraverser { public: VECTOR_INSERT_TRAVERSER( std::vector& aVec, std::function aCond ) : m_files( aVec ), m_condition( aCond ) { } wxDirTraverseResult OnFile( const wxString& aFile ) override { if( m_condition( aFile ) ) m_files.emplace_back( aFile ); return wxDIR_CONTINUE; } wxDirTraverseResult OnDir( const wxString& aDirName ) override { return wxDIR_CONTINUE; } private: std::vector& m_files; std::function m_condition; }; bool SETTINGS_MANAGER::TriggerBackupIfNeeded( REPORTER& aReporter ) const { COMMON_SETTINGS::AUTO_BACKUP settings = GetCommonSettings()->m_Backup; if( !settings.enabled ) return true; wxString prefix = Prj().GetProjectName() + '-'; auto modTime = [&prefix]( const wxString& aFile ) { wxDateTime dt; wxString fn( wxFileName( aFile ).GetName() ); fn.Replace( prefix, "" ); dt.ParseFormat( fn, backupDateTimeFormat ); return dt; }; // Project not saved yet if( Prj().GetProjectPath().empty() ) return true; wxString backupPath = GetProjectBackupsPath(); if( !wxDirExists( backupPath ) ) { wxLogTrace( traceSettings, "Backup path %s doesn't exist, creating it", backupPath ); if( !wxMkdir( backupPath ) ) { wxLogTrace( traceSettings, "Could not create backups path! Skipping backup" ); return false; } } wxDir dir( backupPath ); if( !dir.IsOpened() ) { wxLogTrace( traceSettings, "Could not open project backups path %s", dir.GetName() ); return false; } std::vector files; VECTOR_INSERT_TRAVERSER traverser( files, [&modTime]( const wxString& aFile ) { return modTime( aFile ).IsValid(); } ); dir.Traverse( traverser, wxT( "*.zip" ) ); // Sort newest-first std::sort( files.begin(), files.end(), [&]( const wxString& aFirst, const wxString& aSecond ) -> bool { wxDateTime first = modTime( aFirst ); wxDateTime second = modTime( aSecond ); return first.GetTicks() > second.GetTicks(); } ); // Do we even need to back up? if( !files.empty() ) { wxDateTime lastTime = modTime( files[0] ); if( lastTime.IsValid() ) { wxTimeSpan delta = wxDateTime::Now() - modTime( files[0] ); if( delta.IsShorterThan( wxTimeSpan::Seconds( settings.min_interval ) ) ) return true; } } // Now that we know a backup is needed, apply the retention policy // Step 1: if we're over the total file limit, remove the oldest if( !files.empty() && settings.limit_total_files > 0 ) { while( files.size() > static_cast( settings.limit_total_files ) ) { wxRemoveFile( files.back() ); files.pop_back(); } } // Step 2: Stay under the total size limit if( settings.limit_total_size > 0 ) { wxULongLong totalSize = 0; for( const wxString& file : files ) totalSize += wxFileName::GetSize( file ); while( !files.empty() && totalSize > static_cast( settings.limit_total_size ) ) { totalSize -= wxFileName::GetSize( files.back() ); wxRemoveFile( files.back() ); files.pop_back(); } } // Step 3: Stay under the daily limit if( settings.limit_daily_files > 0 && files.size() > 1 ) { wxDateTime day = modTime( files[0] ); int num = 1; wxASSERT( day.IsValid() ); std::vector filesToDelete; for( size_t i = 1; i < files.size(); i++ ) { wxDateTime dt = modTime( files[i] ); if( dt.IsSameDate( day ) ) { num++; if( num > settings.limit_daily_files ) filesToDelete.emplace_back( files[i] ); } else { day = dt; num = 1; } } for( const wxString& file : filesToDelete ) wxRemoveFile( file ); } return BackupProject( aReporter ); }