-
-
Notifications
You must be signed in to change notification settings - Fork 87
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
base: main
Are you sure you want to change the base?
Changes from 5 commits
cdb3dda
659bdc3
8055417
76293ec
4f1ad73
97aa41f
04016ce
860422c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Implement RESTAPI endpoint for managing recycle bin @rohnsha0 | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
# Empty init file to make the directory a Python package |
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> |
stevepiercy marked this conversation as resolved.
Show resolved
Hide resolved
|
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" | ||
} | ||
``` |
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""" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We'll also want a GET for |
||
|
||
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", | ||
rohnsha0 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} | ||
|
||
# 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", | ||
}, | ||
} | ||
) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should paginate the results using the same batching utility that other services use. This service will also need to take params for the filters needed to support the UI. |
||
|
||
return { | ||
"@id": f"{self.context.absolute_url()}/@recyclebin", | ||
"items": results, | ||
"items_total": len(results), | ||
} |
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""" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should be a DELETE request to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should also support a DELETE on |
||
|
||
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", | ||
rohnsha0 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} | ||
|
||
# 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", | ||
rohnsha0 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} | ||
|
||
# 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", | ||
} |
Uh oh!
There was an error while loading. Please reload this page.