* Copyright 2017-2025, Andrew Lindesay <apl@lindesay.co.nz>.
* All rights reserved. Distributed under the terms of the MIT License.
*/
#include "ServerPkgDataUpdateProcess.h"
#include <stdio.h>
#include <sys/stat.h>
#include <time.h>
#include <AutoDeleter.h>
#include <AutoLocker.h>
#include <Catalog.h>
#include <FileIO.h>
#include <StopWatch.h>
#include <Url.h>
#include "DumpExportPkg.h"
#include "DumpExportPkgCategory.h"
#include "DumpExportPkgJsonListener.h"
#include "DumpExportPkgScreenshot.h"
#include "DumpExportPkgVersion.h"
#include "HaikuDepotConstants.h"
#include "Logger.h"
#include "PackageUtils.h"
#include "ServerSettings.h"
#include "StorageUtils.h"
#undef B_TRANSLATION_CONTEXT
#define B_TRANSLATION_CONTEXT "ServerPkgDataUpdateProcess"
static std::size_t kPackageBatchSize = 100;
static uint32 kPackageChangeMask = PKG_CHANGED_RATINGS | PKG_CHANGED_SCREENSHOTS
| PKG_CHANGED_CLASSIFICATION | PKG_CHANGED_LOCALIZED_TEXT | PKG_CHANGED_LOCAL_INFO
| PKG_CHANGED_CORE_INFO;
packages as they are parsed and processing them.
*/
class PackageFillingPkgListener : public DumpExportPkgListener {
public:
PackageFillingPkgListener(Model *model, Stoppable* stoppable);
virtual ~PackageFillingPkgListener();
virtual bool Handle(DumpExportPkg* item);
virtual void Complete();
uint32 Count();
private:
static ScreenshotInfoRef _CreateScreenshot(DumpExportPkgScreenshot* screenshot);
void _FlushUpdatedPackages();
const PackageInfoRef
_PackageForName(const BString& name) const;
const PackageInfoRef
_CreateUpdatePackage(const PackageInfoRef& package,
const DumpExportPkg* pkg);
void _InitCategories();
private:
Model* fModel;
std::map<BString, CategoryRef>
fCategories;
std::vector<PackageInfoRef>
fUpdatedPackages;
Stoppable* fStoppable;
uint32 fCount;
bool fDebugEnabled;
};
PackageFillingPkgListener::PackageFillingPkgListener(Model* model, Stoppable* stoppable)
:
fModel(model),
fStoppable(stoppable),
fCount(0),
fDebugEnabled(Logger::IsDebugEnabled())
{
_InitCategories();
}
PackageFillingPkgListener::~PackageFillingPkgListener()
{
}
void
PackageFillingPkgListener::_InitCategories()
{
std::vector<CategoryRef> categories = fModel->Categories();
std::vector<CategoryRef>::const_iterator it;
for (it = categories.begin(); it != categories.end(); it++) {
const CategoryRef& category = *it;
fCategories[category->Code()] = category;
}
if (fCategories.empty())
HDERROR("there are no categories present");
}
const PackageInfoRef
PackageFillingPkgListener::_PackageForName(const BString& name) const
{
return fModel->PackageForName(name);
}
void
PackageFillingPkgListener::_FlushUpdatedPackages()
{
fModel->AddPackagesWithChange(fUpdatedPackages, kPackageChangeMask);
fUpdatedPackages.clear();
}
server.
*/
const PackageInfoRef
PackageFillingPkgListener::_CreateUpdatePackage(const PackageInfoRef& package,
const DumpExportPkg* pkg)
{
int32 i;
PackageClassificationInfoBuilder classificationInfoBuilder(
package->PackageClassificationInfo());
PackageLocalizedTextBuilder localizedTextBuilder(package->LocalizedText());
PackageLocalInfoBuilder localInfoBuilder(package->LocalInfo());
PackageCoreInfoBuilder coreInfoBuilder(package->CoreInfo());
PackageScreenshotInfoBuilder screenshotInfoBuilder;
PackageUserRatingInfoBuilder userRatingBuilder;
localizedTextBuilder.WithHasChangelog(pkg->HasChangelog());
if (0 != pkg->CountPkgVersions()) {
DumpExportPkgVersion* pkgVersion = pkg->PkgVersionsItemAt(0);
if (!pkgVersion->TitleIsNull())
localizedTextBuilder.WithTitle(*(pkgVersion->Title()));
if (!pkgVersion->SummaryIsNull())
localizedTextBuilder.WithSummary(*(pkgVersion->Summary()));
if (!pkgVersion->DescriptionIsNull())
localizedTextBuilder.WithDescription(*(pkgVersion->Description()));
if (!pkgVersion->PayloadLengthIsNull())
localInfoBuilder.WithSize(static_cast<off_t>(pkgVersion->PayloadLength()));
if (!pkgVersion->CreateTimestampIsNull()) {
PackageVersionRef versionRef = PackageUtils::Version(package);
if (versionRef.IsSet()) {
PackageVersion* version = versionRef.Get();
versionRef = PackageVersionRef(
new PackageVersion(*version, pkgVersion->CreateTimestamp()), true);
} else {
versionRef
= PackageVersionRef(new PackageVersion(pkgVersion->CreateTimestamp()), true);
}
coreInfoBuilder.WithVersion(versionRef);
}
}
if (!pkg->DerivedRatingIsNull()) {
userRatingBuilder.WithSummary(UserRatingSummaryBuilder()
.WithAverageRating(pkg->DerivedRating())
.WithRatingCount(pkg->DerivedRatingSampleSize())
.BuildRef());
}
int32 countPkgCategories = pkg->CountPkgCategories();
for (i = 0; i < countPkgCategories; i++) {
BString* categoryCode = pkg->PkgCategoriesItemAt(i)->Code();
CategoryRef category = fCategories[*categoryCode];
if (!category.IsSet())
HDERROR("unable to find the category for [%s]", categoryCode->String());
else
classificationInfoBuilder.AddCategory(category);
}
if (!pkg->ProminenceOrderingIsNull())
classificationInfoBuilder.WithProminence(static_cast<uint32>(pkg->ProminenceOrdering()));
if (!pkg->IsDesktopIsNull())
classificationInfoBuilder.WithIsDesktop(pkg->IsDesktop());
if (!pkg->IsNativeDesktopIsNull())
classificationInfoBuilder.WithIsNativeDesktop(pkg->IsNativeDesktop());
int32 countPkgScreenshots = pkg->CountPkgScreenshots();
for (i = 0; i < countPkgScreenshots; i++) {
DumpExportPkgScreenshot* screenshot = pkg->PkgScreenshotsItemAt(i);
screenshotInfoBuilder.AddScreenshot(_CreateScreenshot(screenshot));
}
return PackageInfoBuilder(package)
.WithScreenshotInfo(screenshotInfoBuilder.BuildRef())
.WithUserRatingInfo(userRatingBuilder.BuildRef())
.WithLocalizedText(localizedTextBuilder.BuildRef())
.WithLocalInfo(localInfoBuilder.BuildRef())
.WithCoreInfo(coreInfoBuilder.BuildRef())
.WithPackageClassificationInfo(classificationInfoBuilder.BuildRef())
.BuildRef();
}
ScreenshotInfoRef
PackageFillingPkgListener::_CreateScreenshot(DumpExportPkgScreenshot* screenshot)
{
return ScreenshotInfoRef(
new ScreenshotInfo(*(screenshot->Code()), static_cast<int32>(screenshot->Width()),
static_cast<int32>(screenshot->Height()), static_cast<int32>(screenshot->Length())),
true);
}
uint32
PackageFillingPkgListener::Count()
{
return fCount;
}
bool
PackageFillingPkgListener::Handle(DumpExportPkg* pkg)
{
const BString packageName = *(pkg->Name());
PackageInfoRef package = _PackageForName(packageName);
if (package.IsSet()) {
fUpdatedPackages.push_back(_CreateUpdatePackage(package, pkg));
HDTRACE("did populate data for [%s]", pkg->Name()->String());
fCount++;
} else {
HDINFO("[PackageFillingPkgListener] unable to find the pkg [%s]", packageName.String());
}
if (fUpdatedPackages.size() > kPackageBatchSize)
_FlushUpdatedPackages();
return !fStoppable->WasStopped();
}
void
PackageFillingPkgListener::Complete()
{
_FlushUpdatedPackages();
}
ServerPkgDataUpdateProcess::ServerPkgDataUpdateProcess(BString depotName, Model* model,
uint32 serverProcessOptions)
:
AbstractSingleFileServerProcess(serverProcessOptions),
fModel(model),
fDepotName(depotName)
{
fName.SetToFormat("ServerPkgDataUpdateProcess<%s>", depotName.String());
fDescription.SetTo(B_TRANSLATE("Synchronizing package data for repository '%REPO_NAME%'"));
fDescription.ReplaceAll("%REPO_NAME%", depotName.String());
}
ServerPkgDataUpdateProcess::~ServerPkgDataUpdateProcess()
{
}
const char*
ServerPkgDataUpdateProcess::Name() const
{
return fName.String();
}
const char*
ServerPkgDataUpdateProcess::Description() const
{
return fDescription.String();
}
BString
ServerPkgDataUpdateProcess::UrlPathComponent()
{
BString urlPath;
urlPath.SetToFormat("/__pkg/all-%s-%s.json.gz", _DeriveWebAppRepositorySourceCode().String(),
fModel->PreferredLanguage()->ID());
return urlPath;
}
status_t
ServerPkgDataUpdateProcess::GetLocalPath(BPath& path) const
{
BString webAppRepositorySourceCode = _DeriveWebAppRepositorySourceCode();
if (!webAppRepositorySourceCode.IsEmpty()) {
return StorageUtils::DumpExportPkgDataPath(path, webAppRepositorySourceCode,
fModel->PreferredLanguage());
}
return B_ERROR;
}
status_t
ServerPkgDataUpdateProcess::ProcessLocalData()
{
BStopWatch watch("ServerPkgDataUpdateProcess::ProcessLocalData", true);
PackageFillingPkgListener* itemListener = new PackageFillingPkgListener(fModel, this);
ObjectDeleter<PackageFillingPkgListener> itemListenerDeleter(itemListener);
BulkContainerDumpExportPkgJsonListener* listener
= new BulkContainerDumpExportPkgJsonListener(itemListener);
ObjectDeleter<BulkContainerDumpExportPkgJsonListener> listenerDeleter(listener);
BPath localPath;
status_t result = GetLocalPath(localPath);
if (result != B_OK)
return result;
result = ParseJsonFromFileWithListener(listener, localPath);
if (B_OK != result)
return result;
if (Logger::IsInfoEnabled()) {
double secs = watch.ElapsedTime() / 1000000.0;
HDINFO("[%s] did process %" B_PRIi32 " packages' data in (%6.3g secs)", Name(),
itemListener->Count(), secs);
}
return listener->ErrorStatus();
}
status_t
ServerPkgDataUpdateProcess::GetStandardMetaDataPath(BPath& path) const
{
return GetLocalPath(path);
}
void
ServerPkgDataUpdateProcess::GetStandardMetaDataJsonPath(BString& jsonPath) const
{
jsonPath.SetTo("$.info");
}
BString
ServerPkgDataUpdateProcess::_DeriveWebAppRepositorySourceCode() const
{
const DepotInfo* depot = fModel->DepotForName(fDepotName);
if (depot == NULL)
return BString();
return depot->WebAppRepositorySourceCode();
}
status_t
ServerPkgDataUpdateProcess::RunInternal()
{
if (_DeriveWebAppRepositorySourceCode().IsEmpty()) {
HDINFO("[%s] am not updating data for depot [%s] as there is no web app repository source "
"code available",
Name(), fDepotName.String());
return B_OK;
}
return AbstractSingleFileServerProcess::RunInternal();
}