Skip to content

Add endpoint for managing recycle bin #1920

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions news/1919.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Implement RESTAPI endpoint for managing recycle bin @rohnsha0
1 change: 1 addition & 0 deletions src/plone/restapi/services/configure.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
<include package=".querysources" />
<include package=".querystring" />
<include package=".querystringsearch" />
<include package=".recyclebin" />
<include package=".registry" />
<include package=".relations" />
<include package=".roles" />
Expand Down
1 change: 1 addition & 0 deletions src/plone/restapi/services/recyclebin/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Empty init file to make the directory a Python package
41 changes: 41 additions & 0 deletions src/plone/restapi/services/recyclebin/configure.zcml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<configure
xmlns="http://namespaces.zope.org/zope"
xmlns:plone="http://namespaces.plone.org/plone"
xmlns:zcml="http://namespaces.zope.org/zcml"
>

<include package="plone.restapi" />

<plone:service
method="GET"
factory=".get.RecycleBinGet"
for="Products.CMFPlone.interfaces.IPloneSiteRoot"
permission="cmf.ManagePortal"
name="@recyclebin"
/>

<plone:service
method="GET"
factory=".get.RecycleBinGet"
for="Products.CMFCore.interfaces.IFolderish"
permission="cmf.ManagePortal"
name="@recyclebin"
/>

<plone:service
method="POST"
factory=".restore.RecycleBinRestore"
for="Products.CMFPlone.interfaces.IPloneSiteRoot"
permission="cmf.ManagePortal"
name="@recyclebin-restore"
/>

<plone:service
method="POST"
factory=".purge.RecycleBinPurge"
for="Products.CMFPlone.interfaces.IPloneSiteRoot"
permission="cmf.ManagePortal"
name="@recyclebin-purge"
/>

</configure>
147 changes: 147 additions & 0 deletions src/plone/restapi/services/recyclebin/documentation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
# Recycle Bin REST API

The Recycle Bin REST API provides endpoints to interact with the Plone Recycle Bin functionality.

## List recycle bin contents

To list all items in the recycle bin, send a GET request to the `@recyclebin` endpoint:

```http
GET /@recyclebin HTTP/1.1
Accept: application/json
```

Response:

```json
{
"@id": "http://localhost:8080/Plone/@recyclebin",
"items": [
{
"@id": "http://localhost:8080/Plone/@recyclebin/6d6d626f-8c85-4f22-8747-adb979bbe3b1",
"id": "document-1",
"title": "My Document",
"type": "Document",
"path": "/Plone/folder/document-1",
"parent_path": "/Plone/folder",
"deletion_date": "2025-04-27T10:30:45.123456",
"size": 1024,
"recycle_id": "6d6d626f-8c85-4f22-8747-adb979bbe3b1",
"actions": {
"restore": "http://localhost:8080/Plone/@recyclebin-restore",
"purge": "http://localhost:8080/Plone/@recyclebin-purge"
}
}
],
"items_total": 1
}
```

## Restore an item from the recycle bin

To restore an item from the recycle bin, send a POST request to the `@recyclebin-restore` endpoint:

```http
POST /@recyclebin-restore HTTP/1.1
Accept: application/json
Content-Type: application/json

{
"item_id": "6d6d626f-8c85-4f22-8747-adb979bbe3b1"
}
```

You can optionally specify a target path to restore to:

```json
{
"item_id": "6d6d626f-8c85-4f22-8747-adb979bbe3b1",
"target_path": "/Plone/another-folder"
}
```

Response:

```json
{
"status": "success",
"message": "Item document-1 restored successfully",
"restored_item": {
"@id": "http://localhost:8080/Plone/document-1",
"id": "document-1",
"title": "My Document",
"type": "Document"
}
}
```

## Purge an item from the recycle bin

To permanently delete an item from the recycle bin, send a POST request to the `@recyclebin-purge` endpoint:

```http
POST /@recyclebin-purge HTTP/1.1
Accept: application/json
Content-Type: application/json

{
"item_id": "6d6d626f-8c85-4f22-8747-adb979bbe3b1"
}
```

Response:

```json
{
"status": "success",
"message": "Item document-1 purged successfully"
}
```

## Purge all items from the recycle bin

To purge all items from the recycle bin:

```http
POST /@recyclebin-purge HTTP/1.1
Accept: application/json
Content-Type: application/json

{
"purge_all": true
}
```

Response:

```json
{
"status": "success",
"purged_count": 5,
"message": "Purged 5 items from recycle bin"
}
```

## Purge expired items from the recycle bin

To purge only expired items (based on the retention period):

```http
POST /@recyclebin-purge HTTP/1.1
Accept: application/json
Content-Type: application/json

{
"purge_expired": true
}
```

Response:

```json
{
"status": "success",
"purged_count": 2,
"message": "Purged 2 expired items from recycle bin"
}
```
50 changes: 50 additions & 0 deletions src/plone/restapi/services/recyclebin/get.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from plone.restapi.services import Service
from Products.CMFPlone.interfaces.recyclebin import IRecycleBin
from zope.component import getUtility


class RecycleBinGet(Service):
"""GET /@recyclebin - List items in the recycle bin"""

def reply(self):
recycle_bin = getUtility(IRecycleBin)

# Check if recycle bin is enabled
if not recycle_bin.is_enabled():
self.request.response.setStatus(404)
return {
"error": {
"type": "NotFound",
"message": "Recycle Bin is disabled",
}
}

# Get all items from recycle bin
items = recycle_bin.get_items()

# Format items for response
results = []
for item in items:
results.append(
{
"@id": f"{self.context.absolute_url()}/@recyclebin/{item['recycle_id']}",
"id": item["id"],
"title": item["title"],
"type": item["type"],
"path": item["path"],
"parent_path": item["parent_path"],
"deletion_date": item["deletion_date"].isoformat(),
"size": item["size"],
"recycle_id": item["recycle_id"],
"actions": {
"restore": f"{self.context.absolute_url()}/@recyclebin-restore",
"purge": f"{self.context.absolute_url()}/@recyclebin-purge",
},
}
)

return {
"@id": f"{self.context.absolute_url()}/@recyclebin",
"items": results,
"items_total": len(results),
}
93 changes: 93 additions & 0 deletions src/plone/restapi/services/recyclebin/purge.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
from plone.restapi.deserializer import json_body
from plone.restapi.services import Service
from Products.CMFPlone.interfaces.recyclebin import IRecycleBin
from zope.component import getUtility
from zope.interface import alsoProvides

import plone.protect.interfaces


class RecycleBinPurge(Service):
"""POST /@recyclebin-purge - Permanently delete an item from the recycle bin"""

def reply(self):
# Disable CSRF protection for this request
alsoProvides(self.request, plone.protect.interfaces.IDisableCSRFProtection)

data = json_body(self.request)
item_id = data.get("item_id", None)
purge_all = data.get("purge_all", False)
purge_expired = data.get("purge_expired", False)

recycle_bin = getUtility(IRecycleBin)

# Check if recycle bin is enabled
if not recycle_bin.is_enabled():
self.request.response.setStatus(404)
return {
"error": {
"type": "NotFound",
"message": "Recycle Bin is disabled",
}
}

# Handle purging all items
if purge_all:
purged_count = 0
for item in recycle_bin.get_items():
if recycle_bin.purge_item(item["recycle_id"]):
purged_count += 1

return {
"status": "success",
"purged_count": purged_count,
"message": f"Purged {purged_count} items from recycle bin",
}

# Handle purging expired items
if purge_expired:
purged_count = recycle_bin.purge_expired_items()

return {
"status": "success",
"purged_count": purged_count,
"message": f"Purged {purged_count} expired items from recycle bin",
}

# Handle purging a specific item
if not item_id:
self.request.response.setStatus(400)
return {
"error": {
"type": "BadRequest",
"message": "Missing required parameter: item_id or purge_all or purge_expired",
}
}

# Get the item to purge
item_data = recycle_bin.get_item(item_id)
if not item_data:
self.request.response.setStatus(404)
return {
"error": {
"type": "NotFound",
"message": f"Item with ID {item_id} not found in recycle bin",
}
}

# Purge the item
success = recycle_bin.purge_item(item_id)

if not success:
self.request.response.setStatus(500)
return {
"error": {
"type": "InternalServerError",
"message": "Failed to purge item",
}
}

return {
"status": "success",
"message": f"Item {item_data['id']} purged successfully",
}
Loading
Loading