diff --git a/.github/workflows/api-test.yml b/.github/workflows/api-test.yml deleted file mode 100644 index f6dcc92bd..000000000 --- a/.github/workflows/api-test.yml +++ /dev/null @@ -1,112 +0,0 @@ -name: Test Repomanager API - -on: - push: - branches: [ devel ] - pull_request: - push: - branches: [ main ] - -jobs: - api-test: - runs-on: ubuntu-latest - permissions: - contents: read - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Install curl - run: sudo apt-get install curl jq wget -y - - # Wait for the target URL to be ready - # Generally after the 30 first minutes of the current hour - - name: Wait until API URL is ready - run: | - while (true); do - if $(date +%M | grep -q '^[3-5][0-9]$'); then - break - fi - - sleep 60 - done - - - name: Check if API is reachable - run: | - RESULT=$(curl -s -q --fail-with-body ${{ secrets.API_TEST_HOST }}) - if [ $? -ne 0 ]; then - echo "API is not unreachable" - exit 1 - fi - - # - # Download rpm and deb packages from https://packages.repomanager.net that will be used for upload testing - # To avoid an upload test failure due to the package already been uploaded (when multiple pipelines run at the same time), prefix them with the hostname of the CI runner - # - - name: Download test packages - run: | - wget -O ${HOSTNAME}_nginx-1.26.0-1.el7.ngx.x86_64.rpm https://packages.repomanager.net/repo/ci-test_prod/packages/x86_64/nginx-1.26.0-1.el7.ngx.x86_64.rpm - wget -O ${HOSTNAME}_nginx_1.26.0-1~bookworm_amd64.deb https://packages.repomanager.net/repo/ci-test/bookworm/main_prod/pool/main/nginx_1.26.0-1~bookworm_amd64.deb - - # - # Run tests as admin user - # - - # Register a host and retrieve the host Id and token from results - # Then retrieve and export the host ID and token as environment variables for later steps - - name: (admin) Try to register a host with an admin API key - run: | - RESULT=$(curl --fail-with-body --post301 -s -q -L -X POST -H "Authorization: Bearer ${{ secrets.API_TEST_ADMIN_API_KEY }}" -H "Content-Type: application/json" -d '{"hostname":"test.local","ip":"1.2.3.4"}' ${{ secrets.API_TEST_HOST }}/api/v2/host/registering) - if [ $? -ne 0 ]; then - echo "Failed to register host" - exit 1 - fi - - HOST_ID=$(echo "$RESULT" | jq .results.id | tr -d '"') - HOST_TOKEN=$(echo "$RESULT" | jq -r .results.token | tr -d '"') - - if [ -z "$HOST_ID" ] || [ -z "$HOST_TOKEN" ]; then - echo "Failed to retrieve host ID or token" - exit 1 - fi - - echo "HOST_ID=$HOST_ID" >> $GITHUB_ENV - echo "HOST_TOKEN=$HOST_TOKEN" >> $GITHUB_ENV - - # Unregister the host using the retrieved host ID and token - - name: (admin) Try to unregister a host with an admin API key - run: | - curl --fail-with-body -L -s -q -X DELETE -H "Authorization: Host $HOST_ID:$HOST_TOKEN" -H "Content-Type: application/json" ${{ secrets.API_TEST_HOST }}/api/v2/host/registering - if [ $? -ne 0 ]; then - echo "Failed to unregister host" - exit 1 - fi - - # Upload a deb package using the admin API key - # Generate a unique package name to avoid conflicts when multiple pipelines run at the same time - - name: (admin) Try to upload a deb package with an admin API key - run: | - RANDOM=$(date +%s) - PACKAGE_NAME="${RANDOM}-${HOSTNAME}-admin_nginx_1.26.0-1~bookworm_amd64.deb" - - cp ${HOSTNAME}_nginx_1.26.0-1~bookworm_amd64.deb $PACKAGE_NAME - curl --fail-with-body -L --post301 -s -q -X POST -H "Authorization: Bearer ${{ secrets.API_TEST_ADMIN_API_KEY }}" -F "files=@${PACKAGE_NAME}" https://ci-test.repomanager.net/api/v2/snapshot/1/upload - if [ $? -ne 0 ]; then - echo "Failed to upload deb package" - exit 1 - fi - - # Upload an rpm package using the admin API key - # Generate a unique package name to avoid conflicts when multiple pipelines run at the same time - - name: (admin) Try to upload an rpm package with an admin API key - run: | - RANDOM=$(date +%s) - PACKAGE_NAME="${RANDOM}-${HOSTNAME}-admin_nginx-1.26.0-1.el7.ngx.x86_64.rpm" - - cp ${HOSTNAME}_nginx-1.26.0-1.el7.ngx.x86_64.rpm $PACKAGE_NAME - curl --fail-with-body -L --post301 -s -q -X POST -H "Authorization: Bearer ${{ secrets.API_TEST_ADMIN_API_KEY }}" -F "files=@${PACKAGE_NAME}" https://ci-test.repomanager.net/api/v2/snapshot/2/upload - if [ $? -ne 0 ]; then - echo "Failed to upload rpm package" - exit 1 - fi - diff --git a/.github/workflows/test-api.yml b/.github/workflows/test-api.yml new file mode 100644 index 000000000..adf796606 --- /dev/null +++ b/.github/workflows/test-api.yml @@ -0,0 +1,259 @@ +name: Test Repomanager API + +on: + push: + branches: [ devel ] + pull_request: + push: + branches: [ main ] + +jobs: + api-test: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install curl + run: sudo apt-get install curl jq wget -y + + # Wait for the API to be ready + # Generally after the 30 first minutes of the current hour (xx:30 to xx:59) + - name: Wait until API URL is ready + run: | + while (true); do + if $(date +%M | grep -q '^[3-5][0-9]$'); then + break + fi + + sleep 60 + done + + - name: Check if API is reachable + run: | + RESULT=$(curl -s -q --fail-with-body ${{ secrets.API_TEST_HOST }}) + if [ $? -ne 0 ]; then + echo "API is not unreachable" + exit 1 + fi + + # + # Download rpm and deb packages from https://packages.repomanager.net that will be used for upload testing + # To avoid an upload test failure due to the package already been uploaded (when multiple pipelines run at the same time), prefix them with the hostname of the CI runner + # + - name: Download test packages + run: | + wget -O ${HOSTNAME}_nginx-1.26.0-1.el7.ngx.x86_64.rpm https://packages.repomanager.net/repo/ci-test_prod/packages/x86_64/nginx-1.26.0-1.el7.ngx.x86_64.rpm + wget -O ${HOSTNAME}_nginx_1.26.0-1~bookworm_amd64.deb https://packages.repomanager.net/repo/ci-test/bookworm/main_prod/pool/main/nginx_1.26.0-1~bookworm_amd64.deb + + + # + # Run tests as admin user + # + + # Register a host and retrieve the host Id and token from results + # Then retrieve and export the host ID and token as environment variables for later steps + - name: (admin) Try to register a host with an admin API key + run: | + RESULT=$(curl --fail-with-body --post301 -s -q -L -X POST -H "Authorization: Bearer ${{ secrets.API_TEST_ADMIN_API_KEY }}" -H "Content-Type: application/json" -d '{"hostname":"test.local","ip":"1.2.3.4"}' ${{ secrets.API_TEST_HOST }}/api/v2/host/registering) + if [ $? -ne 0 ]; then + echo "Failed to register host" + exit 1 + fi + + HOST_ID=$(echo "$RESULT" | jq .results.id | tr -d '"') + HOST_TOKEN=$(echo "$RESULT" | jq -r .results.token | tr -d '"') + + if [ -z "$HOST_ID" ] || [ -z "$HOST_TOKEN" ]; then + echo "Failed to retrieve host ID or token" + exit 1 + fi + + echo "HOST_ID=$HOST_ID" >> $GITHUB_ENV + echo "HOST_TOKEN=$HOST_TOKEN" >> $GITHUB_ENV + + # Unregister the host using the retrieved host ID and token + - name: (admin) Try to unregister a host with an admin API key + run: | + curl --fail-with-body -L -s -q -X DELETE -H "Authorization: Host $HOST_ID:$HOST_TOKEN" -H "Content-Type: application/json" ${{ secrets.API_TEST_HOST }}/api/v2/host/registering + if [ $? -ne 0 ]; then + echo "Failed to unregister host" + exit 1 + fi + + # Upload a deb package using the admin API key + # Generate a unique package name to avoid conflicts when multiple pipelines run at the same time + - name: (admin) Try to upload a deb package with an admin API key + run: | + RANDOM=$(date +%s) + PACKAGE_NAME="${RANDOM}-${HOSTNAME}-admin_nginx_1.26.0-1~bookworm_amd64.deb" + + cp ${HOSTNAME}_nginx_1.26.0-1~bookworm_amd64.deb $PACKAGE_NAME + curl --fail-with-body -L --post301 -s -q -X POST -H "Authorization: Bearer ${{ secrets.API_TEST_ADMIN_API_KEY }}" -F "files=@${PACKAGE_NAME}" ${{ secrets.API_TEST_HOST }}/api/v2/snapshot/1/upload + if [ $? -ne 0 ]; then + echo "Failed to upload deb package" + exit 1 + fi + + # Upload a rpm package using the admin API key + # Generate a unique package name to avoid conflicts when multiple pipelines run at the same time + - name: (admin) Try to upload an rpm package with an admin API key + run: | + RANDOM=$(date +%s) + PACKAGE_NAME="${RANDOM}-${HOSTNAME}-admin_nginx-1.26.0-1.el7.ngx.x86_64.rpm" + + cp ${HOSTNAME}_nginx-1.26.0-1.el7.ngx.x86_64.rpm $PACKAGE_NAME + curl --fail-with-body -L --post301 -s -q -X POST -H "Authorization: Bearer ${{ secrets.API_TEST_ADMIN_API_KEY }}" -F "files=@${PACKAGE_NAME}" ${{ secrets.API_TEST_HOST }}/api/v2/snapshot/2/upload + if [ $? -ne 0 ]; then + echo "Failed to upload rpm package" + exit 1 + fi + + # Launch a rebuild of the snapshot metadata for the deb repository + - name: (admin) Try to rebuild deb snapshot metadata with an admin API key + run: | + curl --fail-with-body -L -s -q -X PUT -H "Authorization: Bearer ${{ secrets.API_TEST_ADMIN_API_KEY }}" -d '{"gpgSign":"true"}' ${{ secrets.API_TEST_HOST }}/api/v2/snapshot/1/rebuild + if [ $? -ne 0 ]; then + echo "Failed to rebuild deb snapshot metadata" + exit 1 + fi + + # Launch a rebuild of the snapshot metadata for the rpm repository + - name: (admin) Try to rebuild rpm snapshot metadata with an admin API key + run: | + curl --fail-with-body -L -s -q -X PUT -H "Authorization: Bearer ${{ secrets.API_TEST_ADMIN_API_KEY }}" -d '{"gpgSign":"true"}' ${{ secrets.API_TEST_HOST }}/api/v2/snapshot/2/rebuild + if [ $? -ne 0 ]; then + echo "Failed to rebuild rpm snapshot metadata" + exit 1 + fi + + + # + # Run tests as a regular user: test-user1 + # This user has absolutely no permissions to do anything so all the tests must fail + # + + # Upload a deb package using the test-user1 API key + # Generate a unique package name to avoid conflicts when multiple pipelines run at the same time + - name: (test-user1) Try to upload a deb package with test-user1 API key + run: | + RANDOM=$(date +%s) + PACKAGE_NAME="${RANDOM}-${HOSTNAME}-test-user1_nginx_1.26.0-1~bookworm_amd64.deb" + + cp ${HOSTNAME}_nginx_1.26.0-1~bookworm_amd64.deb $PACKAGE_NAME + OUTPUT=$(curl --fail-with-body -L --post301 -s -q -X POST -H "Authorization: Bearer ${{ secrets.API_TEST_TEST_USER1_API_KEY }}" -F "files=@${PACKAGE_NAME}" ${{ secrets.API_TEST_HOST }}/api/v2/snapshot/3/upload) || true + + if echo "$OUTPUT" | grep -q "You are not allowed to upload packages"; then + echo "Correctly failed to upload deb package with test-user1 API key" + else + echo "Failed to fail the upload deb package with test-user1 API key" + exit 1 + fi + + # Upload a rpm package using the test-user1 API key + # Generate a unique package name to avoid conflicts when multiple pipelines run at the same time + - name: (test-user1) Try to upload an rpm package with test-user1 API key + run: | + RANDOM=$(date +%s) + PACKAGE_NAME="${RANDOM}-${HOSTNAME}-test-user1_nginx-1.26.0-1.el7.ngx.x86_64.rpm" + + cp ${HOSTNAME}_nginx-1.26.0-1.el7.ngx.x86_64.rpm $PACKAGE_NAME + OUTPUT=$(curl --fail-with-body -L --post301 -s -q -X POST -H "Authorization: Bearer ${{ secrets.API_TEST_TEST_USER1_API_KEY }}" -F "files=@${PACKAGE_NAME}" ${{ secrets.API_TEST_HOST }}/api/v2/snapshot/4/upload) || true + + if echo "$OUTPUT" | grep -q "You are not allowed to upload packages"; then + echo "Correctly failed to upload deb package with test-user1 API key" + else + echo "Failed to fail the upload deb package with test-user1 API key" + exit 1 + fi + + # Launch a rebuild of the snapshot metadata for the deb repository + - name: (test-user1) Try to rebuild deb snapshot metadata with test-user1 API key + run: | + OUTPUT=$(curl --fail-with-body -L -s -q -X PUT -H "Authorization: Bearer ${{ secrets.API_TEST_TEST_USER1_API_KEY }}" -d '{"gpgSign":"true"}' ${{ secrets.API_TEST_HOST }}/api/v2/snapshot/3/rebuild) || true + if echo "$OUTPUT" | grep -q "You are not allowed to rebuild a repository snapshot"; then + echo "Correctly failed to rebuild deb snapshot metadata with test-user1 API key" + else + echo "Failed to fail the rebuild deb snapshot metadata with test-user1 API key" + exit 1 + fi + + # Launch a rebuild of the snapshot metadata for the rpm repository + - name: (test-user1) Try to rebuild rpm snapshot metadata with test-user1 API key + run: | + OUTPUT=$(curl --fail-with-body -L -s -q -X PUT -H "Authorization: Bearer ${{ secrets.API_TEST_TEST_USER1_API_KEY }}" -d '{"gpgSign":"true"}' ${{ secrets.API_TEST_HOST }}/api/v2/snapshot/4/rebuild) || true + if echo "$OUTPUT" | grep -q "You are not allowed to rebuild a repository snapshot"; then + echo "Correctly failed to rebuild rpm snapshot metadata with test-user1 API key" + else + echo "Failed to fail the rebuild rpm snapshot metadata with test-user1 API key" + exit 1 + fi + + + # + # Run tests as a regular user: test-user2 + # This user has permissions to upload packages and rebuild snapshots metadata + # + + # Upload a deb package using the test-user2 API key + # Generate a unique package name to avoid conflicts when multiple pipelines run at the same time + - name: (test-user2) Try to upload a deb package with test-user2 API key + run: | + RANDOM=$(date +%s) + PACKAGE_NAME="${RANDOM}-${HOSTNAME}-test-user2_nginx_1.26.0-1~bookworm_amd64.deb" + + cp ${HOSTNAME}_nginx_1.26.0-1~bookworm_amd64.deb $PACKAGE_NAME + curl --fail-with-body -L --post301 -s -q -X POST -H "Authorization: Bearer ${{ secrets.API_TEST_TEST_USER2_API_KEY }}" -F "files=@${PACKAGE_NAME}" ${{ secrets.API_TEST_HOST }}/api/v2/snapshot/5/upload + if [ $? -ne 0 ]; then + echo "Failed to upload deb package" + exit 1 + fi + + # Upload a rpm package using the test-user2 API key + # Generate a unique package name to avoid conflicts when multiple pipelines run at the same time + - name: (test-user2) Try to upload an rpm package with test-user2 API key + run: | + RANDOM=$(date +%s) + PACKAGE_NAME="${RANDOM}-${HOSTNAME}-test-user2_nginx-1.26.0-1.el7.ngx.x86_64.rpm" + + cp ${HOSTNAME}_nginx-1.26.0-1.el7.ngx.x86_64.rpm $PACKAGE_NAME + curl --fail-with-body -L --post301 -s -q -X POST -H "Authorization: Bearer ${{ secrets.API_TEST_TEST_USER2_API_KEY }}" -F "files=@${PACKAGE_NAME}" ${{ secrets.API_TEST_HOST }}/api/v2/snapshot/6/upload + if [ $? -ne 0 ]; then + echo "Failed to upload rpm package" + exit 1 + fi + + # Launch a rebuild of the snapshot metadata for the deb repository + - name: (test-user2) Try to rebuild deb snapshot metadata with test-user2 API key + run: | + curl --fail-with-body -L -s -q -X PUT -H "Authorization: Bearer ${{ secrets.API_TEST_TEST_USER2_API_KEY }}" -d '{"gpgSign":"true"}' ${{ secrets.API_TEST_HOST }}/api/v2/snapshot/5/rebuild + if [ $? -ne 0 ]; then + echo "Failed to rebuild deb snapshot metadata" + exit 1 + fi + + # Launch a rebuild of the snapshot metadata for the rpm repository + - name: (test-user2) Try to rebuild rpm snapshot metadata with test-user2 API key + run: | + curl --fail-with-body -L -s -q -X PUT -H "Authorization: Bearer ${{ secrets.API_TEST_TEST_USER2_API_KEY }}" -d '{"gpgSign":"true"}' ${{ secrets.API_TEST_HOST }}/api/v2/snapshot/6/rebuild + if [ $? -ne 0 ]; then + echo "Failed to rebuild rpm snapshot metadata" + exit 1 + fi + + + # + # Additional tests + # + + # List all repositories + - name: List all repositories + run: | + curl --fail-with-body -s -q -X GET -H "Authorization: Bearer ${{ secrets.API_TEST_ADMIN_API_KEY }}" ${{ secrets.API_TEST_HOST }}/api/v2/repo/ | jq + + # List all repositories with name "local-deb-for-admin" + - name: List repositories with specific name + run: | + curl --fail-with-body -s -q -X GET -H "Authorization: Bearer ${{ secrets.API_TEST_ADMIN_API_KEY }}" ${{ secrets.API_TEST_HOST }}/api/v2/repo/ | jq -r '.results[] | select(.Name == "local-deb-for-admin")' diff --git a/www/controllers/Api/Repo/Repo.php b/www/controllers/Api/Repo/Repo.php new file mode 100644 index 000000000..1297a390a --- /dev/null +++ b/www/controllers/Api/Repo/Repo.php @@ -0,0 +1,77 @@ +uri[4]) and is_numeric($this->uri[4])) { + // Retrieve repository Id if any + if (!empty($this->uri[4])) { + $this->repoId = $this->uri[4]; + } + + // Retrieve action if any + if (!empty($this->uri[5])) { + $this->action = $this->uri[5]; + } + } + + /** + * If no repository Id is specified + * https://repomanager.mydomain.net/api/v2/repo/ + * Get the list of all repositories + */ + if (empty($this->repoId)) { + if ($this->method == 'GET') { + $repos = $repoListingController->listNameOnly(true); + + if (empty($repos)) { + throw new Exception('No repositories found'); + } + + return array('results' => $repos); + } + } + + /** + * If a repository Id is specified + * https://repomanager.mydomain.net/api/v2/repo/$this->repoId/ + */ + if (!empty($this->repoId)) { + /** + * Get the list of all snapshots for a repository + */ + if ($this->method == 'GET') { + // Check if the repository exists + if (!$repoController->existsId($this->repoId)) { + throw new Exception('Repository does not exist'); + } + + // Get the list of snapshots for the repository + $snapshots = $repoListingController->listSnapshots($this->repoId); + + if (empty($snapshots)) { + throw new Exception('No snapshots found for this repository'); + } + + return array('results' => $snapshots); + } + } + + throw new Exception('Invalid request'); + } +} diff --git a/www/controllers/App/Config/Settings.php b/www/controllers/App/Config/Settings.php index f65dc41c6..f06a491ea 100644 --- a/www/controllers/App/Config/Settings.php +++ b/www/controllers/App/Config/Settings.php @@ -167,6 +167,14 @@ public static function get() } } + if (!defined('REPO_DEDUPLICATION')) { + if (!empty($settings['REPO_DEDUPLICATION']) and $settings['REPO_DEDUPLICATION'] == 'false') { + define('REPO_DEDUPLICATION', false); + } else { + define('REPO_DEDUPLICATION', true); + } + } + if (!defined('RETENTION')) { if (isset($settings['RETENTION']) and $settings['RETENTION'] >= 0) { define('RETENTION', intval($settings['RETENTION'], 8)); diff --git a/www/controllers/Profile.php b/www/controllers/Profile.php index e4335c272..0de6b6575 100644 --- a/www/controllers/Profile.php +++ b/www/controllers/Profile.php @@ -561,7 +561,7 @@ public function cleanProfiles() * Get unused repos Id (repos that have no active snapshot and so are not visible from web UI) */ $myrepo = new \Controllers\Repo\Repo(); - $unusedRepos = $myrepo->getUnusedRepos(); + $unusedRepos = $myrepo->getUnused(); /** * Remove those repos Id from profiles diff --git a/www/controllers/Repo/Deb.php b/www/controllers/Repo/Deb.php new file mode 100644 index 000000000..d719d05d4 --- /dev/null +++ b/www/controllers/Repo/Deb.php @@ -0,0 +1,38 @@ +model = new \Models\Repo\Deb(); + } + + /** + * Return the Id of a repository by its name, distribution and component/section + */ + public function getIdByNameDistComponent(string $name, string $distribution, string $component) : int|null + { + return $this->model->getIdByNameDistComponent($name, $distribution, $component); + } + + /** + * Return true if a repository with the specified name, distribution and component/section exists + */ + public function exists(string $name, string $distribution, string $component) : bool + { + return $this->model->exists($name, $distribution, $component); + } + + /** + * Add a new DEB repository + */ + public function add(string $name, string $distribution, string $component, string $source = '') : void + { + $this->model->add($name, $distribution, $component, $source); + } +} diff --git a/www/controllers/Repo/Listing.php b/www/controllers/Repo/Listing.php index ef3b67563..3df9b99fc 100644 --- a/www/controllers/Repo/Listing.php +++ b/www/controllers/Repo/Listing.php @@ -17,7 +17,7 @@ public function __construct() * Return the list of repos, their snapshots and their environments * Does not display repos that have no active environments */ - public function list() + public function list() : array { return $this->model->list(); } @@ -39,4 +39,12 @@ public function listNameOnly(bool $withActiveSnapshots = false) { return $this->model->listNameOnly($withActiveSnapshots); } + + /** + * Return the list of snapshots for a repository + */ + public function listSnapshots(int $repoId) : array + { + return $this->model->listSnapshots($repoId); + } } diff --git a/www/controllers/Repo/Mirror/Deb.php b/www/controllers/Repo/Mirror/Deb.php index 6ed3674fc..840abb322 100644 --- a/www/controllers/Repo/Mirror/Deb.php +++ b/www/controllers/Repo/Mirror/Deb.php @@ -767,21 +767,24 @@ private function downloadDebPackages($url) } /** + * Deduplication * Check if package already exists in the previous snapshot * If so, just create a hard link to the package */ - if (isset($this->previousSnapshotDirPath)) { - if (file_exists($this->previousSnapshotDirPath . '/' . $relativeDir . '/' . $debPackageName)) { - /** - * Create hard link to the package - */ - if (!link($this->previousSnapshotDirPath . '/' . $relativeDir . '/' . $debPackageName, $absoluteDir . '/' . $debPackageName)) { - throw new Exception('Cannot create hard link to package: ' . $this->previousSnapshotDirPath . '/' . $relativeDir . '/' . $debPackageName); - } + if (REPO_DEDUPLICATION) { + if (isset($this->previousSnapshotDirPath)) { + if (file_exists($this->previousSnapshotDirPath . '/' . $relativeDir . '/' . $debPackageName)) { + /** + * Create hard link to the package + */ + if (!link($this->previousSnapshotDirPath . '/' . $relativeDir . '/' . $debPackageName, $absoluteDir . '/' . $debPackageName)) { + throw new Exception('Cannot create hard link to package: ' . $this->previousSnapshotDirPath . '/' . $relativeDir . '/' . $debPackageName); + } - $this->taskLogSubStepController->completed('Linked to previous snapshot'); + $this->taskLogSubStepController->completed('Linked to previous snapshot'); - continue; + continue; + } } } diff --git a/www/controllers/Repo/Mirror/Rpm.php b/www/controllers/Repo/Mirror/Rpm.php index f75bbcde8..47106f4d3 100644 --- a/www/controllers/Repo/Mirror/Rpm.php +++ b/www/controllers/Repo/Mirror/Rpm.php @@ -781,21 +781,24 @@ private function downloadRpmPackages(string $url) } /** + * Deduplication * Check if package already exists in the previous snapshot * If so, just create a hard link to the package */ - if (isset($this->previousSnapshotDirPath)) { - if (file_exists($this->previousSnapshotDirPath . '/' . $relativeDir . '/' . $rpmPackageName)) { - /** - * Create hard link to the package - */ - if (!link($this->previousSnapshotDirPath . '/' . $relativeDir . '/' . $rpmPackageName, $absoluteDir . '/' . $rpmPackageName)) { - throw new Exception('Cannot create hard link to package: ' . $this->previousSnapshotDirPath . '/' . $relativeDir . '/' . $rpmPackageName); - } + if (REPO_DEDUPLICATION) { + if (isset($this->previousSnapshotDirPath)) { + if (file_exists($this->previousSnapshotDirPath . '/' . $relativeDir . '/' . $rpmPackageName)) { + /** + * Create hard link to the package + */ + if (!link($this->previousSnapshotDirPath . '/' . $relativeDir . '/' . $rpmPackageName, $absoluteDir . '/' . $rpmPackageName)) { + throw new Exception('Cannot create hard link to package: ' . $this->previousSnapshotDirPath . '/' . $relativeDir . '/' . $rpmPackageName); + } - $this->taskLogSubStepController->completed('Linked to previous snapshot'); + $this->taskLogSubStepController->completed('Linked to previous snapshot'); - continue; + continue; + } } } diff --git a/www/controllers/Repo/Repo.php b/www/controllers/Repo/Repo.php index e974278ce..1cdbd921b 100644 --- a/www/controllers/Repo/Repo.php +++ b/www/controllers/Repo/Repo.php @@ -7,9 +7,7 @@ class Repo { - private $model; - private $repoListingController; - + protected $model; private $taskId; private $repoId; private $snapId; @@ -41,7 +39,6 @@ class Repo public function __construct() { $this->model = new \Models\Repo\Repo(); - // $this->repoListingController = new \Controllers\Repo\Listing(); } public function setRepoId(string $id) @@ -306,7 +303,7 @@ public function getTaskId() /** * Retrieve all informations from a repo, snapshot and env in database */ - public function getAllById(string|null $repoId = null, string|null $snapId = null, string|null $envId = null) + public function getAllById(string|null $repoId = null, string|null $snapId = null, string|null $envId = null) : void { $data = $this->model->getAllById($repoId, $snapId, $envId); @@ -316,7 +313,7 @@ public function getAllById(string|null $repoId = null, string|null $snapId = nul /** * Function that parses and retrieves the results of the getAllBy* functions */ - private function getAllByParser(array $data) + private function getAllByParser(array $data) : void { if (!empty($data['Source'])) { $this->setSource($data['Source']); @@ -384,50 +381,26 @@ private function getAllByParser(array $data) } } - /** - * Retourne l'Id d'un repo en base de données, à partir de son nom - */ - public function getIdByName(string $name, string $dist = null, string $section = null) - { - return $this->model->getIdByName($name, $dist, $section); - } - - /** - * Retourne l'Id du snapshot le + récent du repo - */ - public function getLastSnapshotId(string $repoId) - { - return $this->model->getLastSnapshotId($repoId); - } - /** * Get unused repos Id (repos that have no active snapshot and so are not visible from web UI) */ - public function getUnusedRepos() + public function getUnused() : array { - return $this->model->getUnusedRepos(); + return $this->model->getUnused(); } /** - * Check if repo exists in database, by its name + * Return true if a repo Id exists in database */ - public function exists(string $name, string $dist = '', string $section = '') - { - return $this->model->exists($name, $dist, $section); - } - - /** - * Retoune true si l'Id de repo existe en base de données - */ - public function existsId(string $repoId) + public function existsId(string $repoId) : bool { return $this->model->existsId($repoId); } /** - * Retourne true si un Id de snapshot existe en base de données + * Return true if a snapshot Id exists in database */ - public function existsSnapId(string $snapId) + public function existsSnapId(string $snapId) : bool { return $this->model->existsSnapId($snapId); } @@ -654,14 +627,6 @@ public function snapSetPackagesExcluded(int $snapId, array $packages) $this->model->snapSetPackagesExcluded($snapId, implode(',', $packages)); } - /** - * Add a repo in database - */ - public function add(string $source, string $packageType, string $name) - { - $this->model->add($source, $packageType, $name); - } - /** * Add a repo snapshot in database */ diff --git a/www/controllers/Repo/Rpm.php b/www/controllers/Repo/Rpm.php new file mode 100644 index 000000000..d9ba4d305 --- /dev/null +++ b/www/controllers/Repo/Rpm.php @@ -0,0 +1,38 @@ +model = new \Models\Repo\Rpm(); + } + + /** + * Return the Id of a repository by its name and release version + */ + public function getIdByNameReleasever(string $name, int $releaseVersion) : int|null + { + return $this->model->getIdByNameReleasever($name, $releaseVersion); + } + + /** + * Return true if a repository with the specified name and release version exists + */ + public function exists(string $name, int $releaseVersion) : bool + { + return $this->model->exists($name, $releaseVersion); + } + + /** + * Add a new RPM repository + */ + public function add(string $name, int $releaseVersion, string $source = '') : void + { + $this->model->add($name, $releaseVersion, $source); + } +} diff --git a/www/controllers/Repo/Snapshot.php b/www/controllers/Repo/Snapshot.php index e069f83e2..72240a447 100644 --- a/www/controllers/Repo/Snapshot.php +++ b/www/controllers/Repo/Snapshot.php @@ -14,20 +14,12 @@ public function __construct() $this->model = new \Models\Repo\Snapshot(); } - /** - * Return true if a task is queued or running for the specified snapshot - */ - public function taskRunning(int $snapId) : bool - { - return $this->model->taskRunning($snapId); - } - /** * Return the list of unused snapshots for the specified repo Id and retention parameter */ - private function getUnunsed(string $repoId, string $retention) : array + private function getUnused(string $repoId, string $retention) : array { - return $this->model->getUnunsed($repoId, $retention); + return $this->model->getUnused($repoId, $retention); } /** @@ -75,7 +67,7 @@ public function clean() : string|null /** * Get the list of unused snapshots for this repository */ - $unusedSnapshots = $this->getUnunsed($repoId, RETENTION); + $unusedSnapshots = $this->getUnused($repoId, RETENTION); /** * Process if there are unused snapshots @@ -180,4 +172,12 @@ public function exists(int $id) : bool { return $this->model->exists($id); } + + /** + * Return true if a task is queued or running for the specified snapshot + */ + public function taskRunning(int $snapId) : bool + { + return $this->model->taskRunning($snapId); + } } diff --git a/www/controllers/Settings.php b/www/controllers/Settings.php index 360b3704c..a2354d10f 100644 --- a/www/controllers/Settings.php +++ b/www/controllers/Settings.php @@ -104,6 +104,10 @@ public function apply(array $sendSettings) : void /** * Repositories / Mirroring settings */ + if (!empty($sendSettings['repo-deduplication']) and in_array($sendSettings['repo-deduplication'], ['true', 'false'])) { + $settingsToApply['REPO_DEDUPLICATION'] = $sendSettings['repo-deduplication']; + } + if (isset($sendSettings['retention'])) { $retention = Common::validateData($sendSettings['retention']); diff --git a/www/controllers/Task/Repo/Create.php b/www/controllers/Task/Repo/Create.php index 163a4d9fb..af48ca61e 100644 --- a/www/controllers/Task/Repo/Create.php +++ b/www/controllers/Task/Repo/Create.php @@ -13,6 +13,8 @@ class Create use Finalize; private $repo; + private $rpmRepoController; + private $debRepoController; private $task; private $repoSnapshotController; private $repoEnvController; @@ -24,6 +26,8 @@ class Create public function __construct(string $taskId) { $this->repo = new \Controllers\Repo\Repo(); + $this->rpmRepoController = new \Controllers\Repo\Rpm(); + $this->debRepoController = new \Controllers\Repo\Deb(); $this->task = new \Controllers\Task\Task(); $this->repoSnapshotController = new \Controllers\Repo\Snapshot(); $this->repoEnvController = new \Controllers\Repo\Environment(); @@ -199,7 +203,8 @@ private function local() $this->repo->setTime(date("H:i")); try { - $this->taskLogStepController->new('create-metadata', 'CREATING REPOSITORY'); + $this->taskLogStepController->new('create-repo', 'CREATING REPOSITORY'); + $this->taskLogSubStepController->new('create-dirs', 'CREATING DIRECTORIES'); /** * Check if a repo/section with the same name is already active with snapshots @@ -267,14 +272,18 @@ private function local() } } + $this->taskLogSubStepController->completed(); + + $this->taskLogSubStepController->new('updating-database', 'UPDATING DATABASE'); + /** * Check if repository already exists in database */ if ($this->repo->getPackageType() == 'rpm') { - $exists = $this->repo->exists($this->repo->getName()); + $exists = $this->rpmRepoController->exists($this->repo->getName(), $this->repo->getReleasever()); } if ($this->repo->getPackageType() == 'deb') { - $exists = $this->repo->exists($this->repo->getName(), $this->repo->getDist(), $this->repo->getSection()); + $exists = $this->debRepoController->exists($this->repo->getName(), $this->repo->getDist(), $this->repo->getSection()); } /** @@ -283,30 +292,20 @@ private function local() */ if ($exists === false) { if ($this->repo->getPackageType() == 'rpm') { - $this->repo->add($this->repo->getName(), 'rpm', $this->repo->getName()); - } - if ($this->repo->getPackageType() == 'deb') { - $this->repo->add($this->repo->getName(), 'deb', $this->repo->getName()); - } - - /** - * Retrieve repo Id from the last insert row - */ - $this->repo->setRepoId($this->repo->getLastInsertRowID()); + $this->rpmRepoController->add($this->repo->getName(), $this->repo->getReleasever(), $this->repo->getName()); - /** - * Set repo releasever - */ - if ($this->repo->getPackageType() == 'rpm') { - $this->repo->updateReleasever($this->repo->getRepoId(), $this->repo->getReleasever()); + /** + * Retrieve repo Id from the last insert row + */ + $this->repo->setRepoId($this->rpmRepoController->getLastInsertRowID()); } - - /** - * Set repo dist and section - */ if ($this->repo->getPackageType() == 'deb') { - $this->repo->updateDist($this->repo->getRepoId(), $this->repo->getDist()); - $this->repo->updateSection($this->repo->getRepoId(), $this->repo->getSection()); + $this->debRepoController->add($this->repo->getName(), $this->repo->getDist(), $this->repo->getSection(), $this->repo->getName()); + + /** + * Retrieve repo Id from the last insert row + */ + $this->repo->setRepoId($this->debRepoController->getLastInsertRowID()); } /** @@ -317,15 +316,21 @@ private function local() * Retrieve and set repo Id from database */ if ($this->repo->getPackageType() == 'rpm') { - $this->repo->setRepoId($this->repo->getIdByName($this->repo->getName(), '', '')); + $repoId = $this->rpmRepoController->getIdByNameReleasever($this->repo->getName(), $this->repo->getReleasever()); } if ($this->repo->getPackageType() == 'deb') { - $this->repo->setRepoId($this->repo->getIdByName($this->repo->getName(), $this->repo->getDist(), $this->repo->getSection())); + $repoId = $this->debRepoController->getIdByNameDistComponent($this->repo->getName(), $this->repo->getDist(), $this->repo->getSection()); + } + + if (empty($repoId)) { + throw new Exception('Could not retrieve repository Id from database'); } + + $this->repo->setRepoId($repoId); } - unset($exists); + unset($exists, $repoId); /** * Add snapshot to database @@ -346,6 +351,10 @@ private function local() } } + $this->taskLogSubStepController->completed(); + + $this->taskLogSubStepController->new('applying-permissions', 'APPLYING PERMISSIONS'); + /** * Apply permissions on the new repo */ @@ -360,7 +369,7 @@ private function local() \Controllers\Filesystem\File::recursiveChown(REPOS_DIR . '/' . $this->repo->getName(), WWW_USER, 'repomanager'); } - $this->taskLogStepController->completed(); + $this->taskLogSubStepController->completed(); /** * Add repo to group if a group has been specified @@ -371,11 +380,16 @@ private function local() $this->taskLogStepController->completed(); } + $this->taskLogSubStepController->new('cleaning', 'CLEANING'); + /** * Clean unused repos in groups */ $this->repo->cleanGroups(); + $this->taskLogSubStepController->completed(); + $this->taskLogStepController->completed(); + /** * Set task status to 'done' */ diff --git a/www/controllers/Task/Repo/Duplicate.php b/www/controllers/Task/Repo/Duplicate.php index 932d7c61d..b639d260a 100644 --- a/www/controllers/Task/Repo/Duplicate.php +++ b/www/controllers/Task/Repo/Duplicate.php @@ -11,6 +11,8 @@ class Duplicate private $sourceRepo; private $repo; + private $rpmRepoController; + private $debRepoController; private $repoEnvController; private $task; private $taskLogStepController; @@ -20,6 +22,8 @@ public function __construct(string $taskId) { $this->sourceRepo = new \Controllers\Repo\Repo(); $this->repo = new \Controllers\Repo\Repo(); + $this->rpmRepoController = new \Controllers\Repo\Rpm(); + $this->debRepoController = new \Controllers\Repo\Deb(); $this->task = new \Controllers\Task\Task(); $this->repoEnvController = new \Controllers\Repo\Environment(); $this->taskLogStepController = new \Controllers\Task\Log\Step($taskId); @@ -306,33 +310,15 @@ public function execute() $this->taskLogSubStepController->new('inserting-database', 'INSERTING REPOSITORY IN DATABASE'); /** - * Insert the new repo in database + * Insert the new repo in database and retrieve its Id */ if ($this->repo->getPackageType() == 'rpm') { - $this->repo->add($this->repo->getSource(), 'rpm', $this->repo->getName()); + $this->rpmRepoController->add($this->repo->getName(), $this->repo->getReleasever(), $this->repo->getSource()); + $targetRepoId = $this->rpmRepoController->getLastInsertRowID(); } if ($this->repo->getPackageType() == 'deb') { - $this->repo->add($this->repo->getSource(), 'deb', $this->repo->getName()); - } - - /** - * Retrieve the Id of the new repo in database - */ - $targetRepoId = $this->repo->getLastInsertRowID(); - - if ($this->repo->getPackageType() == 'rpm') { - /** - * Set repo releasever - */ - $this->repo->updateReleasever($targetRepoId, $this->repo->getReleasever()); - } - - if ($this->repo->getPackageType() == 'deb') { - /** - * Set repo dist and section - */ - $this->repo->updateDist($targetRepoId, $this->repo->getDist()); - $this->repo->updateSection($targetRepoId, $this->repo->getSection()); + $this->debRepoController->add($this->repo->getName(), $this->repo->getDist(), $this->repo->getSection(), $this->repo->getSource()); + $targetRepoId = $this->debRepoController->getLastInsertRowID(); } /** diff --git a/www/controllers/Task/Repo/Finalize.php b/www/controllers/Task/Repo/Finalize.php index 32ad2fee7..24ee828b6 100644 --- a/www/controllers/Task/Repo/Finalize.php +++ b/www/controllers/Task/Repo/Finalize.php @@ -23,24 +23,19 @@ protected function finalize() * If currently no rpm repo of this name exists in the database then we add it */ if ($this->repo->getPackageType() == 'rpm') { - if ($this->repo->exists($this->repo->getName()) === false) { - $this->repo->add($this->repo->getSource(), 'rpm', $this->repo->getName()); + if (!$this->rpmRepoController->exists($this->repo->getName(), $this->repo->getReleasever())) { + $this->rpmRepoController->add($this->repo->getName(), $this->repo->getReleasever(), $this->repo->getSource()); /** * Repository Id becomes the Id of the last inserted row in the database */ - $this->repo->setRepoId($this->repo->getLastInsertRowID()); - - /** - * Set repo releasever - */ - $this->repo->updateReleasever($this->repo->getRepoId(), $this->repo->getReleasever()); + $this->repo->setRepoId($this->rpmRepoController->getLastInsertRowID()); /** * Otherwise, if a repo of the same name exists, we retrieve its Id from the database */ } else { - $this->repo->setRepoId($this->repo->getIdByName($this->repo->getName(), '', '')); + $this->repo->setRepoId($this->rpmRepoController->getIdByNameReleasever($this->repo->getName(), $this->repo->getReleasever())); } } @@ -48,25 +43,19 @@ protected function finalize() * If currently no deb repo of this name exists in the database then we add it */ if ($this->repo->getPackageType() == 'deb') { - if ($this->repo->exists($this->repo->getName(), $this->repo->getDist(), $this->repo->getSection()) === false) { - $this->repo->add($this->repo->getSource(), 'deb', $this->repo->getName()); + if (!$this->debRepoController->exists($this->repo->getName(), $this->repo->getDist(), $this->repo->getSection())) { + $this->debRepoController->add($this->repo->getName(), $this->repo->getDist(), $this->repo->getSection(), $this->repo->getSource()); /** * Repository Id becomes the Id of the last inserted row in the database */ - $this->repo->setRepoId($this->repo->getLastInsertRowID()); - - /** - * Set repo dist and section - */ - $this->repo->updateDist($this->repo->getRepoId(), $this->repo->getDist()); - $this->repo->updateSection($this->repo->getRepoId(), $this->repo->getSection()); + $this->repo->setRepoId($this->debRepoController->getLastInsertRowID()); /** * Otherwise, if a repo of the same name exists, we retrieve its Id from the database */ } else { - $this->repo->setRepoId($this->repo->getIdByName($this->repo->getName(), $this->repo->getDist(), $this->repo->getSection())); + $this->repo->setRepoId($this->debRepoController->getIdByNameDistComponent($this->repo->getName(), $this->repo->getDist(), $this->repo->getSection())); } } @@ -212,8 +201,8 @@ protected function finalize() \Controllers\Filesystem\File::recursiveChmod(REPOS_DIR . '/' . $this->repo->getName() . '/' . $this->repo->getDist() . '/' . $this->repo->getDateFormatted() . '_' . $this->repo->getSection(), 'dir', 770); \Controllers\Filesystem\File::recursiveChown(REPOS_DIR . '/' . $this->repo->getName(), WWW_USER, 'repomanager'); } - $this->taskLogSubStepController->completed(); + $this->taskLogSubStepController->completed(); $this->taskLogStepController->completed(); /** diff --git a/www/controllers/Task/Repo/Update.php b/www/controllers/Task/Repo/Update.php index 55e10ac7c..dd8f8e73e 100644 --- a/www/controllers/Task/Repo/Update.php +++ b/www/controllers/Task/Repo/Update.php @@ -311,7 +311,7 @@ private function updateLocal() } /** - * Create hardlinks to the previous snapshot packages + * Deduplication/Copy packages from previous snapshot to the new snapshot */ foreach ($packages as $packagePath) { // Get package name @@ -320,16 +320,41 @@ private function updateLocal() // Increment counter $packageCounter++; - $this->taskLogSubStepController->new('hardlink-package-' . $packageCounter, 'LINKING PACKAGE TO PREVIOUS SNAPSHOT (' . $packageCounter . '/' . $totalPackages . ')', $packagePath); + /** + * Deduplication + * Create hardlink to the previous snapshot package + */ + if (REPO_DEDUPLICATION) { + $this->taskLogSubStepController->new('hardlink-package-' . $packageCounter, 'LINKING PACKAGE TO PREVIOUS SNAPSHOT (' . $packageCounter . '/' . $totalPackages . ')', $packagePath); - if ($this->repo->getPackageType() == 'deb') { - if (!link($packagePath, $repoPath . '/pool/' . $this->repo->getSection() . '/' . $name)) { - throw new Exception('Cannot create hard link to package: ' . $packagePath); + if ($this->repo->getPackageType() == 'deb') { + if (!link($packagePath, $repoPath . '/pool/' . $this->repo->getSection() . '/' . $name)) { + throw new Exception('Cannot create hard link to package: ' . $packagePath); + } + } + if ($this->repo->getPackageType() == 'rpm') { + if (!link($packagePath, $repoPath . '/packages/' . $name)) { + throw new Exception('Cannot create hard link to package: ' . $packagePath); + } } } - if ($this->repo->getPackageType() == 'rpm') { - if (!link($packagePath, $repoPath . '/packages/' . $name)) { - throw new Exception('Cannot create hard link to package: ' . $packagePath); + + /** + * When deduplication is disabled + * Copy the package from the previous snapshot to the new snapshot + */ + if (!REPO_DEDUPLICATION) { + $this->taskLogSubStepController->new('copy-package-' . $packageCounter, 'COPYING PACKAGE TO NEW SNAPSHOT (' . $packageCounter . '/' . $totalPackages . ')', $packagePath); + + if ($this->repo->getPackageType() == 'deb') { + if (!copy($packagePath, $repoPath . '/pool/' . $this->repo->getSection() . '/' . $name)) { + throw new Exception('Cannot copy package: ' . $packagePath); + } + } + if ($this->repo->getPackageType() == 'rpm') { + if (!copy($packagePath, $repoPath . '/packages/' . $name)) { + throw new Exception('Cannot copy package: ' . $packagePath); + } } } diff --git a/www/models/Connection.php b/www/models/Connection.php index f99228d24..eec4b1ff0 100644 --- a/www/models/Connection.php +++ b/www/models/Connection.php @@ -303,6 +303,11 @@ private function generateMainTables() Source VARCHAR(255) NOT NULL, Package_type VARCHAR(10) NOT NULL)"); + /** + * Create indexes + */ + $this->exec("CREATE INDEX IF NOT EXISTS repos_ALL_index ON repos (Name, Releasever, Dist, Section, Source, Package_type)"); + /** * repos_snap table */ @@ -320,6 +325,11 @@ private function generateMainTables() Status CHAR(8) NOT NULL, Id_repo INTEGER NOT NULL)"); + /** + * Create indexes + */ + $this->exec("CREATE INDEX IF NOT EXISTS repos_snap_status_id_repo_index ON repos_snap (Status, Id_repo)"); + /** * repos_env table */ @@ -329,6 +339,11 @@ private function generateMainTables() Description VARCHAR(255), Id_snap INTEGER NOT NULL)"); + /** + * Create indexes + */ + $this->exec("CREATE INDEX IF NOT EXISTS repos_env_id_snap_index ON repos_env (Id_snap)"); + /** * env table */ @@ -450,6 +465,12 @@ private function generateMainTables() Id_repo INTEGER NOT NULL, Id_group INTEGER NOT NULL);"); + /** + * Create indexes + */ + $this->exec("CREATE INDEX IF NOT EXISTS group_members_id_repo_index ON group_members (Id_repo)"); + $this->exec("CREATE INDEX IF NOT EXISTS group_members_id_group_index ON group_members (Id_group)"); + $this->exec("CREATE TABLE IF NOT EXISTS tasks ( Id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, Type CHAR(9), /* immediate, scheduled */ @@ -554,6 +575,7 @@ private function generateMainTables() TASK_QUEUING_MAX_SIMULTANEOUS INTEGER, TASK_CLEAN_OLDER_THAN INTEGER, /* Repo settings */ + REPO_DEDUPLICATION CHAR(5), /* true, false */ RETENTION INTEGER, REPO_CONF_FILES_PREFIX VARCHAR(255), /* Mirroring */ @@ -655,6 +677,7 @@ private function generateMainTables() EMAIL_RECIPIENT, SESSION_TIMEOUT, DEBUG_MODE, + REPO_DEDUPLICATION, REPO_CONF_FILES_PREFIX, TIMEZONE, TASK_EXECUTION_MEMORY_LIMIT, @@ -707,6 +730,7 @@ private function generateMainTables() '', '3600', 'false', + 'true', 'repomanager-', 'Europe/Paris', '1024', diff --git a/www/models/Repo/Deb.php b/www/models/Repo/Deb.php new file mode 100644 index 000000000..02cc5e422 --- /dev/null +++ b/www/models/Repo/Deb.php @@ -0,0 +1,76 @@ +getConnection('main'); + } + + /** + * Return the Id of a repository by its name, distribution and component/section + */ + public function getIdByNameDistComponent(string $name, string $distribution, string $component) : int|null + { + $id = null; + + try { + $stmt = $this->db->prepare("SELECT Id FROM repos WHERE Name = :name AND Dist = :distribution AND Section = :component"); + $stmt->bindValue(':name', $name, SQLITE3_TEXT); + $stmt->bindValue(':distribution', $distribution, SQLITE3_TEXT); + $stmt->bindValue(':component', $component, SQLITE3_TEXT); + $result = $stmt->execute(); + } catch (Exception $e) { + $this->db->logError($e); + } + + while ($row = $result->fetchArray(SQLITE3_ASSOC)) { + $id = $row['Id']; + } + + return $id; + } + + /** + * Return true if a repository with the specified name, distribution and component/section exists + */ + public function exists(string $name, string $distribution, string $component) : bool + { + try { + $stmt = $this->db->prepare("SELECT Id FROM repos WHERE Name = :name AND Dist = :distribution AND Section = :component"); + $stmt->bindValue(':name', $name, SQLITE3_TEXT); + $stmt->bindValue(':distribution', $distribution, SQLITE3_TEXT); + $stmt->bindValue(':component', $component, SQLITE3_TEXT); + $result = $stmt->execute(); + } catch (Exception $e) { + $this->db->logError($e); + } + + if ($this->db->isempty($result) === true) { + return false; + } + + return true; + } + + /** + * Add a new DEB repository + */ + public function add(string $name, string $distribution, string $component, string $source = '') : void + { + try { + $stmt = $this->db->prepare("INSERT INTO repos ('Name', 'Dist', 'Section', 'Source', 'Package_type') VALUES (:name, :distribution, :component, :source, 'deb')"); + $stmt->bindValue(':name', $name, SQLITE3_TEXT); + $stmt->bindValue(':distribution', $distribution, SQLITE3_TEXT); + $stmt->bindValue(':component', $component, SQLITE3_TEXT); + $stmt->bindValue(':source', $source, SQLITE3_TEXT); + $stmt->execute(); + } catch (Exception $e) { + $this->db->logError($e); + } + } +} diff --git a/www/models/Repo/Listing.php b/www/models/Repo/Listing.php index f5f10f7aa..69403da85 100644 --- a/www/models/Repo/Listing.php +++ b/www/models/Repo/Listing.php @@ -15,8 +15,10 @@ public function __construct() * Return the list of repos, their snapshots and their environments * Does not display repos that have no active environments */ - public function list() + public function list() : array { + $data = []; + try { $result = $this->db->query("SELECT repos.Id AS repoId, @@ -47,24 +49,24 @@ public function list() $this->db->logError($e); } - $repos = array(); - - while ($datas = $result->fetchArray(SQLITE3_ASSOC)) { - $repos[] = $datas; + while ($row = $result->fetchArray(SQLITE3_ASSOC)) { + $data[] = $row; } - return $repos; + return $data; } /** * Return the list of repos by group name */ - public function listByGroup(string $groupName) + public function listByGroup(string $groupName) : array { + $data = []; + // If the group is 'Default' (a fictitious group), then we display all repos that do not belong to any group try { if ($groupName == 'Default') { - $reposInGroup = $this->db->query("SELECT DISTINCT + $result = $this->db->query("SELECT DISTINCT repos.Id AS repoId, repos_snap.Id AS snapId, repos_env.Id AS envId, @@ -124,21 +126,18 @@ public function listByGroup(string $groupName) WHERE groups.Name = :groupname AND repos_snap.Status = 'active' ORDER BY repos.Name ASC, repos.Dist ASC, repos.Section ASC, repos_snap.Date DESC"); - $stmt->bindValue(':groupname', $groupName); - $reposInGroup = $stmt->execute(); + $result = $stmt->execute(); } } catch (Exception $e) { $this->db->logError($e); } - $reposIn = array(); - - while ($datas = $reposInGroup->fetchArray(SQLITE3_ASSOC)) { - $reposIn[] = $datas; + while ($row = $result->fetchArray(SQLITE3_ASSOC)) { + $data[] = $row; } - return $reposIn; + return $data; } /** @@ -148,7 +147,7 @@ public function listByGroup(string $groupName) */ public function listNameOnly(bool $withActiveSnapshots) { - $repos = array(); + $data = []; try { if (!$withActiveSnapshots) { @@ -161,6 +160,7 @@ public function listNameOnly(bool $withActiveSnapshots) $result = $this->db->query("SELECT DISTINCT repos.Id, repos.Name, + repos.Releasever, repos.Dist, repos.Section, repos.Source, @@ -176,10 +176,32 @@ public function listNameOnly(bool $withActiveSnapshots) $this->db->logError($e); } - while ($datas = $result->fetchArray(SQLITE3_ASSOC)) { - $repos[] = $datas; + while ($row = $result->fetchArray(SQLITE3_ASSOC)) { + $data[] = $row; + } + + return $data; + } + + /** + * Return the list of snapshots for a repository + */ + public function listSnapshots(int $repoId) : array + { + $data = []; + + try { + $stmt = $this->db->prepare("SELECT * FROM repos_snap WHERE Id_repo = :repoId AND Status = 'active' ORDER BY Date DESC"); + $stmt->bindValue(':repoId', $repoId); + $result = $stmt->execute(); + } catch (Exception $e) { + $this->db->logError($e); + } + + while ($row = $result->fetchArray(SQLITE3_ASSOC)) { + $data[] = $row; } - return $repos; + return $data; } } diff --git a/www/models/Repo/Repo.php b/www/models/Repo/Repo.php index 621902415..b8bc71e65 100644 --- a/www/models/Repo/Repo.php +++ b/www/models/Repo/Repo.php @@ -18,9 +18,9 @@ public function __construct() /** * Retrieve all informations from a repo, snapshot and env in database */ - public function getAllById(string|null $repoId, string|null $snapId, string|null $envId) + public function getAllById(string|null $repoId, string|null $snapId, string|null $envId) : array { - $data = ''; + $data = []; try { if (!empty($repoId) and !empty($snapId) and !empty($envId)) { @@ -156,9 +156,7 @@ public function getAllById(string|null $repoId, string|null $snapId, string|null $this->db->logError($e); } - /** - * Si rien n'a été trouvé en BDD avec l'ID fourni alors on quitte - */ + // Throw an exception if no data found if ($this->db->isempty($result) === true) { throw new Exception("Error: cannot find repo with specified Id"); } @@ -167,48 +165,9 @@ public function getAllById(string|null $repoId, string|null $snapId, string|null $data = $row; } - /** - * Retourne un array contenant toutes les données trouvées concernant le repo / snapshot / env - */ return $data; } - /** - * Retourne l'Id du repo en base de données à partir de son nom - */ - public function getIdByName(string $name, string $dist = null, string $section = null) - { - try { - /** - * Cas où on a seulement spécifié le nom du repo - */ - if (empty($dist) or empty($section)) { - $stmt = $this->db->prepare("SELECT Id from repos - WHERE Name = :name - AND (Dist IS NULL OR Dist = '') - AND (Section IS NULL OR Section = '')"); - - /** - * Cas où la distribution et la section ont été spécifié - */ - } else { - $stmt = $this->db->prepare("SELECT Id from repos WHERE Name = :name and Dist = :dist and Section = :section"); - $stmt->bindValue(':dist', $dist); - $stmt->bindValue(':section', $section); - } - $stmt->bindValue(':name', $name); - $result = $stmt->execute(); - } catch (\Exception $e) { - $this->db->logError($e); - } - - while ($row = $result->fetchArray(SQLITE3_ASSOC)) { - $id = $row['Id']; - } - - return $id; - } - /** * Return latest snapshot Id from repo Id */ @@ -353,37 +312,6 @@ public function getSnapByRepoId(string $repoId, string $status = null) return $snapshots; } - /** - * Retourne l'Id du snapshot le + récent du repo - */ - public function getLastSnapshotId(string $repoId) - { - try { - $stmt = $this->db->prepare("SELECT - repos_snap.Id AS snapId, - repos_snap.Date - FROM repos_snap - INNER JOIN repos - ON repos.Id = repos_snap.Id_repo - WHERE repos.Id = :repoId - AND repos_snap.Status = 'active' - ORDER BY repos_snap.Date DESC - LIMIT 1;"); - $stmt->bindValue(':repoId', $repoId); - $result = $stmt->execute(); - } catch (\Exception $e) { - $this->db->logError($e); - } - - $id = ''; - - while ($row = $result->fetchArray(SQLITE3_ASSOC)) { - $id = $row['snapId']; - } - - return $id; - } - /** * Get repository environment description by the repo name */ @@ -459,8 +387,10 @@ public function getSnapDateById(string $snapId) /** * Get unused repos Id (repos that have no active snapshot and so are not visible from web UI) */ - public function getUnusedRepos() + public function getUnused() : array { + $data = []; + try { $stmt = $this->db->prepare("SELECT repos.Id, @@ -481,8 +411,6 @@ public function getUnusedRepos() $this->db->logError($e); } - $data = array(); - while ($row = $result->fetchArray(SQLITE3_ASSOC)) { $data[] = $row; } @@ -611,9 +539,9 @@ public function snapSetPackagesExcluded(int $snapId, string $packages) } /** - * Retourne true si l'Id de repo existe en base de données + * Return true if a repo Id exists in database */ - public function existsId(string $id) + public function existsId(string $id) : bool { try { $stmt = $this->db->prepare("SELECT Id FROM repos WHERE Id = :id"); @@ -631,9 +559,9 @@ public function existsId(string $id) } /** - * Vérifie que l'Id du snapshot existe en base de données + * Return true if a snapshot Id exists in database */ - public function existsSnapId(string $id) + public function existsSnapId(string $id) : bool { try { $stmt = $this->db->prepare("SELECT Id FROM repos_snap WHERE Id = :id AND Status = 'active'"); @@ -670,37 +598,6 @@ public function existsEnvId(string $id) return true; } - /** - * Vérifie que le repo existe à partir de son nom - * Retourne true si existe - * Retourne false si n'existe pas - */ - public function exists(string $name, string $dist = '', string $section = '') - { - try { - if (!empty($dist) and !empty($section)) { - $stmt = $this->db->prepare("SELECT * FROM repos WHERE Name = :name AND Dist = :dist AND Section = :section"); - $stmt->bindValue(':dist', $dist); - $stmt->bindValue(':section', $section); - } else { - $stmt = $this->db->prepare("SELECT * FROM repos WHERE - Name = :name - AND (Dist IS NULL OR Dist = '') - AND (Section IS NULL OR Section = '')"); - } - $stmt->bindValue(':name', $name); - $result = $stmt->execute(); - } catch (\Exception $e) { - $this->db->logError($e); - } - - if ($this->db->isempty($result) === true) { - return false; - } - - return true; - } - /** * Vérifie qu'un environnement de repo existe * Retourne true si existe @@ -884,35 +781,6 @@ public function count() return $this->db->count($result); } - /** - * Add a repo in database - */ - public function add(string $source, string $packageType, string $name, string $dist = null, string $section = null) - { - try { - /** - * Cas où seul le nom a été renseigné - */ - if (empty($dist) or empty($section)) { - $stmt = $this->db->prepare("INSERT INTO repos ('Name', 'Source', 'Package_type') VALUES (:name, :source, :packageType)"); - - /** - * Cas où une distribution et une section ont été renseignés - */ - } else { - $stmt = $this->db->prepare("INSERT INTO repos ('Name', 'Dist', 'Section', 'Source', 'Package_type') VALUES (:name, :dist, :section, :source, :packageType)"); - $stmt->bindValue(':dist', $dist); - $stmt->bindValue(':section', $section); - } - $stmt->bindValue(':name', $name); - $stmt->bindValue(':source', $source); - $stmt->bindValue(':packageType', $packageType); - $stmt->execute(); - } catch (\Exception $e) { - $this->db->logError($e); - } - } - /** * Add a repo snapshot in database */ diff --git a/www/models/Repo/Rpm.php b/www/models/Repo/Rpm.php new file mode 100644 index 000000000..3ed53a5ef --- /dev/null +++ b/www/models/Repo/Rpm.php @@ -0,0 +1,73 @@ +getConnection('main'); + } + + /** + * Return the Id of a repository by its name and release version + */ + public function getIdByNameReleasever(string $name, int $releaseVersion) : int|null + { + $id = null; + + try { + $stmt = $this->db->prepare("SELECT Id FROM repos WHERE Name = :name AND Releasever = :releaseVersion"); + $stmt->bindValue(':name', $name, SQLITE3_TEXT); + $stmt->bindValue(':releaseVersion', $releaseVersion, SQLITE3_INTEGER); + $result = $stmt->execute(); + } catch (Exception $e) { + $this->db->logError($e); + } + + while ($row = $result->fetchArray(SQLITE3_ASSOC)) { + $id = $row['Id']; + } + + return $id; + } + + /** + * Return true if a repository with the specified name and release version exists + */ + public function exists(string $name, int $releaseVersion) : bool + { + try { + $stmt = $this->db->prepare("SELECT Id FROM repos WHERE Name = :name AND Releasever = :releaseVersion"); + $stmt->bindValue(':name', $name, SQLITE3_TEXT); + $stmt->bindValue(':releaseVersion', $releaseVersion, SQLITE3_INTEGER); + $result = $stmt->execute(); + } catch (Exception $e) { + $this->db->logError($e); + } + + if ($this->db->isempty($result) === true) { + return false; + } + + return true; + } + + /** + * Add a new RPM repository + */ + public function add(string $name, int $releaseVersion, string $source = '') : void + { + try { + $stmt = $this->db->prepare("INSERT INTO repos ('Name', 'Releasever', 'Source', 'Package_type') VALUES (:name, :releaseVersion, :source, 'rpm')"); + $stmt->bindValue(':name', $name, SQLITE3_TEXT); + $stmt->bindValue(':releaseVersion', $releaseVersion, SQLITE3_TEXT); + $stmt->bindValue(':source', $source, SQLITE3_TEXT); + $stmt->execute(); + } catch (Exception $e) { + $this->db->logError($e); + } + } +} diff --git a/www/models/Repo/Snapshot.php b/www/models/Repo/Snapshot.php index 8ec58f496..310aad4f5 100644 --- a/www/models/Repo/Snapshot.php +++ b/www/models/Repo/Snapshot.php @@ -11,34 +11,12 @@ public function __construct() $this->getConnection('main'); } - /** - * Return true if a task is queued or running for the specified snapshot - */ - public function taskRunning(int $snapId) : bool - { - try { - $stmt = $this->db->prepare("SELECT Id FROM tasks - WHERE json_extract(COALESCE(Raw_params, '{}'), '$.snap-id') == :snapId - AND Status IN ('queued', 'running')"); - $stmt->bindValue(':snapId', strval($snapId)); - $result = $stmt->execute(); - } catch (Exception $e) { - $this->db->logError($e); - } - - if ($this->db->isempty($result) === true) { - return false; - } - - return true; - } - /** * Return the list of unused snapshots for the specified repo Id and retention parameter */ - public function getUnunsed(string $repoId, string $retention) : array + public function getUnused(string $repoId, string $retention) : array { - $data = array(); + $data = []; try { $stmt = $this->db->prepare("SELECT @@ -101,4 +79,26 @@ public function exists(int $id) : bool return true; } + + /** + * Return true if a task is queued or running for the specified snapshot + */ + public function taskRunning(int $snapId) : bool + { + try { + $stmt = $this->db->prepare("SELECT Id FROM tasks + WHERE json_extract(COALESCE(Raw_params, '{}'), '$.snap-id') == :snapId + AND Status IN ('queued', 'running')"); + $stmt->bindValue(':snapId', strval($snapId)); + $result = $stmt->execute(); + } catch (Exception $e) { + $this->db->logError($e); + } + + if ($this->db->isempty($result) === true) { + return false; + } + + return true; + } } diff --git a/www/public/resources/js/classes/Container.js b/www/public/resources/js/classes/Container.js index e45c1a63d..3aadd37b2 100644 --- a/www/public/resources/js/classes/Container.js +++ b/www/public/resources/js/classes/Container.js @@ -2,9 +2,12 @@ class Container { /** * Reload container content * @param {*} container + * @param {string|null} identifier */ reload(container, identifier = null) { + var useMorphdom = false; + return new Promise((resolve, reject) => { try { /** @@ -41,18 +44,48 @@ class Container { // Print error alert: true ).then(() => { - // If an identifier is provided, reload only that specific container - if (identifier) { - // Find the specific identifier (e.g #hostDiv) in jsonValue.message - const content = $(jsonValue.message).find(identifier); + // Check if container must use Morphdom + if (typeof containersUsingMorphdom !== 'undefined' && containersUsingMorphdom.includes(container)) { + useMorphdom = true; + } - // If the content is found, replace the container with the new content - if (content.length) { - $('.reloadable-container[container="' + container + '"] > ' + identifier).replaceWith(content); - } - // Otherwise, replace the entire container with the new content + // If morphdom must be used + if (useMorphdom) { + // Replace with new content using morphdom + morphdom($('.reloadable-container[container="' + container + '"]')[0], jsonValue.message, { + // Avoid some elements to be updated if they are currently used (e.g. video playing) + onBeforeElUpdated: function (fromEl, toEl) { + /** + * Case the element is a video and it is currently playing, do not update it + */ + if (fromEl.tagName === 'VIDEO' && !fromEl.paused) { + return false; + } + + /** + * Case the element is a checkbox and it is currently checked, do not update it + */ + if (fromEl.tagName === 'INPUT' && fromEl.type === 'checkbox' && fromEl.checked) { + return false; + } + + return true; + } + }); } else { - $('.reloadable-container[container="' + container + '"]').replaceWith(jsonValue.message); + // If an identifier is provided, reload only that specific container + if (identifier) { + // Find the specific identifier (e.g #identifier) in jsonValue.message + const content = $(jsonValue.message).find(identifier); + + // If the content is found, replace the container with the new content + if (content.length) { + $('.reloadable-container[container="' + container + '"]').find(identifier).replaceWith(content); + } + // Otherwise, replace the entire container with the new content + } else { + $('.reloadable-container[container="' + container + '"]').replaceWith(jsonValue.message); + } } // Reload opened or closed elements that were opened/closed before reloading diff --git a/www/update/database/4.24.0.php b/www/update/database/4.24.0.php new file mode 100644 index 000000000..e587df81d --- /dev/null +++ b/www/update/database/4.24.0.php @@ -0,0 +1,28 @@ +db->columnExist('settings', 'REPO_DEDUPLICATION')) { + $this->db->exec("ALTER TABLE settings ADD COLUMN REPO_DEDUPLICATION CHAR(5) DEFAULT 'true'"); +} + +/** + * Create indexes for the repos table + */ +$this->db->exec("CREATE INDEX IF NOT EXISTS repos_ALL_index ON repos (Name, Releasever, Dist, Section, Source, Package_type)"); + +/** + * Create indexes for the repos_snap table + */ +$this->db->exec("CREATE INDEX IF NOT EXISTS repos_snap_status_id_repo_index ON repos_snap (Status, Id_repo)"); + +/** + * Create indexes for the repos_env table + */ +$this->db->exec("CREATE INDEX IF NOT EXISTS repos_env_id_snap_index ON repos_env (Id_snap)"); + +/** + * Create indexes for the group_members table + */ +$this->db->exec("CREATE INDEX IF NOT EXISTS group_members_id_repo_index ON group_members (Id_repo)"); +$this->db->exec("CREATE INDEX IF NOT EXISTS group_members_id_group_index ON group_members (Id_group)"); diff --git a/www/update/database/ci/test-api/local-deb-for-admin.php b/www/update/database/ci/test-api/local-deb-for-admin.php new file mode 100644 index 000000000..4d0bb3b49 --- /dev/null +++ b/www/update/database/ci/test-api/local-deb-for-admin.php @@ -0,0 +1,38 @@ + 'false', + 'schedule-type' => '', + 'schedule-date' => '', + 'schedule-time' => '', + 'schedule-notify-error' => '', + 'schedule-notify-success' => '', + 'schedule-reminder' => [], + 'schedule-recipient' => [''] +]; + +try { + $rawParams = json_encode($rawParams, JSON_THROW_ON_ERROR); +} catch (Exception $e) { + throw new Exception('Error while encoding raw params to JSON: ' . $e->getMessage()); +} + +$stmt = $this->db->prepare("INSERT INTO tasks (Type, Raw_params, Status) VALUES ('immediate', :rawParams, 'queued');"); +$stmt->bindParam(':rawParams', $rawParams); +$stmt->execute(); diff --git a/www/update/database/ci/test-api/local-deb-for-test-user1.php b/www/update/database/ci/test-api/local-deb-for-test-user1.php new file mode 100644 index 000000000..998654a70 --- /dev/null +++ b/www/update/database/ci/test-api/local-deb-for-test-user1.php @@ -0,0 +1,38 @@ + 'false', + 'schedule-type' => '', + 'schedule-date' => '', + 'schedule-time' => '', + 'schedule-notify-error' => '', + 'schedule-notify-success' => '', + 'schedule-reminder' => [], + 'schedule-recipient' => [''] +]; + +try { + $rawParams = json_encode($rawParams, JSON_THROW_ON_ERROR); +} catch (Exception $e) { + throw new Exception('Error while encoding raw params to JSON: ' . $e->getMessage()); +} + +$stmt = $this->db->prepare("INSERT INTO tasks (Type, Raw_params, Status) VALUES ('immediate', :rawParams, 'queued');"); +$stmt->bindParam(':rawParams', $rawParams); +$stmt->execute(); diff --git a/www/update/database/ci/test-api/local-deb-for-test-user2.php b/www/update/database/ci/test-api/local-deb-for-test-user2.php new file mode 100644 index 000000000..0e963b4c3 --- /dev/null +++ b/www/update/database/ci/test-api/local-deb-for-test-user2.php @@ -0,0 +1,38 @@ + 'false', + 'schedule-type' => '', + 'schedule-date' => '', + 'schedule-time' => '', + 'schedule-notify-error' => '', + 'schedule-notify-success' => '', + 'schedule-reminder' => [], + 'schedule-recipient' => [''] +]; + +try { + $rawParams = json_encode($rawParams, JSON_THROW_ON_ERROR); +} catch (Exception $e) { + throw new Exception('Error while encoding raw params to JSON: ' . $e->getMessage()); +} + +$stmt = $this->db->prepare("INSERT INTO tasks (Type, Raw_params, Status) VALUES ('immediate', :rawParams, 'queued');"); +$stmt->bindParam(':rawParams', $rawParams); +$stmt->execute(); diff --git a/www/update/database/ci/test-api/local-rpm-for-admin.php b/www/update/database/ci/test-api/local-rpm-for-admin.php new file mode 100644 index 000000000..087297661 --- /dev/null +++ b/www/update/database/ci/test-api/local-rpm-for-admin.php @@ -0,0 +1,37 @@ + 'false', + 'schedule-type' => '', + 'schedule-date' => '', + 'schedule-time' => '', + 'schedule-notify-error' => '', + 'schedule-notify-success' => '', + 'schedule-reminder' => [], + 'schedule-recipient' => [''] +]; + +try { + $rawParams = json_encode($rawParams, JSON_THROW_ON_ERROR); +} catch (Exception $e) { + throw new Exception('Error while encoding raw params to JSON: ' . $e->getMessage()); +} + +$stmt = $this->db->prepare("INSERT INTO tasks (Type, Raw_params, Status) VALUES ('immediate', :rawParams, 'queued');"); +$stmt->bindParam(':rawParams', $rawParams); +$stmt->execute(); diff --git a/www/update/database/ci/test-api/local-rpm-for-test-user1.php b/www/update/database/ci/test-api/local-rpm-for-test-user1.php new file mode 100644 index 000000000..aede539ae --- /dev/null +++ b/www/update/database/ci/test-api/local-rpm-for-test-user1.php @@ -0,0 +1,37 @@ + 'false', + 'schedule-type' => '', + 'schedule-date' => '', + 'schedule-time' => '', + 'schedule-notify-error' => '', + 'schedule-notify-success' => '', + 'schedule-reminder' => [], + 'schedule-recipient' => [''] +]; + +try { + $rawParams = json_encode($rawParams, JSON_THROW_ON_ERROR); +} catch (Exception $e) { + throw new Exception('Error while encoding raw params to JSON: ' . $e->getMessage()); +} + +$stmt = $this->db->prepare("INSERT INTO tasks (Type, Raw_params, Status) VALUES ('immediate', :rawParams, 'queued');"); +$stmt->bindParam(':rawParams', $rawParams); +$stmt->execute(); diff --git a/www/update/database/ci/test-api/local-rpm-for-test-user2.php b/www/update/database/ci/test-api/local-rpm-for-test-user2.php new file mode 100644 index 000000000..a7844ed20 --- /dev/null +++ b/www/update/database/ci/test-api/local-rpm-for-test-user2.php @@ -0,0 +1,37 @@ + 'false', + 'schedule-type' => '', + 'schedule-date' => '', + 'schedule-time' => '', + 'schedule-notify-error' => '', + 'schedule-notify-success' => '', + 'schedule-reminder' => [], + 'schedule-recipient' => [''] +]; + +try { + $rawParams = json_encode($rawParams, JSON_THROW_ON_ERROR); +} catch (Exception $e) { + throw new Exception('Error while encoding raw params to JSON: ' . $e->getMessage()); +} + +$stmt = $this->db->prepare("INSERT INTO tasks (Type, Raw_params, Status) VALUES ('immediate', :rawParams, 'queued');"); +$stmt->bindParam(':rawParams', $rawParams); +$stmt->execute(); diff --git a/www/version b/www/version index 3374ced24..2c4686f5c 100644 --- a/www/version +++ b/www/version @@ -1 +1 @@ -4.23.0 \ No newline at end of file +4.24.0 \ No newline at end of file diff --git a/www/views/includes/containers/browse/list.inc.php b/www/views/includes/containers/browse/list.inc.php index f85dca143..3a1ffd364 100644 --- a/www/views/includes/containers/browse/list.inc.php +++ b/www/views/includes/containers/browse/list.inc.php @@ -12,7 +12,10 @@
= $repo ?>
+= $repo ?>
+ = strtoupper($myrepo->getPackageType()) ?> +Root URL for accessing repositories. This URL is not browseable for security reasons. To explore the content of a repository snapshot, use the snapshot browsing system.
+Enable or disable deduplication of packages in snapshots. When enabled, identical packages in snapshots will be stored only once, saving disk space. Default: enabled.
+Note: deduplication is using hard links, disable it if your repositories are stored on a filesystem that does not support hard links (e.g., S3, NFS).
+ +Maximum number of unused snapshots to keep per repository. Set to 0 to disable retention.
diff --git a/www/views/includes/containers/stats/list.inc.php b/www/views/includes/containers/stats/list.inc.php index bf91f609c..ebffe5ca4 100644 --- a/www/views/includes/containers/stats/list.inc.php +++ b/www/views/includes/containers/stats/list.inc.php @@ -12,7 +12,10 @@= $repo ?>
+= $repo ?>
+ = strtoupper($myrepo->getPackageType()) ?> +This chart shows the number of accesses to the repository snapshot over time.
+Number of accesses to the repository snapshot over time.