Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
54e93c2
spec(openbuilt-export-to-real-app): Phase-2 export pipeline
rubenvdlinde May 11, 2026
3267bca
feat(openbuilt-export-to-real-app): salvage Phase-2 export pipeline (…
rubenvdlinde May 11, 2026
e540d3a
fix(export): pascalCase camelCase boundaries + drop invalid ISimpleFo…
rubenvdlinde May 11, 2026
b1e26ee
fix(export): drive ExportJob lifecycle via OR TransitionEngine (CRITI…
rubenvdlinde May 11, 2026
27807c7
fix(export): close IDOR + add route-auth on ExportsController (CRITICAL)
rubenvdlinde May 11, 2026
b288afb
fix(export): make exported app a Tier-4 manifest consumer (ADR-024)
rubenvdlinde May 11, 2026
f77ad5e
fix(export): clean up PHPCS/PHPMD — named params, complexity split, e…
rubenvdlinde May 11, 2026
0369f21
fix(tests): make bootstrap work outside the docker container
rubenvdlinde May 11, 2026
0abbd2d
fix(export): drop unused container dep + green PHPStan
rubenvdlinde May 11, 2026
2943aab
test(exporter): cover PAT-handling contract for ExportJob + GitHub se…
rubenvdlinde May 11, 2026
9b74035
test(exporter): cover RunExportJob lifecycle + PAT-clear contract
rubenvdlinde May 11, 2026
ea34cc0
test(exporter): cover ExportsController submit/download HTTP surface
rubenvdlinde May 11, 2026
531f489
test(exporter): add Newman collection for export pipeline
rubenvdlinde May 11, 2026
7ad9d80
test(exporter): add Playwright e2e for ZIP export download
rubenvdlinde May 11, 2026
f026306
fix(ci): regenerate lockfile + stylelint and lint auto-fixes
rubenvdlinde May 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -92,4 +92,8 @@ Vrij en open source onder de EUPL-1.2-licentie.
<step>OCA\OpenBuilt\Repair\PopulateApplicationPermissions</step>
</post-migration>
</repair-steps>

<background-jobs>
<job>OCA\OpenBuilt\BackgroundJob\CleanupExpiredExports</job>
</background-jobs>
</info>
4 changes: 4 additions & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@
// Specific route MUST precede the SPA catch-all (memory rule: Symfony specific-first).
['name' => 'applications#diffVersions', 'url' => '/api/applications/{slug}/versions/diff', 'verb' => 'GET', 'requirements' => ['slug' => '[a-z0-9][a-z0-9-]*[a-z0-9]']],

// Export pipeline (Phase-2 graduation).
['name' => 'exports#submit', 'url' => '/api/applications/{slug}/exports', 'verb' => 'POST', 'requirements' => ['slug' => '[a-z0-9][a-z0-9-]*[a-z0-9]']],
['name' => 'exports#download', 'url' => '/api/exports/{uuid}/download', 'verb' => 'GET'],

// SPA catch-all — same controller as the index route; must use a distinct route name
// (duplicate names replace the earlier route in Symfony, which breaks GET /).
['name' => 'dashboard#catchAll', 'url' => '/{path}', 'verb' => 'GET', 'requirements' => ['path' => '.+'], 'defaults' => ['path' => '']],
Expand Down
5 changes: 4 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@
"autoload": {
"psr-4": {
"OCA\\OpenBuilt\\": "lib/"
}
},
"exclude-from-classmap": [
"lib/Resources/template/"
]
},
"require": {
"php": "^8.1"
Expand Down
26 changes: 25 additions & 1 deletion l10n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,31 @@
"Schema {slug} created.": "Schema {slug} created.",
"Schema {slug} deleted.": "Schema {slug} deleted.",
"Failed to delete schema: {error}": "Failed to delete schema: {error}",
"Schema saved.": "Schema saved."
"Schema saved.": "Schema saved.",
"Export application": "Export application",
"Target": "Target",
"ZIP download": "ZIP download",
"Push to GitHub": "Push to GitHub",
"License": "License",
"GitHub organisation": "GitHub organisation",
"Repository name": "Repository name",
"Visibility": "Visibility",
"Public": "Public",
"Private": "Private",
"GitHub personal access token": "GitHub personal access token",
"The token needs the repo scope. It is sent once over your Nextcloud session, stored encrypted via the credentials manager, and deleted automatically when the export finishes.": "The token needs the repo scope. It is sent once over your Nextcloud session, stored encrypted via the credentials manager, and deleted automatically when the export finishes.",
"Include seed data": "Include seed data",
"Start export": "Start export",
"Queued": "Queued",
"Running": "Running",
"Succeeded": "Succeeded",
"Failed": "Failed",
"Download ZIP": "Download ZIP",
"View pull request": "View pull request",
"Unknown application version.": "Unknown application version.",
"Draft versions cannot be exported.": "Draft versions cannot be exported.",
"Repository already exists in the target organisation.": "Repository already exists in the target organisation.",
"GitHub authentication failed. Please check the token scope and try again.": "GitHub authentication failed. Please check the token scope and try again."
},
"plurals": ""
}
26 changes: 25 additions & 1 deletion l10n/nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,31 @@
"Schema {slug} created.": "Schema {slug} aangemaakt.",
"Schema {slug} deleted.": "Schema {slug} verwijderd.",
"Failed to delete schema: {error}": "Schema verwijderen mislukt: {error}",
"Schema saved.": "Schema opgeslagen."
"Schema saved.": "Schema opgeslagen.",
"Export application": "Applicatie exporteren",
"Target": "Doel",
"ZIP download": "ZIP-download",
"Push to GitHub": "Push naar GitHub",
"License": "Licentie",
"GitHub organisation": "GitHub-organisatie",
"Repository name": "Repository-naam",
"Visibility": "Zichtbaarheid",
"Public": "Openbaar",
"Private": "Privé",
"GitHub personal access token": "GitHub persoonlijk toegangstoken",
"The token needs the repo scope. It is sent once over your Nextcloud session, stored encrypted via the credentials manager, and deleted automatically when the export finishes.": "Het token heeft de repo-scope nodig. Het wordt eenmalig via je Nextcloud-sessie verzonden, versleuteld opgeslagen via de credentials-manager en automatisch verwijderd als de export klaar is.",
"Include seed data": "Voorbeeldgegevens meenemen",
"Start export": "Export starten",
"Queued": "In wachtrij",
"Running": "Wordt uitgevoerd",
"Succeeded": "Gelukt",
"Failed": "Mislukt",
"Download ZIP": "ZIP downloaden",
"View pull request": "Pull request bekijken",
"Unknown application version.": "Onbekende applicatieversie.",
"Draft versions cannot be exported.": "Conceptversies kunnen niet worden geëxporteerd.",
"Repository already exists in the target organisation.": "Repository bestaat al in de doelorganisatie.",
"GitHub authentication failed. Please check the token scope and try again.": "GitHub-authenticatie is mislukt. Controleer de tokenscope en probeer het opnieuw."
},
"plurals": ""
}
97 changes: 97 additions & 0 deletions lib/BackgroundJob/CleanupExpiredExports.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<?php

/**
* OpenBuilt Cleanup Expired Exports
*
* Daily background job that purges expired ZIP archives from app-data while
* preserving the ExportJob audit trail.
*
* @category BackgroundJob
* @package OCA\OpenBuilt\BackgroundJob
*
* @author Conduction Development Team <dev@conduction.nl>
* @copyright 2026 Conduction B.V.
* @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
*
* @version GIT: <git-id>
*
* @link https://conduction.nl
*
* @SPDX-License-Identifier: EUPL-1.2
* @SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>
*/

declare(strict_types=1);

namespace OCA\OpenBuilt\BackgroundJob;

use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\TimedJob;
use Psr\Log\LoggerInterface;

/**
* 24-hour cleanup job for expired export archives.
*/
class CleanupExpiredExports extends TimedJob
{
/**
* Constructor.
*
* @param ITimeFactory $time Time factory.
* @param LoggerInterface $logger Logger.
*/
public function __construct(
ITimeFactory $time,
private LoggerInterface $logger,
) {
parent::__construct(time: $time);
$this->setInterval(seconds: 86400);
}//end __construct()

/**
* Iterate ExportJobs with `downloadExpiresAt < now()` and unlink ZIPs.
*
* Preserves the ExportJob OR record — only the ZIP file is purged
* (audit trail remains intact). Idempotent.
*
* @param mixed $argument Job argument injected by Nextcloud. Unused —
* we always scan the same fixed location.
*
* @return void
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
protected function run($argument): void
{
unset($argument);

$exportsRoot = sys_get_temp_dir().'/openbuilt-exports';
if (is_dir($exportsRoot) === false) {
return;
}

$now = time();
$expiryWindow = 86400;
// 24h
$purged = 0;
$zipPaths = glob($exportsRoot.'/*.zip');
if ($zipPaths === false) {
$zipPaths = [];
}

foreach ($zipPaths as $zip) {
$mtime = filemtime($zip);
if ($mtime !== false && ($now - $mtime) > $expiryWindow) {
// Suppress unlink warnings — concurrent cleanup of the same
// ZIP from a sibling worker is harmless and need not be logged.
if (unlink($zip) === true) {
$purged++;
}
}
}

if ($purged > 0) {
$this->logger->info('OpenBuilt cleanup: purged '.$purged.' expired export archive(s)');
}
}//end run()
}//end class
Loading
Loading