From 6c2f90559e47d1d1813db9b6c1ace1b6acca0001 Mon Sep 17 00:00:00 2001 From: Adam Rauch Date: Sat, 21 Mar 2026 20:07:56 -0700 Subject: [PATCH 1/2] Delete orphaned attachments --- .../api/attachments/AttachmentService.java | 4 +- .../org/labkey/api/data/ContainerManager.java | 2 +- core/module.properties | 2 +- .../postgresql/core-26.002-26.003.sql | 1 + .../sqlserver/core-26.002-26.003.sql | 1 + core/src/org/labkey/core/CoreUpgradeCode.java | 19 +++++++ .../attachment/AttachmentServiceImpl.java | 51 ++++++++++++++++++- 7 files changed, 76 insertions(+), 4 deletions(-) create mode 100644 core/resources/schemas/dbscripts/postgresql/core-26.002-26.003.sql create mode 100644 core/resources/schemas/dbscripts/sqlserver/core-26.002-26.003.sql diff --git a/api/src/org/labkey/api/attachments/AttachmentService.java b/api/src/org/labkey/api/attachments/AttachmentService.java index 5b10805aa3d..39e0eca2425 100644 --- a/api/src/org/labkey/api/attachments/AttachmentService.java +++ b/api/src/org/labkey/api/attachments/AttachmentService.java @@ -140,7 +140,9 @@ static AttachmentService get() HttpView getFindAttachmentParentsView(); - void detectOrphans(); + void logOrphanedAttachments(); + + void deleteOrphanedAttachments(); class DuplicateFilenameException extends IOException implements SkipMothershipLogging { diff --git a/api/src/org/labkey/api/data/ContainerManager.java b/api/src/org/labkey/api/data/ContainerManager.java index 74507dfb6ac..fd459df101d 100644 --- a/api/src/org/labkey/api/data/ContainerManager.java +++ b/api/src/org/labkey/api/data/ContainerManager.java @@ -1944,7 +1944,7 @@ private static boolean delete(final Container c, User user, @Nullable String com setContainerTabDeleted(c.getParent(), c.getName(), c.getParent().getFolderType().getName()); } - AttachmentService.get().detectOrphans(); + AttachmentService.get().logOrphanedAttachments(); fireDeleteContainer(c, user); diff --git a/core/module.properties b/core/module.properties index 26d5f3f5eb5..90a5223381f 100644 --- a/core/module.properties +++ b/core/module.properties @@ -1,6 +1,6 @@ Name: Core ModuleClass: org.labkey.core.CoreModule -SchemaVersion: 26.002 +SchemaVersion: 26.003 Label: Administration and Essential Services Description: The Core module provides central services such as login, \ security, administration, folder management, user management, \ diff --git a/core/resources/schemas/dbscripts/postgresql/core-26.002-26.003.sql b/core/resources/schemas/dbscripts/postgresql/core-26.002-26.003.sql new file mode 100644 index 00000000000..407b5538278 --- /dev/null +++ b/core/resources/schemas/dbscripts/postgresql/core-26.002-26.003.sql @@ -0,0 +1 @@ +SELECT core.executeJavaUpgradeCode('deleteOrphanedAttachments'); diff --git a/core/resources/schemas/dbscripts/sqlserver/core-26.002-26.003.sql b/core/resources/schemas/dbscripts/sqlserver/core-26.002-26.003.sql new file mode 100644 index 00000000000..23425e25857 --- /dev/null +++ b/core/resources/schemas/dbscripts/sqlserver/core-26.002-26.003.sql @@ -0,0 +1 @@ +EXEC core.executeJavaUpgradeCode 'deleteOrphanedAttachments'; diff --git a/core/src/org/labkey/core/CoreUpgradeCode.java b/core/src/org/labkey/core/CoreUpgradeCode.java index f12197cc99a..eb5d1040372 100644 --- a/core/src/org/labkey/core/CoreUpgradeCode.java +++ b/core/src/org/labkey/core/CoreUpgradeCode.java @@ -71,6 +71,7 @@ public static void migrateAllowedExternalConnectionHosts(ModuleContext context) if (context.isNewInstall()) return; + // TODO: Remove getExternalSourceHosts() method when this upgrade code is deleted List hosts = AppProps.getInstance().getExternalSourceHosts(); List allowedHosts = hosts.stream() .map(host -> new AllowedHost(Directive.Connection, host)) @@ -106,4 +107,22 @@ public static void populateAttachmentParentTypeColumn(ModuleContext context) new SqlExecutor(CoreSchema.getInstance().getSchema()).execute(updateSql); } } + + /** + * Called from core-26.002-26.003.sql + */ + @DeferredUpgrade // Need to execute this after AttachmentTypes are registered + @SuppressWarnings("unused") + public static void deleteOrphanedAttachments(ModuleContext context) + { + if (context.isNewInstall()) + return; + + AttachmentService svc = AttachmentService.get(); + + if (svc != null) + { + svc.deleteOrphanedAttachments(); + } + } } \ No newline at end of file diff --git a/core/src/org/labkey/core/attachment/AttachmentServiceImpl.java b/core/src/org/labkey/core/attachment/AttachmentServiceImpl.java index ed266908800..2e7dfb70f71 100644 --- a/core/src/org/labkey/core/attachment/AttachmentServiceImpl.java +++ b/core/src/org/labkey/core/attachment/AttachmentServiceImpl.java @@ -45,6 +45,7 @@ import org.labkey.api.data.ColumnRenderProperties; import org.labkey.api.data.CompareType; import org.labkey.api.data.Container; +import org.labkey.api.data.ContainerFilter; import org.labkey.api.data.ContainerFilter.AllFolders; import org.labkey.api.data.ContainerManager; import org.labkey.api.data.CoreSchema; @@ -1098,7 +1099,7 @@ public int available() private record Orphan(String documentName, String parentType){} @Override - public void detectOrphans() + public void logOrphanedAttachments() { // Log orphaned attachments in this server, but in dev mode only, since this is for our testing. Also, we // don't yet offer a way to delete orphaned attachments via the UI, so it's not helpful to inform admins. @@ -1135,6 +1136,54 @@ public void detectOrphans() } } + record OrphanedAttachment(String container, String parent, String documentName) + { + AttachmentParent getAttachmentParent() + { + return new AttachmentParent() + { + @Override + public String getEntityId() + { + return parent; + } + + @Override + public String getContainerId() + { + return container; + } + + @Override + public @NotNull AttachmentParentType getAttachmentParentType() + { + return AttachmentParentType.UNKNOWN; + } + }; + } + } + + @Override + public void deleteOrphanedAttachments() + { + User user = ElevatedUser.getElevatedUser(User.getSearchUser(), TroubleshooterRole.class); + UserSchema core = DefaultSchema.get(user, ContainerManager.getRoot()).getUserSchema(CoreQuerySchema.NAME); + if (core != null) + { + // Use "unsafe everything" container filter because it's possible that orphaned attachments have a container + // that no longer exists. + TableInfo documents = core.getTable(CoreQuerySchema.DOCUMENTS_TABLE_NAME, ContainerFilter.getUnsafeEverythingFilter()); + if (null != documents) + { + SimpleFilter filter = new SimpleFilter(FieldKey.fromParts("Orphaned"), true); + new TableSelector(documents, new CsvSet("Container, Parent, DocumentName"), filter, null).forEach(OrphanedAttachment.class, orphan -> { + LOG.info("Deleting orphaned attachment: {}", orphan); + deleteAttachment(orphan.getAttachmentParent(), orphan.documentName(), user); + }); + } + } + } + private CoreSchema coreTables() { return CoreSchema.getInstance(); From f47cbae424c03a4ede2cdcd9eb4b16f030261c36 Mon Sep 17 00:00:00 2001 From: Adam Rauch Date: Sun, 22 Mar 2026 12:23:35 -0700 Subject: [PATCH 2/2] Select and attempt to resolve ParentType so it gets included in the audit log delete event --- .../org/labkey/core/attachment/AttachmentServiceImpl.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/core/src/org/labkey/core/attachment/AttachmentServiceImpl.java b/core/src/org/labkey/core/attachment/AttachmentServiceImpl.java index 2e7dfb70f71..7ed40c5743c 100644 --- a/core/src/org/labkey/core/attachment/AttachmentServiceImpl.java +++ b/core/src/org/labkey/core/attachment/AttachmentServiceImpl.java @@ -1136,7 +1136,7 @@ public void logOrphanedAttachments() } } - record OrphanedAttachment(String container, String parent, String documentName) + record OrphanedAttachment(String container, String parent, String parentType, String documentName) { AttachmentParent getAttachmentParent() { @@ -1157,7 +1157,9 @@ public String getContainerId() @Override public @NotNull AttachmentParentType getAttachmentParentType() { - return AttachmentParentType.UNKNOWN; + // Attempt to resolve the parent type. This will get written to the audit log. + AttachmentParentType type = ATTACHMENT_TYPE_MAP.get(parentType()); + return type != null ? type : AttachmentParentType.UNKNOWN; } }; } @@ -1176,7 +1178,7 @@ public void deleteOrphanedAttachments() if (null != documents) { SimpleFilter filter = new SimpleFilter(FieldKey.fromParts("Orphaned"), true); - new TableSelector(documents, new CsvSet("Container, Parent, DocumentName"), filter, null).forEach(OrphanedAttachment.class, orphan -> { + new TableSelector(documents, new CsvSet("Container, Parent, ParentType, DocumentName"), filter, null).forEach(OrphanedAttachment.class, orphan -> { LOG.info("Deleting orphaned attachment: {}", orphan); deleteAttachment(orphan.getAttachmentParent(), orphan.documentName(), user); });