Skip to content

VPR-59 [1/4] CMS migration: files, photos, import, rate limiting (backend)#251

Open
rlorenzo wants to merge 2 commits into
mainfrom
VPR-59-cms-r1-files-backend
Open

VPR-59 [1/4] CMS migration: files, photos, import, rate limiting (backend)#251
rlorenzo wants to merge 2 commits into
mainfrom
VPR-59-cms-r1-files-backend

Conversation

@rlorenzo

@rlorenzo rlorenzo commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Slice 1 of 4 stacked PRs for the CMS migration (VPR-59), restacked from the CI-verified branch tip. Replaces the earlier 6-PR stack (#245-#250): those slices were historical cut points, so code predating the branch's later OS-independent path handling and ReSharper/Fallow cleanups failed CI on Linux runners. Each new slice is built from the final file states, so every slice carries all fixes and passes the full gate set (verified locally per slice: lint, 2,400+ backend tests, 1,026 frontend tests, build).

Scope — files-domain backend

  • /api/cms/files: paged list with filters and folder counts, upload with name-conflict handling, edit with permission/person deltas and a 409 stale-edit guard, soft delete/restore with owner scoping, admin-gated permanent delete.
  • CmsFileStorageService (containment-checked paths, collision handling), CmsFileCrypto + CmsFileEncryptionService (CF-compatible AES so the legacy ColdFusion app can still read encrypted files), CmsFileAuditService, import-from-legacy-webroot, 30-day trash purge job (Cms:TrashPurgeEnabled, default off).
  • User photos (/api/cms/photos, by mail/login/iam/mothra) with Last-Modified/304 conditional caching.
  • Download rate limiting on the retained /CMS/Files handler (token bucket for ZIPs, sliding window for files) and the rebuilt Data/CMS.cs download path with legacy-parity access auditing.
  • CMSOptionsController for permission/person typeahead; unit tests for all of the above.

All 27 review comments from the previous stack are addressed in this restack (fixes) or answered on the old PRs (declines with rationale).

Stack: this PR → content blocks/left nav backend → management SPA. Merging this slice alone ships inert-but-tested APIs.

@codecov-commenter

Copy link
Copy Markdown

Bundle Report

Bundle size has no change ✅

@codecov-commenter

codecov-commenter commented Jul 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 72.94056% with 519 lines in your changes missing coverage. Please review.
✅ Project coverage is 46.60%. Comparing base (ef4f08b) to head (8b2cee3).

Files with missing lines Patch % Lines
web/Areas/CMS/Services/CmsFileService.cs 80.88% 79 Missing and 33 partials ⚠️
web/Areas/CMS/Services/CmsFileAuditService.cs 7.79% 71 Missing ⚠️
web/Areas/CMS/Services/CmsFileImportService.cs 74.08% 61 Missing and 10 partials ⚠️
web/Areas/CMS/Services/CmsDownloadRateLimiting.cs 15.78% 63 Missing and 1 partial ⚠️
web/Areas/CMS/Services/CmsFileEncryptionService.cs 0.00% 45 Missing ⚠️
web/Areas/CMS/Services/CmsFileStorageService.cs 75.58% 33 Missing and 9 partials ⚠️
web/Areas/CMS/Data/CMS.cs 31.03% 39 Missing and 1 partial ⚠️
web/Areas/CMS/Controllers/CMSFilesController.cs 80.58% 24 Missing and 9 partials ⚠️
web/Areas/CMS/Services/CmsUserPhotoService.cs 80.51% 10 Missing and 5 partials ⚠️
web/Areas/CMS/Services/CmsFileCrypto.cs 83.92% 9 Missing ⚠️
... and 8 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #251      +/-   ##
==========================================
+ Coverage   45.29%   46.60%   +1.30%     
==========================================
  Files         897      920      +23     
  Lines       51929    53704    +1775     
  Branches     4879     5089     +210     
==========================================
+ Hits        23522    25028    +1506     
- Misses      27803    27992     +189     
- Partials      604      684      +80     
Flag Coverage Δ
backend 46.75% <72.94%> (+1.36%) ⬆️
frontend 43.41% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

@rlorenzo rlorenzo requested a review from Copilot July 3, 2026 02:30
@rlorenzo

rlorenzo commented Jul 3, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Implements the first slice of the CMS migration backend for the files domain: new CMS file-management APIs (list/upload/edit/delete/restore/audit/import/bulk-encrypt), hardened legacy-parity download handling (nosniff + attachment for inline-unsafe types), CMS download rate limiting, a conditional-cached user-photo API, and a gated scheduled trash-purge job—plus supporting validators/constants and comprehensive unit tests.

Changes:

  • Add CMS file management services/controllers (storage, encryption/crypto, audit, import, trash purge job) and DTO/mapping infrastructure.
  • Harden and optimize the existing /CMS/Files download surface (rate limiting, access auditing parity, nosniff + attachment rules, permission lookup efficiency).
  • Add CMS user photo endpoints + service with Last-Modified/304 support and caching headers, plus shared validation/date-range helpers and tests.

Reviewed changes

Copilot reviewed 49 out of 49 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
web/Viper.csproj Adds htmldiff dependency for diff rendering.
web/Services/IHtmlSanitizerService.cs Adds SanitizeDiff API contract.
web/Services/HtmlSanitizerService.cs Implements diff-aware sanitizer policy.
web/Program.cs Registers CMS download rate limiting + middleware.
web/Classes/Utilities/DateRangeHelper.cs Shared inclusive “To date” handling.
web/Areas/CMS/Validation/SafeUrlAttribute.cs URL validation with optional relative support.
web/Areas/CMS/Validation/MaxLengthEachAttribute.cs Per-element max-length validation for collections.
web/Areas/CMS/Services/CmsUserPhotoService.cs User photo resolution + alt photo support.
web/Areas/CMS/Services/CmsFileStorageService.cs Safe file storage + path containment + collisions.
web/Areas/CMS/Services/CmsFileService.cs Core file CRUD, encryption toggle, purge.
web/Areas/CMS/Services/CmsFileResponse.cs Download response hardening helpers.
web/Areas/CMS/Services/CmsFileImportService.cs Legacy-webroot import + bulk encrypt tools.
web/Areas/CMS/Services/CmsFileEncryptionService.cs Master-key wrapper + in-place transforms.
web/Areas/CMS/Services/CmsFileCrypto.cs CF-compatible AES-ECB + UU encoding primitives.
web/Areas/CMS/Services/CmsFileAuditService.cs File audit row creation + paging/filtering.
web/Areas/CMS/Services/CmsDownloadRateLimiting.cs Rate limiter policy + partition keying.
web/Areas/CMS/Services/CmsConcurrencyException.cs Shared 409 stale-edit exception type.
web/Areas/CMS/Models/DTOs/CreateLinkDto.cs Fixes SafeUrlAttribute namespace reference.
web/Areas/CMS/Models/DTOs/CmsFileRequests.cs Request DTOs for create/update endpoints.
web/Areas/CMS/Models/DTOs/CmsFileImportDtos.cs Import + preview + bulk-encrypt DTOs.
web/Areas/CMS/Models/DTOs/CmsFileDto.cs File management DTOs and name-check result.
web/Areas/CMS/Models/CmsFileMapper.cs Mapperly-based entity→DTO mapping.
web/Areas/CMS/Models/CMSFile.cs Aligns URL generation with new handler shape.
web/Areas/CMS/Jobs/CmsTrashPurgeScheduledJob.cs Gated daily purge job for trashed files.
web/Areas/CMS/Data/CMS.cs Download hardening, auditing parity, perf tweaks.
web/Areas/CMS/Controllers/CMSUserPhotoController.cs /api/cms/photos endpoints + 304 behavior.
web/Areas/CMS/Controllers/CMSOptionsController.cs CMS option lists for permissions/people.
web/Areas/CMS/Controllers/CMSFilesController.cs /api/cms/files management API surface.
web/Areas/CMS/Controllers/CMSController.cs Enables rate limiting on /CMS/Files.
web/Areas/CMS/Constants/CmsTrash.cs Trash retention + config key constants.
web/Areas/CMS/Constants/CmsPermissions.cs Central CMS permission strings.
web/Areas/CMS/Constants/CmsFileTypes.cs File allowlist + inline-unsafe extension set.
web/Areas/CMS/Constants/CmsFileNaming.cs Friendly-name + canonical folder key rules.
web/appsettings.json Adds default-off trash purge config.
test/Services/HtmlSanitizerServiceTests.cs Tests for SanitizeDiff XSS guarantees.
test/CMS/SafeUrlAttributeTests.cs SafeUrlAttribute absolute/relative-mode tests.
test/CMS/MaxLengthEachAttributeTests.cs MaxLengthEachAttribute behavior tests.
test/CMS/CmsUserPhotoServiceTests.cs Photo service resolution/traversal/Last-Modified tests.
test/CMS/CMSUserPhotoControllerTests.cs Controller wiring + cache/304 behavior tests.
test/CMS/CmsTrashPurgeScheduledJobTests.cs Ensures purge gate stays off by default.
test/CMS/CmsFileStorageServiceTests.cs Storage containment, collisions, temp writes tests.
test/CMS/CMSFilesControllerTests.cs Controller status mapping + admin-only hard delete tests.
test/CMS/CmsFileResponseTests.cs nosniff + attachment/inline disposition tests.
test/CMS/CmsFileImportServiceTests.cs Import preview/execute + bulk-encrypt tests.
test/CMS/CmsFileCryptoTests.cs Crypto format round-trip compatibility tests.
test/CMS/CmsDownloadRateLimitingTests.cs ZIP vs file partitioning key tests.
test/CMS/CMSContentPermissionTests.cs Content permission gate tests (perf change coverage).
test/Classes/Utilities/DateRangeHelperTests.cs Exclusive upper-bound helper tests.

Comment thread web/Areas/CMS/Services/CmsFileService.cs Outdated
Comment thread web/Areas/CMS/Services/CmsFileService.cs
Comment thread web/Areas/CMS/Services/CmsFileEncryptionService.cs
@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds CMS file-management APIs, storage, encryption, import, purge, rate limiting, and photo serving, plus a diff-aware HTML sanitizer path and tests.

Changes

CMS File Management System

Layer / File(s) Summary
Shared contracts and models
web/Areas/CMS/Constants/*, web/Areas/CMS/Models/DTOs/*, web/Areas/CMS/Validation/SafeUrlAttribute.cs, web/Areas/CMS/Services/CmsFilePathSafety.cs
Defines CMS file naming, file type, permission, trash, DTO, request, concurrency, and path-safety helpers used by file management code.
Validation and utility tests
web/Areas/CMS/Services/CmsFileResponse.cs, web/Areas/CMS/Validation/MaxLengthEachAttribute.cs, web/Classes/Utilities/DateRangeHelper.cs, test/CMS/CmsFileResponseTests.cs, test/CMS/MaxLengthEachAttributeTests.cs, test/CMS/SafeUrlAttributeTests.cs, test/Classes/Utilities/DateRangeHelperTests.cs
Adds validation helpers and tests for CMS file response hardening, max-length-per-item validation, safe URL validation, and date-range bounds.
Crypto and managed storage
web/Areas/CMS/Services/CmsFileCrypto.cs, web/Areas/CMS/Services/CmsFileEncryptionService.cs, web/Areas/CMS/Services/CmsFileStorageService.cs, test/CMS/CmsFileCryptoTests.cs, test/CMS/CmsFileStorageServiceTests.cs
Adds CMS file crypto primitives, in-place encryption, and managed storage safety and collision handling.
Data layer, audit, and file service
web/Areas/CMS/Data/CMS.cs, web/Areas/CMS/Models/CMSFile.cs, web/Areas/CMS/Models/CmsFileMapper.cs, web/Areas/CMS/Services/CmsFileAuditService.cs, web/Areas/CMS/Services/CmsFileImportService.cs, web/Areas/CMS/Services/CmsFileService.cs, test/CMS/CMSContentPermissionTests.cs, test/CMS/CmsFileImportServiceTests.cs, test/CMS/CmsFileServiceTests.cs
Updates CMS data access, file mapping, auditing, CRUD orchestration, and legacy import/bulk-encryption flows, with tests covering permissions and service behavior.
Controllers, photos, purge, and wiring
web/Areas/CMS/Controllers/CMSController.cs, web/Areas/CMS/Controllers/CMSFilesController.cs, web/Areas/CMS/Controllers/CMSOptionsController.cs, web/Areas/CMS/Controllers/CMSUserPhotoController.cs, web/Areas/CMS/Jobs/CmsTrashPurgeScheduledJob.cs, web/Areas/CMS/Services/CmsDownloadRateLimiting.cs, web/Areas/CMS/Services/CmsUserPhotoService.cs, web/Program.cs, web/appsettings.json, .coderabbit.yaml, test/CMS/CMSFilesControllerTests.cs, test/CMS/CMSOptionsControllerTests.cs, test/CMS/CMSUserPhotoControllerTests.cs, test/CMS/CmsTrashPurgeScheduledJobTests.cs, test/CMS/CmsUserPhotoServiceTests.cs, test/CMS/CmsDownloadRateLimitingTests.cs
Adds CMS file, options, and photo endpoints; download rate limiting; the trash purge job; and startup/config wiring, with tests for controller and service flows.

HTML Diff Sanitization

Layer / File(s) Summary
Diff sanitizer policy and API
web/Services/HtmlSanitizerService.cs, web/Services/IHtmlSanitizerService.cs, web/Viper.csproj, test/Services/HtmlSanitizerServiceTests.cs
Adds SanitizeDiff with a separate sanitizer that permits ins/del markers while retaining the existing sanitization policy for unsafe content.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

  • ucdavis/VIPER#184: Both PRs modify CMS download logic in web/Areas/CMS/Data/CMS.cs, including ZIP/file response handling and hardening.

Suggested reviewers: bsedwards

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title is specific and matches the main CMS backend migration work in this PR.
Description check ✅ Passed The description clearly describes the CMS files, photos, import, and rate-limiting backend changes in scope.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch VPR-59-cms-r1-files-backend

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 15

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
web/Areas/CMS/Data/CMS.cs (1)

398-424: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Per-file synchronous SaveChanges() in the DownloadZip loop.

AuditFileAccess clears the change tracker and calls SaveChanges() for every guid in guids (both allowed and now denied files). A zip request with many files causes that many individual round-trips to the audit table instead of one batched write.

Consider collecting FileAudit rows in the loop and calling AddRange + a single SaveChanges() after the loop completes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/Areas/CMS/Data/CMS.cs` around lines 398 - 424, The DownloadZip audit path
is doing a synchronous SaveChanges for every file through AuditFileAccess,
causing one database round-trip per guid. Update the CMS.DownloadZip flow and
the AuditFileAccess usage so the loop only collects FileAudit entries for both
allowed and denied files, then persist them once after the loop with AddRange
and a single SaveChanges call. Keep the existing audit action names and use the
current _viperContext, CheckFilePermission, and AuditFileAccess flow to locate
the batching change.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/Services/HtmlSanitizerServiceTests.cs`:
- Around line 249-269: Add an idempotency regression test for SanitizeDiff in
HtmlSanitizerServiceTests, mirroring the existing Sanitization_is_idempotent
coverage for Sanitize. Use SanitizeDiff as the target and verify that running it
twice on already-sanitized diff HTML (for example content with <ins> and <del>
markers) produces the same output both times. This will protect the SanitizeDiff
method and its diff-specific allowlist from double-sanitization issues when
re-rendered.

In `@web/Areas/CMS/Constants/CmsFileTypes.cs`:
- Around line 31-37: The CmsFileTypes allow-list currently includes potentially
dangerous binary/archive types, which creates a malware-distribution risk.
Review the CmsFileTypes mapping and remove unsupported executable/archive
entries such as exe, dmg, 7z, and zip unless there is a confirmed legacy
requirement; if they must remain, ensure the upload flow that uses this
allow-list also routes those types through malware scanning before they are
accepted into the CMS file store. Refer to the CmsFileTypes constant so the
allow-list and any related upload validation stay in sync.

In `@web/Areas/CMS/Controllers/CMSFilesController.cs`:
- Around line 259-317: Add an upper bound check for batch sizes in
CMSFilesController so ImportFiles, PreviewImport, and BulkEncrypt reject
oversized requests instead of only checking for empty lists. Use the existing
request collections (CmsFileImportRequest.FilePaths and the BulkEncrypt
fileGuids parameter) to enforce a reasonable max, and return BadRequest when
exceeded. Keep the validation close to the guards in ImportFiles, PreviewImport,
and BulkEncrypt so the limit is enforced before calling _importService.

In `@web/Areas/CMS/Controllers/CMSOptionsController.cs`:
- Around line 47-84: Add coverage for CMSOptionsController.SearchPeople because
it has non-trivial search filtering and result-shaping logic. Create a
controller test class for CMSOptionsController that verifies the
min-length/whitespace guard returns an empty list, the multi-field OR search
matches DisplayLastName/DisplayFirstName/LoginId/MailId, and the query respects
ordering plus the Take(25) limit. Use the SearchPeople action and
CmsPersonOption type to locate the behavior.
- Around line 47-74: The SearchPeople action in CMSOptionsController is using
Contains() on concatenated name fields and LoginId/MailId, which translates to
leading-wildcard scans and won’t benefit from indexes. Update the filtering
logic to use an index-friendly search strategy for AaudUsers, such as a
persisted/computed display-name field or full-text search, and adjust the query
in SearchPeople so it targets those indexed/searchable fields instead of
concatenated expressions and broad Contains() predicates.

In `@web/Areas/CMS/Data/CMS.cs`:
- Around line 642-647: `DecryptFile` is re-reading the master key from disk on
every invocation, which is expensive when `DownloadZip` calls it repeatedly
inside the archive loop. Update the `DecryptFile` flow to reuse a cached master
key instead of calling
`CmsFileCrypto.ReadMasterKey(CmsFileCrypto.GetDefaultKeyFilePath())` each time,
either by confirming `ReadMasterKey` caches internally or by caching the key
once per `DownloadZip`/`ProvideFile` execution and passing it through to
`DecryptFile` or delegating to `CmsFileEncryptionService`.
- Around line 60-86: The CMS permission check is using a hardcoded security
string instead of the shared constant. Update the access-resolution logic in
CMS.cs where isCmsAdmin is assigned to use CmsPermissions.ManageContentBlocks
rather than "SVMSecure.CMS.ManageContentBlocks", so the check stays aligned with
the rest of the CMS permission code and avoids drift.
- Around line 611-619: The kiosk IP filter in AuditFileAccess only checks the
raw RemoteIpAddress string, so IPv4-mapped IPv6 values like ::ffff:192.168.x.x
slip through. Update the RemoteIpAddress handling in CMS.AuditFileAccess to
normalize mapped IPv4 addresses before evaluating the kiosk range, then perform
the 192.168. prefix check using StartsWith with StringComparison.Ordinal.

In `@web/Areas/CMS/Services/CmsFileAuditService.cs`:
- Around line 87-88: The audit history pagination in CmsFileAuditService has the
same unbounded perPage issue as CmsFileService.GetFilesAsync: only the lower
bound is enforced. Update the paging logic around the page/perPage normalization
to also cap perPage at a safe maximum, using the existing CmsFileAuditService
method that fetches audit history so callers cannot request arbitrarily large
page sizes.

In `@web/Areas/CMS/Services/CmsFileEncryptionService.cs`:
- Around line 25-31: `CmsFileEncryptionService` is still scoped, so the
`_masterKey` Lazy<string> is recreated per request and repeatedly hits the key
file on first use. Update the DI registration or introduce a singleton key-cache
so the master key is loaded once and reused across scopes, and keep the caching
behind `CmsFileEncryptionService`/`CmsFileCrypto.ReadMasterKey` rather than per
scoped instance.

In `@web/Areas/CMS/Services/CmsFileImportService.cs`:
- Around line 331-389: `EncryptSingleFileAsync` is missing some expected
file/key failure handling, so malformed key data or invalid paths can still
escape and abort `BulkEncryptAsync`. Update this method to catch the same
anticipated exceptions as `ImportSingleFileAsync`, especially `FormatException`
alongside the existing file access cases, and convert them into `result.Message`
so the bulk loop can continue collecting results. Keep the fix localized to
`EncryptSingleFileAsync` and preserve the existing rollback behavior for save
failures.

In `@web/Areas/CMS/Services/CmsFileService.cs`:
- Line 390: The current encryption toggle flow in CmsFileService unnecessarily
rewrites the existing file on disk when a replacement upload is present. Update
the logic around ApplyEncryptionToggle, WriteReplacementUploadAsync, and the
replacement path so that when file != null, only metadata changes (entity.Key
and entity.Encrypted) are applied before the new bytes are written. Keep the
in-place encrypt/decrypt work only for the no-replacement case, and ensure the
replacement upload already handles writing the file in the requested encryption
state.
- Around line 76-77: Clamp both bounds in CmsFileService pagination: the current
page/perPage normalization only enforces a minimum in the paging logic, so
perPage can still grow without limit. Update the relevant method in
CmsFileService to cap perPage at a safe maximum before it is used for Skip/Take,
while keeping the existing lower-bound guard and preserving the page
normalization behavior.
- Around line 235-297: The overwrite path in CreateFileAsync does not clean up
when ReplaceInPlace throws after BackupManagedFile succeeds, leaving a stale
backup and inconsistent disk state. Update the overwrite branch in
CmsFileService.CreateFileAsync to wrap ReplaceInPlace in error handling, and on
failure call ReconcileStoreAfterFailedCreate(targetPath, overwriteBackup) before
rethrowing so the backup/original is restored or removed consistently.

In `@web/Areas/CMS/Services/CmsFileStorageService.cs`:
- Around line 136-154: FileNameInUse is being called repeatedly from
GetUniqueFileName, causing up to 1000 synchronous, non-sargable database checks
per upload. Refactor the uniqueness check so GetUniqueFileName loads the
existing file names for the target folder once, combines that with the disk
listing, and then checks candidates in memory across the suffix loop. Keep
FileNameInUse/CmsFileStorageService as the place to locate the current logic,
but avoid per-candidate _context.Files.Any calls and the EndsWith-based DB scan.

---

Outside diff comments:
In `@web/Areas/CMS/Data/CMS.cs`:
- Around line 398-424: The DownloadZip audit path is doing a synchronous
SaveChanges for every file through AuditFileAccess, causing one database
round-trip per guid. Update the CMS.DownloadZip flow and the AuditFileAccess
usage so the loop only collects FileAudit entries for both allowed and denied
files, then persist them once after the loop with AddRange and a single
SaveChanges call. Keep the existing audit action names and use the current
_viperContext, CheckFilePermission, and AuditFileAccess flow to locate the
batching change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5fbfb8bc-5434-4e95-94af-4ed1a1343881

📥 Commits

Reviewing files that changed from the base of the PR and between 3eaecff and 65f6ba5.

📒 Files selected for processing (49)
  • test/CMS/CMSContentPermissionTests.cs
  • test/CMS/CMSFilesControllerTests.cs
  • test/CMS/CMSUserPhotoControllerTests.cs
  • test/CMS/CmsDownloadRateLimitingTests.cs
  • test/CMS/CmsFileCryptoTests.cs
  • test/CMS/CmsFileImportServiceTests.cs
  • test/CMS/CmsFileResponseTests.cs
  • test/CMS/CmsFileServiceTests.cs
  • test/CMS/CmsFileStorageServiceTests.cs
  • test/CMS/CmsTrashPurgeScheduledJobTests.cs
  • test/CMS/CmsUserPhotoServiceTests.cs
  • test/CMS/MaxLengthEachAttributeTests.cs
  • test/CMS/SafeUrlAttributeTests.cs
  • test/Classes/Utilities/DateRangeHelperTests.cs
  • test/Services/HtmlSanitizerServiceTests.cs
  • web/Areas/CMS/Constants/CmsFileNaming.cs
  • web/Areas/CMS/Constants/CmsFileTypes.cs
  • web/Areas/CMS/Constants/CmsPermissions.cs
  • web/Areas/CMS/Constants/CmsTrash.cs
  • web/Areas/CMS/Controllers/CMSController.cs
  • web/Areas/CMS/Controllers/CMSFilesController.cs
  • web/Areas/CMS/Controllers/CMSOptionsController.cs
  • web/Areas/CMS/Controllers/CMSUserPhotoController.cs
  • web/Areas/CMS/Data/CMS.cs
  • web/Areas/CMS/Jobs/CmsTrashPurgeScheduledJob.cs
  • web/Areas/CMS/Models/CMSFile.cs
  • web/Areas/CMS/Models/CmsFileMapper.cs
  • web/Areas/CMS/Models/DTOs/CmsFileDto.cs
  • web/Areas/CMS/Models/DTOs/CmsFileImportDtos.cs
  • web/Areas/CMS/Models/DTOs/CmsFileRequests.cs
  • web/Areas/CMS/Models/DTOs/CreateLinkDto.cs
  • web/Areas/CMS/Services/CmsConcurrencyException.cs
  • web/Areas/CMS/Services/CmsDownloadRateLimiting.cs
  • web/Areas/CMS/Services/CmsFileAuditService.cs
  • web/Areas/CMS/Services/CmsFileCrypto.cs
  • web/Areas/CMS/Services/CmsFileEncryptionService.cs
  • web/Areas/CMS/Services/CmsFileImportService.cs
  • web/Areas/CMS/Services/CmsFileResponse.cs
  • web/Areas/CMS/Services/CmsFileService.cs
  • web/Areas/CMS/Services/CmsFileStorageService.cs
  • web/Areas/CMS/Services/CmsUserPhotoService.cs
  • web/Areas/CMS/Validation/MaxLengthEachAttribute.cs
  • web/Areas/CMS/Validation/SafeUrlAttribute.cs
  • web/Classes/Utilities/DateRangeHelper.cs
  • web/Program.cs
  • web/Services/HtmlSanitizerService.cs
  • web/Services/IHtmlSanitizerService.cs
  • web/Viper.csproj
  • web/appsettings.json

Comment thread test/Services/HtmlSanitizerServiceTests.cs
Comment thread web/Areas/CMS/Constants/CmsFileTypes.cs
Comment thread web/Areas/CMS/Controllers/CMSFilesController.cs
Comment thread web/Areas/CMS/Controllers/CMSOptionsController.cs
Comment thread web/Areas/CMS/Controllers/CMSOptionsController.cs
Comment thread web/Areas/CMS/Services/CmsFileImportService.cs
Comment thread web/Areas/CMS/Services/CmsFileService.cs Outdated
Comment thread web/Areas/CMS/Services/CmsFileService.cs
Comment thread web/Areas/CMS/Services/CmsFileService.cs Outdated
Comment thread web/Areas/CMS/Services/CmsFileStorageService.cs

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 49 out of 49 changed files in this pull request and generated 3 comments.

Comment thread web/Areas/CMS/Data/CMS.cs Outdated
Comment thread web/Areas/CMS/Services/CmsFileStorageService.cs
Comment thread web/Areas/CMS/Services/CmsFileImportService.cs

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 50 out of 50 changed files in this pull request and generated 2 comments.

Comment thread web/Areas/CMS/Services/CmsFileService.cs
Comment thread web/Areas/CMS/Services/CmsFileService.cs

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 50 out of 50 changed files in this pull request and generated 2 comments.

Comment thread web/Areas/CMS/Services/CmsFileService.cs Outdated
Comment thread web/Areas/CMS/Models/CmsFileMapper.cs Outdated
@rlorenzo rlorenzo requested a review from Copilot July 3, 2026 04:53
@rlorenzo

rlorenzo commented Jul 3, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 52 out of 52 changed files in this pull request and generated 1 comment.

Comment thread web/Areas/CMS/Services/CmsFileImportService.cs

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 52 out of 52 changed files in this pull request and generated 3 comments.

Comment thread web/Areas/CMS/Validation/MaxLengthEachAttribute.cs
Comment thread web/Areas/CMS/Services/CmsFileEncryptionService.cs
Comment thread web/Areas/CMS/Services/CmsFileStorageService.cs Outdated
Comment thread web/Areas/CMS/Services/CmsFileStorageService.cs Fixed
Comment thread web/Areas/CMS/Services/CmsFileStorageService.cs Fixed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 52 out of 52 changed files in this pull request and generated no new comments.

Comment thread web/Areas/CMS/Services/CmsFileStorageService.cs Fixed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 52 out of 52 changed files in this pull request and generated 4 comments.

Comment thread web/Areas/CMS/Services/CmsFileStorageService.cs Outdated
Comment thread web/Areas/CMS/Controllers/CMSFilesController.cs
Comment thread web/Areas/CMS/Controllers/CMSFilesController.cs
Comment thread web/Areas/CMS/Controllers/CMSFilesController.cs
Comment thread web/Areas/CMS/Services/CmsFileStorageService.cs Fixed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 52 out of 52 changed files in this pull request and generated no new comments.

@rlorenzo

rlorenzo commented Jul 3, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
web/Areas/CMS/Services/CmsFileImportService.cs (2)

99-130: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

.AnyAsync() per line issues up to MaxBatchItems (500) sequential queries. The f.FriendlyName == friendlyName existence check runs inside the foreach. Since preview is read-only, preload the folder's existing friendly names once and probe in memory alongside plannedNames.

As per coding guidelines: "Avoid nesting .FirstOrDefault(), .Single(), or .Any() inside a foreach or .Select() over another list; materialize once with .ToDictionary() or .ToLookup() and use TryGetValue instead."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/Areas/CMS/Services/CmsFileImportService.cs` around lines 99 - 130, The
preview loop in CmsFileImportService currently runs an _context.Files.AnyAsync()
query for every FilePaths entry, causing up to MaxBatchItems sequential database
hits. Move the existing friendly-name lookup out of the foreach by preloading
the folder’s current friendly names once, then check each generated friendlyName
in memory together with plannedNames. Keep the existing ResolveSource,
plannedNames, and friendlyName generation flow, but replace the per-item
AnyAsync probe with a cached lookup (for example a hash set or lookup built
before the loop) and preserve the same duplicate/existing-file messaging.

Source: Coding guidelines


303-322: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Catch FormatException/CryptographicException here too. GenerateKeyForDb() can throw on a bad master key, and this block currently lets that escape and abort the whole batch instead of failing just the current file. Mirror the catch set used by EncryptSingleFileAsync.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/Areas/CMS/Services/CmsFileImportService.cs` around lines 303 - 322, The
exception handling in CmsFileImportService should match EncryptSingleFileAsync
so a bad master key does not abort the whole batch. In the current catch block
for the file import flow, add handling for FormatException and
CryptographicException alongside the existing IOException,
UnauthorizedAccessException, InvalidOperationException, and ArgumentException
cases. Keep the behavior consistent by setting result.Message from the caught
exception and returning result so only the current file fails.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@web/Areas/CMS/Services/CmsFileImportService.cs`:
- Around line 99-130: The preview loop in CmsFileImportService currently runs an
_context.Files.AnyAsync() query for every FilePaths entry, causing up to
MaxBatchItems sequential database hits. Move the existing friendly-name lookup
out of the foreach by preloading the folder’s current friendly names once, then
check each generated friendlyName in memory together with plannedNames. Keep the
existing ResolveSource, plannedNames, and friendlyName generation flow, but
replace the per-item AnyAsync probe with a cached lookup (for example a hash set
or lookup built before the loop) and preserve the same duplicate/existing-file
messaging.
- Around line 303-322: The exception handling in CmsFileImportService should
match EncryptSingleFileAsync so a bad master key does not abort the whole batch.
In the current catch block for the file import flow, add handling for
FormatException and CryptographicException alongside the existing IOException,
UnauthorizedAccessException, InvalidOperationException, and ArgumentException
cases. Keep the behavior consistent by setting result.Message from the caught
exception and returning result so only the current file fails.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 607c4e1a-0d23-4b26-923b-c773ad7f3c35

📥 Commits

Reviewing files that changed from the base of the PR and between cc0707f and 2e6fcd7.

📒 Files selected for processing (6)
  • test/CMS/MaxLengthEachAttributeTests.cs
  • web/Areas/CMS/Controllers/CMSFilesController.cs
  • web/Areas/CMS/Services/CmsFileEncryptionService.cs
  • web/Areas/CMS/Services/CmsFileImportService.cs
  • web/Areas/CMS/Services/CmsFileStorageService.cs
  • web/Areas/CMS/Validation/MaxLengthEachAttribute.cs

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 53 out of 53 changed files in this pull request and generated 1 comment.

Comment thread web/Areas/CMS/Services/CmsDownloadRateLimiting.cs
@rlorenzo

rlorenzo commented Jul 3, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@rlorenzo rlorenzo changed the title VPR-59 [1/3] CMS migration: files, photos, import, rate limiting (backend) VPR-59 [1/4] CMS migration: files, photos, import, rate limiting (backend) Jul 6, 2026
rlorenzo added 2 commits July 10, 2026 01:23
Slice 1/3 of the restacked CMS migration (built from the verified
branch tip, so every slice carries the OS-independent path handling
and the review fixes). Adds the files-domain backend: storage,
CF-compatible crypto, audit, import, trash purge job, user photos,
options lookups, the rebuilt /CMS/Files download path, and download
rate limiting, with their unit tests. The content/nav backend and the
management SPA follow in slices 2-3.
Its strict JSON parser cannot read appsettings*.json (ASP.NET parses
those as JSON-with-comments by design) and warned on every review
touching them; CI already covers its lint surface.
@rlorenzo rlorenzo force-pushed the VPR-59-cms-r1-files-backend branch from 5b15989 to 8b2cee3 Compare July 10, 2026 08:28
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.

4 participants