Repository: ckan/ckan

Issue:
Purging deleted packages does not delete associated filestore/datastore resources
**CKAN version**
2.9

**Describe the bug**
After deleting and purging dataset packages, the associated datastore/filestore resources are orphaned and not deleted.

**Steps to reproduce**

1. Delete datasets
2. Go to sysadmin/trash
3. Select "Purge All"
4. Check datastore and filestore.  Associated resources are still there.

**Expected behavior**
Dataset resources in the filestore and the datastore are deleted when the associated package resources are purged.

**Additional details**
This is related to #4705 and a lot of work has been done on PRs #4867 and #4905, but neither have been merged.



Discussion:

Resolving patch:
diff --git a/ckanext/datastore/cli.py b/ckanext/datastore/cli.py
--- a/ckanext/datastore/cli.py
+++ b/ckanext/datastore/cli.py
@@ -7,6 +7,7 @@
 
 from ckan.model import parse_db_config
 from ckan.common import config
+import ckan.logic as logic
 
 import ckanext.datastore as datastore_module
 from ckanext.datastore.backend.postgres import identifier
@@ -111,3 +112,69 @@ def _parse_db_config(config_key=u'sqlalchemy.url'):
         )
         raise click.Abort()
     return db_config
+
+
+@datastore.command(
+    u'purge',
+    short_help=u'purge orphaned resources from the datastore.'
+)
+def purge():
+    u'''Purge orphaned resources from the datastore using the datastore_delete
+    action, which drops tables when called without filters.'''
+
+    site_user = logic.get_action(u'get_site_user')({u'ignore_auth': True}, {})
+    context = {u'user': site_user[u'name']}
+
+    result = logic.get_action(u'datastore_search')(
+        context,
+        {u'resource_id': u'_table_metadata'}
+    )
+
+    resource_id_list = []
+    for record in result[u'records']:
+        try:
+            # ignore 'alias' records (views) as they are automatically
+            # deleted when the parent resource table is dropped
+            if record[u'alias_of']:
+                continue
+
+            # we need to do this to trigger resource_show auth function
+            site_user = logic.get_action(u'get_site_user')(
+                {u'ignore_auth': True}, {})
+            context = {u'user': site_user[u'name']}
+
+            logic.get_action(u'resource_show')(
+                context,
+                {u'id': record[u'name']}
+            )
+        except logic.NotFound:
+            resource_id_list.append(record[u'name'])
+            click.echo(u"Resource '%s' orphaned - queued for drop" %
+                       record[u'name'])
+        except KeyError:
+            continue
+
+    orphaned_table_count = len(resource_id_list)
+    click.echo(u'%d orphaned tables found.' % orphaned_table_count)
+
+    if not orphaned_table_count:
+        return
+
+    click.confirm(u'Proceed with purge?', abort=True)
+
+    # Drop the orphaned datastore tables. When datastore_delete is called
+    # without filters, it does a drop table cascade
+    drop_count = 0
+    for resource_id in resource_id_list:
+        logic.get_action(u'datastore_delete')(
+            context,
+            {u'resource_id': resource_id, u'force': True}
+        )
+        click.echo(u"Table '%s' dropped)" % resource_id)
+        drop_count += 1
+
+    click.echo(u'Dropped %s tables' % drop_count)
+
+
+def get_commands():
+    return (set_permissions, dump, purge)


Test patch: