-
-
Notifications
You must be signed in to change notification settings - Fork 89
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 all 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 |
---|---|---|
|
@@ -42,6 +42,7 @@ portrait | |
principals | ||
querystring | ||
querystringsearch | ||
recycle-bin | ||
registry | ||
relations | ||
roles | ||
|
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,147 @@ | ||||||||||||||||||||||||||||||
# Recycle Bin | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
The Recycle Bin REST API provides endpoints to interact with the Plone Recycle Bin functionality. | ||||||||||||||||||||||||||||||
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.
Suggested change
|
||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
## List recycle bin contents | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
To list all items in the recycle bin, send a GET request to the `@recyclebin` endpoint: | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
```http-example | ||||||||||||||||||||||||||||||
GET /@recyclebin HTTP/1.1 | ||||||||||||||||||||||||||||||
Accept: application/json | ||||||||||||||||||||||||||||||
``` | ||||||||||||||||||||||||||||||
Comment on lines
+9
to
+12
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 and all http example requests and responses should follow the pattern of other documentation in this directory. This allows tests to run both on the documentation and on the code. Here's example MyST markup that pulls in files as literal includes. You'll also need to create the included files. plone.restapi/docs/source/endpoints/addons.md Lines 17 to 30 in 70da1de
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. See also the contributing guide at https://6.docs.plone.org/plone.restapi/docs/source/contributing/index.html#generate-documentation-examples 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. Steve is right--look at some of the other services for examples. We're supposed to have tests like the ones in test_documentation.py which actually call the endpoint, then record the request and response so they can be included in the docs. That way we will notice if something changes the response in the future. |
||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
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-example | ||||||||||||||||||||||||||||||
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-example | ||||||||||||||||||||||||||||||
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-example | ||||||||||||||||||||||||||||||
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-example | ||||||||||||||||||||||||||||||
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 @@ | ||
Add 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> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
from plone.base.interfaces.recyclebin import IRecycleBin | ||
from plone.restapi.services import Service | ||
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", | ||
} | ||
} | ||
|
||
# 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.base.interfaces.recyclebin import IRecycleBin | ||
from plone.restapi.deserializer import json_body | ||
from plone.restapi.services import Service | ||
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", | ||
} | ||
} | ||
|
||
# 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, 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", | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Above this page title, insert required html meta stuff. See other files in this directory for examples.