diff --git a/headers/os/package/CleanUpAdminDirectoryRequest.h b/headers/os/package/CleanUpAdminDirectoryRequest.h new file mode 100644 index 0000000000..925e8611b6 --- /dev/null +++ b/headers/os/package/CleanUpAdminDirectoryRequest.h @@ -0,0 +1,40 @@ +/* + * Copyright 2026, Haiku, Inc. All rights reserved. + * Distributed under the terms of the MIT License. + */ +#ifndef _PACKAGE__CLEAN_UP_ADMIN_DIR_REQUEST_H_ +#define _PACKAGE__CLEAN_UP_ADMIN_DIR_REQUEST_H_ + + +#include + +#include +#include +#include + + +namespace BPackageKit { + + +class CleanUpAdminDirectoryRequest : public BRequest { + typedef BRequest inherited; + +public: + CleanUpAdminDirectoryRequest(const BContext& context, + const BInstallationLocationInfo& location, + time_t cleanupBefore, int32 minStatesToKeep); + virtual ~CleanUpAdminDirectoryRequest(); + + virtual status_t CreateInitialJobs(); + +private: + const BInstallationLocationInfo fLocationInfo; + time_t fCleanupBefore; + int32 fMinimumStatesToKeep; +}; + + +} // namespace BPackageKit + + +#endif // _PACKAGE__CLEAN_UP_ADMIN_DIR_REQUEST_H_ diff --git a/src/bin/pkgman/PackageManager.cpp b/src/bin/pkgman/PackageManager.cpp index 2560f351fc..300963054f 100644 --- a/src/bin/pkgman/PackageManager.cpp +++ b/src/bin/pkgman/PackageManager.cpp @@ -20,6 +20,7 @@ #include #include +#include #include #include #include @@ -28,6 +29,7 @@ #include #include "pkgman.h" +#include "JobStateListener.h" using namespace BPackageKit::BPrivate; @@ -332,6 +334,19 @@ PackageManager::ProgressApplyingChangesDone(InstalledRepository& repository) { printf("[%s] Done.\n", repository.Name().String()); + BInstallationLocationInfo info; + if (BPackageRoster().GetInstallationLocationInfo(repository.Location(), info) == B_OK) { + // Offer to delete older state and transaction directories. + BJobStateListener listener; + BContext context(fDecisionProvider, listener); + + const int days = 30; + time_t before = time(NULL) - days * 24 * 60 * 60; + + CleanUpAdminDirectoryRequest request(context, info, before, 10); + request.Process(); + } + if (BPackageRoster().IsRebootNeeded()) printf("A reboot is necessary to complete the installation process.\n"); } diff --git a/src/kits/package/CleanUpAdminDirectoryRequest.cpp b/src/kits/package/CleanUpAdminDirectoryRequest.cpp new file mode 100644 index 0000000000..995bcdd2c0 --- /dev/null +++ b/src/kits/package/CleanUpAdminDirectoryRequest.cpp @@ -0,0 +1,215 @@ +/* + * Copyright 2026, Haiku, Inc. All rights reserved. + * Distributed under the terms of the MIT License. + */ + + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + + +namespace BPackageKit { + + +using namespace BPrivate; + + +class CleanUpAdminDirectoryJob : public BJob { + typedef BJob inherited; + +public: + CleanUpAdminDirectoryJob( + const BContext& context, const BString& title, + const BInstallationLocationInfo& location, + time_t cleanupBefore, int32 minStatesToKeep); + virtual ~CleanUpAdminDirectoryJob(); + +protected: + virtual status_t Execute(); + +private: + status_t _GetOldStateDirectories(BStringList& directories); + +private: + const BInstallationLocationInfo& fLocationInfo; + time_t fCleanupBefore; + int32 fMinimumStatesToKeep; +}; + + +CleanUpAdminDirectoryJob::CleanUpAdminDirectoryJob(const BContext& context, + const BString& title, const BInstallationLocationInfo& location, + time_t cleanupBefore, int32 minStatesToKeep) + : + inherited(context, title), + fLocationInfo(location), + fCleanupBefore(cleanupBefore), + fMinimumStatesToKeep(minStatesToKeep) +{ +} + + +CleanUpAdminDirectoryJob::~CleanUpAdminDirectoryJob() +{ +} + + +status_t +CleanUpAdminDirectoryJob::Execute() +{ + BStringList oldStatesAndTransactions; + status_t status = _GetOldStateDirectories(oldStatesAndTransactions); + if (status != B_OK) + return status; + if (oldStatesAndTransactions.IsEmpty()) + return B_OK; + + size_t totalSize = 0; + for (int32 i = 0; i < oldStatesAndTransactions.CountStrings(); i++) { + BDirectory dir(oldStatesAndTransactions.StringAt(i)); + BEntry entry; + while (dir.GetNextEntry(&entry, true) == B_OK) { + off_t entrySize; + if (entry.GetSize(&entrySize) == B_OK) + totalSize += entrySize; + } + } + + static BStringFormat format("{0, plural," + "one{Clean up # old state (%size)?} other{Clean up # old states (%size)?}}"); + + BString question; + format.Format(question, oldStatesAndTransactions.CountStrings()); + + char buffer[128]; + question.ReplaceAll("%size", string_for_size(totalSize, buffer, sizeof(buffer))); + + bool yes = fContext.DecisionProvider().YesNoDecisionNeeded("", question, + "yes", "no", "yes"); + if (!yes) + return B_CANCELED; + + for (int32 i = 0; i < oldStatesAndTransactions.CountStrings(); i++) { + BString directory = oldStatesAndTransactions.StringAt(i); + status_t status = BRemoveEngine().RemoveEntry(BRemoveEngine::Entry( + directory)); + + if (status != B_OK) { + SetErrorString(BString().SetToFormat("Failed to remove %s: %s\n", + directory.String(), strerror(status))); + return status; + } + } + + return B_OK; +} + + +status_t +CleanUpAdminDirectoryJob::_GetOldStateDirectories(BStringList& directories) +{ + BDirectory packages(&fLocationInfo.PackagesDirectoryRef()); + BDirectory administrative(&packages, PACKAGES_DIRECTORY_ADMIN_DIRECTORY); + if (administrative.InitCheck() != B_OK) + return administrative.InitCheck(); + + BStringList states, transactions; + BEntry entry; + int32 skippedStates = 0; + while (administrative.GetNextEntry(&entry) == B_OK) { + if (!entry.IsDirectory()) + continue; + + time_t mtime; + if (entry.GetModificationTime(&mtime) != B_OK) + continue; + + BStringList* list = NULL; + BString name = entry.Name(); + if (name.StartsWith("transaction-")) { + if (mtime >= fCleanupBefore) + continue; + + list = &transactions; + } else if (name.StartsWith("state_")) { + if (mtime >= fCleanupBefore) { + skippedStates++; + continue; + } + if (!fLocationInfo.ActiveStateName().IsEmpty() + && name >= fLocationInfo.ActiveStateName()) { + skippedStates++; + continue; + } + + list = &states; + } else { + continue; + } + + list->Add(BPath(&administrative, name).Path()); + } + + states.Sort(); + while (!states.IsEmpty() && skippedStates < fMinimumStatesToKeep) { + states.Remove(states.CountStrings() - 1); + skippedStates++; + } + + directories.MakeEmpty(); + directories.Add(states); + directories.Add(transactions); + return B_OK; +} + + +CleanUpAdminDirectoryRequest::CleanUpAdminDirectoryRequest(const BContext& context, + const BInstallationLocationInfo& location, + time_t cleanupBefore, int32 minStatesToKeep) + : + inherited(context), + fLocationInfo(location), + fCleanupBefore(cleanupBefore), + fMinimumStatesToKeep(minStatesToKeep) +{ +} + + +CleanUpAdminDirectoryRequest::~CleanUpAdminDirectoryRequest() +{ +} + + +status_t +CleanUpAdminDirectoryRequest::CreateInitialJobs() +{ + status_t result = InitCheck(); + if (result != B_OK) + return B_NO_INIT; + + CleanUpAdminDirectoryJob* cleanUpJob + = new (std::nothrow) CleanUpAdminDirectoryJob(fContext, + "", fLocationInfo, fCleanupBefore, fMinimumStatesToKeep); + if (cleanUpJob == NULL) + return B_NO_MEMORY; + if ((result = QueueJob(cleanUpJob)) != B_OK) { + delete cleanUpJob; + return result; + } + + return B_OK; +} + + +} // namespace BPackageKit diff --git a/src/kits/package/Jamfile b/src/kits/package/Jamfile index a4b497a824..2d27d0fb4d 100644 --- a/src/kits/package/Jamfile +++ b/src/kits/package/Jamfile @@ -77,6 +77,7 @@ for architectureObject in [ MultiArchSubDirSetup ] { AddRepositoryRequest.cpp Attributes.cpp ChecksumAccessors.cpp + CleanUpAdminDirectoryRequest.cpp Context.cpp DaemonClient.cpp DownloadFileRequest.cpp