Skip to content
This repository has been archived by the owner on Apr 26, 2024. It is now read-only.

Admin API to delete media for a specific user #10558

Merged
merged 6 commits into from
Aug 11, 2021
Merged
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 changelog.d/10558.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Admin API to delete several media for a specific user.
9 changes: 8 additions & 1 deletion docs/admin_api/media_admin_api.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
- [Delete local media](#delete-local-media)
* [Delete a specific local media](#delete-a-specific-local-media)
* [Delete local media by date or size](#delete-local-media-by-date-or-size)
* [Delete media uploaded by a user](#delete-media-uploaded-by-a-user)
- [Purge Remote Media API](#purge-remote-media-api)

# Querying media
Expand Down Expand Up @@ -47,7 +48,8 @@ The API returns a JSON body like the following:
## List all media uploaded by a user

Listing all media that has been uploaded by a local user can be achieved through
the use of the [List media of a user](user_admin_api.md#list-media-of-a-user)
the use of the
[List media uploaded by a user](user_admin_api.md#list-media-uploaded-by-a-user)
Admin API.

# Quarantine media
Expand Down Expand Up @@ -281,6 +283,11 @@ The following fields are returned in the JSON response body:
* `deleted_media`: an array of strings - List of deleted `media_id`
* `total`: integer - Total number of deleted `media_id`

## Delete media uploaded by a user

You can find details of how to delete multiple media uploaded by a user in
[User Admin API](user_admin_api.md#delete-media-uploaded-by-a-user).

# Purge Remote Media API

The purge remote media API allows server admins to purge old cached remote media.
Expand Down
54 changes: 49 additions & 5 deletions docs/admin_api/user_admin_api.md
Original file line number Diff line number Diff line change
Expand Up @@ -443,8 +443,9 @@ The following fields are returned in the JSON response body:
- `joined_rooms` - An array of `room_id`.
- `total` - Number of rooms.

## User media

## List media of a user
### List media uploaded by a user
Gets a list of all local media that a specific `user_id` has created.
By default, the response is ordered by descending creation date and ascending media ID.
The newest media is on top. You can change the order with parameters
Expand Down Expand Up @@ -543,21 +544,65 @@ The following fields are returned in the JSON response body:

- `media` - An array of objects, each containing information about a media.
Media objects contain the following fields:

- `created_ts` - integer - Timestamp when the content was uploaded in ms.
- `last_access_ts` - integer - Timestamp when the content was last accessed in ms.
- `media_id` - string - The id used to refer to the media.
- `media_length` - integer - Length of the media in bytes.
- `media_type` - string - The MIME-type of the media.
- `quarantined_by` - string - The user ID that initiated the quarantine request
for this media.

- `safe_from_quarantine` - bool - Status if this media is safe from quarantining.
- `upload_name` - string - The name the media was uploaded with.

- `next_token`: integer - Indication for pagination. See above.
- `total` - integer - Total number of media.

### Delete media uploaded by a user

This API deletes the *local* media from the disk of your own server
that a specific `user_id` has created. This includes any local thumbnails.

This API will not affect media that has been uploaded to external
media repositories (e.g https://github.com/turt2live/matrix-media-repo/).

By default, the API deletes media ordered by descending creation date and ascending media ID.
The newest media is deleted first. You can change the order with parameters
`order_by` and `dir`. If no `limit` is set the API deletes `100` files per request.

The API is:

```
DELETE /_synapse/admin/v1/users/<user_id>/media
```

To use it, you will need to authenticate by providing an `access_token` for a
server admin: [Admin API](../usage/administration/admin_api)

A response body like the following is returned:

```json
{
"deleted_media": [
"abcdefghijklmnopqrstuvwx"
],
"total": 1
}
```

The following fields are returned in the JSON response body:

* `deleted_media`: an array of strings - List of deleted `media_id`
* `total`: integer - Total number of deleted `media_id`

**Note**: There is no `next_token`. This is not useful for deleting media, because
after deleting media the remaining media have a new order.

**Parameters**

This API has the same parameters as
[List media uploaded by a user](#list-media-uploaded-by-a-user).
With the parameters you can for example limit the number of files to delete at once or
delete largest/smallest or newest/oldest files first.

## Login as a user

Get an access token that can be used to authenticate as that user. Useful for
Expand Down Expand Up @@ -1012,4 +1057,3 @@ The following parameters should be set in the URL:

- `user_id` - The fully qualified MXID: for example, `@user:server.com`. The user must
be local.

4 changes: 3 additions & 1 deletion synapse/rest/admin/media.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,9 @@ async def on_DELETE(

logging.info("Deleting local media by ID: %s", media_id)

deleted_media, total = await self.media_repository.delete_local_media(media_id)
deleted_media, total = await self.media_repository.delete_local_media_ids(
[media_id]
)
return 200, {"deleted_media": deleted_media, "total": total}


Expand Down
80 changes: 74 additions & 6 deletions synapse/rest/admin/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ async def on_GET(

target_user = UserID.from_string(user_id)
if not self.hs.is_mine(target_user):
raise SynapseError(400, "Can only lookup local users")
raise SynapseError(400, "Can only look up local users")

ret = await self.admin_handler.get_user(target_user)

Expand Down Expand Up @@ -796,7 +796,7 @@ async def on_GET(
await assert_requester_is_admin(self.auth, request)

if not self.is_mine(UserID.from_string(user_id)):
raise SynapseError(400, "Can only lookup local users")
raise SynapseError(400, "Can only look up local users")

if not await self.store.get_user_by_id(user_id):
raise NotFoundError("User not found")
Expand All @@ -811,10 +811,10 @@ async def on_GET(
class UserMediaRestServlet(RestServlet):
"""
Gets information about all uploaded local media for a specific `user_id`.
With DELETE request you can delete all this media.

Example:
http://localhost:8008/_synapse/admin/v1/users/
@user:server/media
http://localhost:8008/_synapse/admin/v1/users/@user:server/media

Args:
The parameters `from` and `limit` are required for pagination.
Expand All @@ -830,6 +830,7 @@ def __init__(self, hs: "HomeServer"):
self.is_mine = hs.is_mine
self.auth = hs.get_auth()
self.store = hs.get_datastore()
self.media_repository = hs.get_media_repository()

async def on_GET(
self, request: SynapseRequest, user_id: str
Expand All @@ -840,7 +841,7 @@ async def on_GET(
await assert_requester_is_admin(self.auth, request)

if not self.is_mine(UserID.from_string(user_id)):
raise SynapseError(400, "Can only lookup local users")
raise SynapseError(400, "Can only look up local users")

user = await self.store.get_user_by_id(user_id)
if user is None:
Expand Down Expand Up @@ -898,6 +899,73 @@ async def on_GET(

return 200, ret

async def on_DELETE(
self, request: SynapseRequest, user_id: str
) -> Tuple[int, JsonDict]:
# This will always be set by the time Twisted calls us.
assert request.args is not None

await assert_requester_is_admin(self.auth, request)

if not self.is_mine(UserID.from_string(user_id)):
raise SynapseError(400, "Can only look up local users")

user = await self.store.get_user_by_id(user_id)
if user is None:
raise NotFoundError("Unknown user")

start = parse_integer(request, "from", default=0)
limit = parse_integer(request, "limit", default=100)

if start < 0:
raise SynapseError(
400,
"Query parameter from must be a string representing a positive integer.",
errcode=Codes.INVALID_PARAM,
)

if limit < 0:
raise SynapseError(
400,
"Query parameter limit must be a string representing a positive integer.",
errcode=Codes.INVALID_PARAM,
)

# If neither `order_by` nor `dir` is set, set the default order
# to newest media is on top for backward compatibility.
if b"order_by" not in request.args and b"dir" not in request.args:
order_by = MediaSortOrder.CREATED_TS.value
direction = "b"
else:
order_by = parse_string(
request,
"order_by",
default=MediaSortOrder.CREATED_TS.value,
allowed_values=(
MediaSortOrder.MEDIA_ID.value,
MediaSortOrder.UPLOAD_NAME.value,
MediaSortOrder.CREATED_TS.value,
MediaSortOrder.LAST_ACCESS_TS.value,
MediaSortOrder.MEDIA_LENGTH.value,
MediaSortOrder.MEDIA_TYPE.value,
MediaSortOrder.QUARANTINED_BY.value,
MediaSortOrder.SAFE_FROM_QUARANTINE.value,
),
)
direction = parse_string(
request, "dir", default="f", allowed_values=("f", "b")
)

media, _ = await self.store.get_local_media_by_user_paginate(
start, limit, user_id, order_by, direction
)

deleted_media, total = await self.media_repository.delete_local_media_ids(
([row["media_id"] for row in media])
)

return 200, {"deleted_media": deleted_media, "total": total}


class UserTokenRestServlet(RestServlet):
"""An admin API for logging in as a user.
Expand Down Expand Up @@ -1017,7 +1085,7 @@ async def on_GET(
await assert_requester_is_admin(self.auth, request)

if not self.hs.is_mine_id(user_id):
raise SynapseError(400, "Can only lookup local users")
raise SynapseError(400, "Can only look up local users")

if not await self.store.get_user_by_id(user_id):
raise NotFoundError("User not found")
Expand Down
6 changes: 4 additions & 2 deletions synapse/rest/media/v1/media_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -836,7 +836,9 @@ async def delete_old_remote_media(self, before_ts: int) -> Dict[str, int]:

return {"deleted": deleted}

async def delete_local_media(self, media_id: str) -> Tuple[List[str], int]:
async def delete_local_media_ids(
self, media_ids: List[str]
) -> Tuple[List[str], int]:
"""
Delete the given local or remote media ID from this server

Expand All @@ -845,7 +847,7 @@ async def delete_local_media(self, media_id: str) -> Tuple[List[str], int]:
Returns:
A tuple of (list of deleted media IDs, total deleted media IDs).
"""
return await self._remove_local_media_from_disk([media_id])
return await self._remove_local_media_from_disk(media_ids)

async def delete_old_local_media(
self,
Expand Down
Loading