Skip to content

Conversation

@jmendeza
Copy link
Member

@jmendeza jmendeza commented Jan 16, 2026

Site exists API. Cleanup
craftercms/craftercms#6061

Summary by CodeRabbit

  • New Features

    • Added an API endpoint to check whether a site exists.
  • Improvements

    • Unified repository garbage-collection and startup cleanup flows.
    • Simplified input validation for deleting user properties.
  • Refactor

    • Consolidated site-related services and integrations to a single service surface and updated monitoring/cleanup wiring.
  • Breaking Changes

    • Legacy "list all sites" public APIs and related hooks removed; update scripts/integrations accordingly.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Jan 16, 2026

Walkthrough

Adds GET /api/2/sites/{siteId}/exists; removes legacy getAllAvailableSites usages; replaces RepositoryCleanupJob with SitesService.garbageCollectRepositories(); migrates callers and wiring from v1 SiteService to v2 SitesService across controllers, services, repos, config, scripts, and tests.

Changes

Cohort / File(s) Summary
API Definition
src/main/api/studio-api.yaml
Added GET /api/2/sites/{siteId}/exists returning { "exists": boolean } and standard error mappings.
Sites Controller
src/main/java/org/craftercms/studio/controller/rest/v2/SitesController.java
Added siteExists(@ValidSiteId @PathVariable siteId) endpoint; switched class-level mapping to API_2 + SITES.
Request Constants & Users Controller
src/main/java/org/craftercms/studio/controller/rest/v2/RequestMappingConstants.java, src/main/java/org/craftercms/studio/controller/rest/v2/UsersController.java
Moved/added LOG_MONITOR_ENGINE_PROXY constant; removed @Valid wrapper from deleteUserProperties list parameter.
Service APIs
src/main/java/org/craftercms/studio/api/v1/service/site/SiteService.java, src/main/java/org/craftercms/studio/api/v2/service/site/SitesService.java
Removed getAllAvailableSites() from v1; removed setPublishedRepoCreated() and added garbageCollectRepositories() in v2.
Service Implementations
src/main/java/org/craftercms/studio/impl/v1/service/site/SiteServiceImpl.java, src/main/java/org/craftercms/studio/impl/v2/service/site/SitesServiceImpl.java, src/main/java/org/craftercms/studio/impl/v2/service/site/internal/SitesServiceInternalImpl.java
Removed v1 getAllAvailableSites() impl; SitesServiceImpl.exists annotated with @SiteId and permission; removed setPublishedRepoCreated; added garbageCollectRepositories() delegating to internal; internal impl performs global GC and per-site GC for READY sites.
Repository & Startup Cleanup
src/main/java/org/craftercms/studio/impl/v1/repository/job/RepositoryCleanupJob.java, src/main/java/org/craftercms/studio/impl/v2/repository/RepositoryStartupCleanup.java, src/main/java/org/craftercms/studio/impl/v2/repository/GitContentRepositoryImpl.java
Deleted RepositoryCleanupJob; RepositoryStartupCleanup now uses SitesService and filters getSitesByState(READY); GitContentRepositoryImpl removed setSiteService setter and calls siteDao.setPublishedRepoCreated(siteId).
Disk Monitor & Tests
src/main/java/org/craftercms/studio/impl/v2/monitor/DiskMonitor.java, src/test/java/org/craftercms/studio/impl/v2/monitor/DiskMonitorTest.java
Replaced dependency on RepositoryCleanupJob with SitesService; call changed from cleanupAllRepositories() to garbageCollectRepositories(); tests updated accordingly.
Spring Context / Scheduling
src/main/resources/crafter/studio/studio-services-context.xml
Removed studioRepositoryCleanupJob bean; trigger now targets sitesServiceInternal.garbageCollectRepositories(); wired sitesServiceInternal into startup and diskMonitor beans; removed some siteService injections.
Groovy Script APIs / UI scripts
src/main/webapp/default-site/scripts/api/SiteServices.groovy, src/main/webapp/default-site/scripts/api/impl/site/SpringSiteServices.groovy, src/main/webapp/default-site/scripts/api/impl/content/ContentMonitoring.groovy
Removed getAllAvailableSites() delegations; ContentMonitoring switched bean to sitesServiceInternal, uses getSitesByState(READY), and passes site.siteId to monitoring calls.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant Client as Client
participant Controller as SitesController
participant Service as SitesService
participant DAO as SitesServiceInternal/DAO
rect rgba(52,152,219,0.5)
Client->>Controller: GET /api/2/sites/{siteId}/exists
Controller->>Service: exists(siteId)
Service->>DAO: exists(siteId) (permission/@siteid)
DAO-->>Service: boolean
Service-->>Controller: boolean
Controller-->>Client: 200 { "exists": boolean }
end

mermaid
sequenceDiagram
participant Scheduler as Trigger/DiskMonitor
participant SitesService as SitesService
participant BlobRepo as BlobAwareRepository
participant SiteDAO as Site DAO (getSitesByState)
rect rgba(46,204,113,0.5)
Scheduler->>SitesService: garbageCollectRepositories()
SitesService->>BlobRepo: garbageCollectGitRepositories(EMPTY) -- global GC
SitesService->>SiteDAO: getSitesByState(READY)
SiteDAO-->>SitesService: list of sites
loop per site
SitesService->>BlobRepo: garbageCollectGitRepositories(siteId)
end
SitesService-->>Scheduler: done
end

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • sumerjabri
🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 19.44% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: introducing a new Site exists API and performing related cleanup of deprecated code patterns.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@src/main/java/org/craftercms/studio/controller/rest/v2/UsersController.java`:
- Around line 521-523: The controller's deleteUserProperties method currently
accepts a potentially empty List properties which can cause the DAO's
deleteUserProperties to generate invalid SQL (property_key IN ()) in MyBatis;
either reapply validation by annotating the properties parameter with `@NotEmpty`
(e.g., change `@RequestParam` List<@NotBlank String> properties to List<@NotBlank
`@NotEmpty` String> properties) or add a guard in the service/controller before
invoking the DAO: check if properties.isEmpty() and return a successful no-op
Result or throw a validation error, ensuring the DAO.deleteUserProperties(...)
is never called with an empty collection.

In
`@src/main/java/org/craftercms/studio/impl/v2/service/site/SitesServiceImpl.java`:
- Around line 96-100: The internal call this.exists(siteId) inside
SitesServiceImpl.getSite(...) bypasses Spring AOP so the `@HasPermission` on
exists(...) won’t be applied on self-invocation; to fix, either annotate
getSite(...) with the same permission annotation (add `@HasPermission`(type =
DefaultPermission.class, action = PERMISSION_CONTENT_READ) to getSite) so
permission is enforced on every invocation, or change the code to call the
proxied bean (inject SitesService or ApplicationContext and call
sitesService.exists(siteId) instead of this.exists(siteId)) to ensure the AOP
interceptor runs; refer to getSite(), exists(), SitesServiceImpl,
`@HasPermission`, PERMISSION_CONTENT_READ and `@SiteId` when making the change.
🧹 Nitpick comments (5)
src/main/java/org/craftercms/studio/api/v2/service/site/SitesService.java (1)

217-221: Minor: Improve Javadoc grammar.

The Javadoc comment has a grammatical issue. Consider rephrasing for clarity.

📝 Suggested Javadoc improvement
 /**
- * Git Garbage collect global repository all site repositories (sandbox and published)
+ * Run Git garbage collection on the global repository and all site repositories (sandbox and published)
  */
 void garbageCollectRepositories();
src/main/java/org/craftercms/studio/impl/v2/service/site/internal/SitesServiceInternalImpl.java (2)

66-68: Inconsistent use of commons-lang vs commons-lang3.

Line 66 imports EMPTY from org.apache.commons.lang.StringUtils (commons-lang 2.x), while lines 67-68 use org.apache.commons.lang3.StringUtils (commons-lang3). For consistency, prefer using commons-lang3 throughout.

♻️ Suggested fix
-import static org.apache.commons.lang.StringUtils.EMPTY;
+import static org.apache.commons.lang3.StringUtils.EMPTY;

556-568: Consider adding error handling for per-site garbage collection.

The garbageCollectRepositories() method iterates over all READY sites and calls garbageCollectGitRepositories() for each. If GC fails for one site, the exception will propagate and subsequent sites won't be processed. Consider wrapping individual site GC calls in try-catch to ensure all sites are processed.

♻️ Suggested improvement with error handling
 `@Override`
 public void garbageCollectRepositories() {
     blobAwareRepository.garbageCollectGitRepositories(EMPTY);
     getSitesByState(READY).forEach(site -> {
         String siteId = site.getSiteId();
-        blobAwareRepository.garbageCollectGitRepositories(siteId);
+        try {
+            blobAwareRepository.garbageCollectGitRepositories(siteId);
+        } catch (Exception e) {
+            logger.error("Failed to garbage collect repositories for site '{}'", siteId, e);
+        }
     });
 }
src/main/api/studio-api.yaml (1)

2034-2062: Add summary/operationId/description (and align security note).

This endpoint is missing the usual OpenAPI metadata used throughout the spec. Please add at least a summary and operationId, and align the description (and security if applicable) with the actual permission requirements so the 401/403 responses remain consistent.

♻️ Suggested doc metadata (adjust permission/security to match implementation)
 /api/2/sites/{siteId}/exists:
     get:
         tags:
             - sites
+        summary: Check if a site exists
+        operationId: siteExists
+        description: Checks whether a site exists for the given siteId.
         parameters:
             -   name: siteId
                 in: path
                 schema:
                     type: string
                 required: true
src/main/java/org/craftercms/studio/impl/v2/service/site/SitesServiceImpl.java (1)

44-44: Unused import: Site.State.READY is not referenced in this file.

Consider removing this import unless it's intended for upcoming changes in this PR.

Suggested fix
-import static org.craftercms.studio.api.v2.dal.Site.State.READY;

Comment on lines 96 to 100
@Override
public boolean exists(String siteId) {
@HasPermission(type = DefaultPermission.class, action = PERMISSION_CONTENT_READ)
public boolean exists(@SiteId String siteId) {
return sitesServiceInternal.exists(siteId);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n src/main/java/org/craftercms/studio/impl/v2/service/site/SitesServiceImpl.java | sed -n '90,140p'

Repository: craftercms/studio

Length of output: 2273


🏁 Script executed:

#!/bin/bash
# Search for calls to getSite() to understand usage patterns
echo "=== getSite() method calls ===" 
rg -n 'getSite\s*\(' --type java -C 1 | head -80

echo -e "\n=== getSite() method definitions ===" 
rg -n 'public.*getSite\s*\(' --type java -A 3

Repository: craftercms/studio

Length of output: 12646


🏁 Script executed:

#!/bin/bash
# Find the SitesService interface
find . -type f -name "*SitesService.java" | grep -v Impl | grep v2

# Show the interface
echo "=== SitesService interface ==="
cat -n $(find . -type f -name "SitesService.java" | grep v2 | grep -v Impl) | sed -n '1,150p'

Repository: craftercms/studio

Length of output: 6127


🏁 Script executed:

cat -n src/main/java/org/craftercms/studio/api/v2/service/site/SitesService.java | sed -n '145,200p'

Repository: craftercms/studio

Length of output: 2284


🏁 Script executed:

#!/bin/bash
# Check if there are REST endpoints or other public entry points calling getSite()
echo "=== Checking for REST controller methods calling getSite() ==="
rg -n 'GetMapping|PostMapping|PutMapping|DeleteMapping' --type java -A 10 | grep -B 10 'getSite' | head -50

echo -e "\n=== Check if other service methods calling getSite() have `@HasPermission` ==="
rg -B 5 '\.getSite\(' src/main/java --type java | grep -E '(`@HasPermission`|public|private)' | head -40

Repository: craftercms/studio

Length of output: 13630


🏁 Script executed:

#!/bin/bash
# Check if SitesController has a REST endpoint for getSite
echo "=== SitesController methods ==="
rg -n 'GetMapping|PostMapping' src/main/java/org/craftercms/studio/controller/rest/v2/SitesController.java -A 5 | head -100

echo -e "\n=== Search for explicit getSite REST endpoint ==="
rg -n 'getSite|get-site' src/main/java/org/craftercms/studio/controller/rest/v2/SitesController.java -B 3 -A 3

Repository: craftercms/studio

Length of output: 3091


🏁 Script executed:

#!/bin/bash
# Check if there are any patterns where service methods call other service's methods
echo "=== Check how getSite is used in service layer ==="
rg -n 'sitesService\.getSite|\.getSite\(siteId\)' src/main/java/org/craftercms/studio --type java -B 2 | head -80

echo -e "\n=== Check if getSite is used in REST controllers ==="
rg -n 'getSite' src/main/java/org/craftercms/studio/controller/rest/v2 --type java

Repository: craftercms/studio

Length of output: 16421


Permission enforcement on exists() is a good security addition.

The @HasPermission annotation with PERMISSION_CONTENT_READ and @SiteId properly secures the method. However, note that getSite() (lines 128-133) calls this.exists(siteId) internally. Due to Spring AOP's self-invocation limitation, the permission check will not be enforced on this internal call. While getSite() currently lacks a direct REST endpoint and is primarily used within the service layer where callers typically have their own permission validation, this could be a concern if getSite() becomes part of a public-facing API in the future. Consider adding explicit @HasPermission to getSite() if it should require permission enforcement at all invocation points, not just when called externally.

🤖 Prompt for AI Agents
In
`@src/main/java/org/craftercms/studio/impl/v2/service/site/SitesServiceImpl.java`
around lines 96 - 100, The internal call this.exists(siteId) inside
SitesServiceImpl.getSite(...) bypasses Spring AOP so the `@HasPermission` on
exists(...) won’t be applied on self-invocation; to fix, either annotate
getSite(...) with the same permission annotation (add `@HasPermission`(type =
DefaultPermission.class, action = PERMISSION_CONTENT_READ) to getSite) so
permission is enforced on every invocation, or change the code to call the
proxied bean (inject SitesService or ApplicationContext and call
sitesService.exists(siteId) instead of this.exists(siteId)) to ensure the AOP
interceptor runs; refer to getSite(), exists(), SitesServiceImpl,
`@HasPermission`, PERMISSION_CONTENT_READ and `@SiteId` when making the change.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In
`@src/main/java/org/craftercms/studio/impl/v2/service/site/internal/SitesServiceInternalImpl.java`:
- Line 66: Replace the v2 StringUtils EMPTY import with the commons-lang3
version to match the other imports: change the static import of EMPTY from
org.apache.commons.lang.StringUtils to org.apache.commons.lang3.StringUtils so
it aligns with the existing static imports of isEmpty/isNotEmpty used in
SitesServiceInternalImpl and maintains consistency across the class.
♻️ Duplicate comments (1)
src/main/java/org/craftercms/studio/impl/v2/service/site/SitesServiceImpl.java (1)

96-100: Permission check won’t apply to self‑invocation from getSite.

getSite() calls exists(siteId) within the same bean, so Spring AOP won’t enforce @HasPermission on that internal call. If getSite can be reached without its own permission checks, consider annotating getSite or calling the proxied bean.

Spring AOP self-invocation advice not applied to internal method calls?
🧹 Nitpick comments (1)
src/main/java/org/craftercms/studio/impl/v2/service/site/internal/SitesServiceInternalImpl.java (1)

562-567: Isolate per-site GC failures so one site doesn’t stop the sweep.

If a single repository GC throws, the remaining sites won’t be processed. Consider logging and continuing per-site.

♻️ Suggested update
 public void garbageCollectRepositories() {
 	blobAwareRepository.garbageCollectGitRepositories(EMPTY);
 	getSitesByState(READY).forEach(site -> {
 		String siteId = site.getSiteId();
-		blobAwareRepository.garbageCollectGitRepositories(siteId);
+		try {
+			blobAwareRepository.garbageCollectGitRepositories(siteId);
+		} catch (Exception e) {
+			logger.warn("Failed to garbage collect repository for site '{}'", siteId, e);
+		}
 	});
 }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant