Files
admin/.wiki/concepts/cms-admin-assets-root-folder-seed.md

5.1 KiB
Raw Blame History

title, type, tags, sources, updated
title type tags sources updated
Admin assets crash — missing root AssetsFolder seed concept
cms
admin
data-seed
mssql
gotcha
null-check
../sources/iis-host-migration-2026-05-19.md
2026-05-19

CMS admin assets crash — root AssetsFolder seed

AssetsJsonViewModelBuilder.cs:22 (admin /admin/assets/.../getList) падает NullReferenceException на любом site, где root row Folders (Discriminator='AssetsFolder', LoweredPath='/', OwnerId=SiteId) отсутствует. Root создаётся lazy (при first asset upload через admin), и sites которые никогда не использовали admin assets UI — без root.

Связано: cms-server-port-leak-fix (нашли при verification после port-leak fix), recovery-architecture-snapshot (был open issue #8).

Симптом

GET /admin/assets/<siteId>/getList?path=
→ 500 NullReferenceException

[NullReferenceException]
  MoreThenCms.Admin.ViewModels.Builders.AssetsJsonViewModelBuilder.Build(...)
    AssetsJsonViewModelBuilder.cs:22
  MoreThenCms.Web.Admin.Controllers.AssetsController.GetList(Guid ownerId, String path)
    AssetsController.cs:46

В AssetsController.GetList (line 46):

var model = _assetsViewModelBuilder.Build(
    _assetsFoldersService.GetFolderByPath(ExecutionContext, ownerId, path), ...);

GetFolderByPath (MoreThenCms\Assets\Services\AssetsFoldersService.cs:47-54) делает repo.Find(p => p.LoweredPath == path && p.OwnerId == ownerId) — возвращает null если row нет. Build дальше делает new JValue(model.ParentPath) без null-check → crash.

Frontend код (AssetsAppFunc.cs:66-86) делает proper null-check → HTTP 404. Только admin view-model builder упустил.

Кого затрагивает (snapshot 2026-05-19)

После recovery: 15 sites без root (из ~50). Sites где admin assets когда-либо открывался → root есть. Остальные:

  • emspb.ru, pilorama98.ru, labtools.pro, kupimknigi.spb.ru, sestech.ru, aquamax.spb.ru, artmone.pro, priemka-kvartiry.ru, profund.spb.ru, ics-artmaterials.com, _voda-indigo.ru
  • 4 sites с NULL PrimaryDomain (legacy/test data)

Проверочный запрос:

SELECT s.SiteId, s.PrimaryDomain,
       CASE WHEN EXISTS (SELECT 1 FROM Folders f
                         WHERE f.OwnerId = s.SiteId
                           AND f.LoweredPath = '/'
                           AND f.Discriminator = 'AssetsFolder')
            THEN 'OK' ELSE 'NO ROOT' END AS RootStatus
FROM Sites s
ORDER BY RootStatus, s.PrimaryDomain;

Fix (применён 2026-05-19)

DB seed — один INSERT для всех missing roots, идемпотентный (WHERE NOT EXISTS):

DECLARE @CreatedById uniqueidentifier = 'E4CC416B-5D13-4757-8EFE-03CBEA15B18C'; -- existing admin user
DECLARE @Inserted TABLE (FolderId uniqueidentifier, OwnerId uniqueidentifier);

INSERT INTO Folders (FolderId, OwnerId, Path, LoweredPath,
                     DateCreated, UtcDateCreated, CreatedById, Discriminator)
OUTPUT INSERTED.FolderId, INSERTED.OwnerId INTO @Inserted
SELECT NEWID(), s.SiteId, '/', '/',
       SYSDATETIME(), SYSUTCDATETIME(), @CreatedById, 'AssetsFolder'
FROM Sites s
WHERE NOT EXISTS (SELECT 1 FROM Folders f
                  WHERE f.OwnerId = s.SiteId
                    AND f.LoweredPath = '/'
                    AND f.Discriminator = 'AssetsFolder');

SELECT FolderId, OwnerId FROM @Inserted;  -- save for atomic revert

15 rows вставлены. Inserted FolderId/OwnerId сохранены в .tasks/cms-admin-assets-root-folders-seed.inserted-rows.txt для atomic revert (DELETE FROM Folders WHERE FolderId IN (...)).

После: /admin/assets/<siteId>/getList?path= возвращает HTTP 302 → /login для unauth (normal auth path), вместо 500. Authorized users видят empty assets list.

Долгосрочный TODO (не сделано — нет рабочего build env)

AssetsJsonViewModelBuilder.Build должна null-guard'иться — возвращать empty JObject ({parentPath:"", path:"/", folders:[], files:[]}) когда model null. Это defensive code, плюс auto-create root в GetFolderByPath для root path (как обычно делают админ-репозитории).

Требует recompile MoreThenCms.Admin.dll — отложено до восстановления полного build pipeline (gitea/build/registry — см. vds-kzntsv-bootstrap task).

Возможно затрагивает ещё (open для следующей сессии)

Folders table — polymorphic: AssetsFolder, ImagesFolder (OwnerId=ThemeId), ScriptsFolder (ThemeId), StylesheetsFolder (ThemeId). Аналогичная null-falling логика может быть в admin views для Themes (Stylesheets/Scripts/Images). Не проверено, не reported user'ом.

Если будет — same pattern: SELECT s.SiteId vs Folders root по соответствующему Discriminator + seed.