Compare commits
10 Commits
a8f22fb741
...
5fd8fada2c
Author | SHA1 | Date | |
---|---|---|---|
5fd8fada2c | |||
281ad30eec | |||
c0947ebbe2 | |||
089b14e6c9 | |||
4a80d4097e | |||
9bbd87ed36 | |||
1f7ade3c1f | |||
2914ff7856 | |||
4a55ad7ac0 | |||
a9b263f33c |
@ -1,2 +1,33 @@
|
|||||||
Admin API
|
Admin API
|
||||||
=========
|
=========
|
||||||
|
|
||||||
|
The administration API ``/admin`` helps the administrator user manage the Sachet server.
|
||||||
|
|
||||||
|
An important component that is not within this endpoint is user management.
|
||||||
|
See :ref:`user_info_api` and :ref:`user_list_api` for information about managing users.
|
||||||
|
|
||||||
|
Server settings
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Sachet has a server settings API::
|
||||||
|
|
||||||
|
GET /admin/settings
|
||||||
|
PATCH /admin/settings
|
||||||
|
PUT /admin/settings
|
||||||
|
|
||||||
|
Currently, server settings are represented by the following object:
|
||||||
|
|
||||||
|
.. code-block:: json
|
||||||
|
|
||||||
|
{
|
||||||
|
"default_permissions": ["PERMISSION1", "PERMISSION2"]
|
||||||
|
}
|
||||||
|
|
||||||
|
Anonymous permissions
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
Anonymous permissions (``default_permissions`` in the schema) are given to clients that do not authenticate.
|
||||||
|
It is an array of strings as described by :ref:`permissions_table`.
|
||||||
|
|
||||||
|
This can be useful, for example, to publish a file to the Internet.
|
||||||
|
If the Read shares permission is enabled in anonymous permissions, anyone can read a share if given the link to it.
|
||||||
|
293
docs/files.rst
293
docs/files.rst
@ -1,2 +1,295 @@
|
|||||||
Files API
|
Files API
|
||||||
=========
|
=========
|
||||||
|
|
||||||
|
Overview
|
||||||
|
--------
|
||||||
|
The file upload process essentially works as follows:
|
||||||
|
|
||||||
|
#. ``POST /files`` with the metadata to create a share.
|
||||||
|
This will return an URL with the share you just created.
|
||||||
|
(See :ref:`files_list_api`.)
|
||||||
|
#. ``POST /files/<uuid>/content`` (the UUID is in the URL from the previous step) to upload the actual data of the share. (See :ref:`files_content_api`.)
|
||||||
|
#. ``GET /files/<uuid>/content`` to read the share.
|
||||||
|
|
||||||
|
Share modification is done with ``PUT /files/<uuid>/content``.
|
||||||
|
|
||||||
|
Shares can be locked, which means they can't be modified or deleted.
|
||||||
|
See :ref:`files_lock_api` for more information.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
All data uploads are chunked: see :ref:`files_chunked_upload`.
|
||||||
|
|
||||||
|
.. _files_schema:
|
||||||
|
|
||||||
|
File Schema
|
||||||
|
-----------
|
||||||
|
|
||||||
|
In JSON, a file share has the following properties::
|
||||||
|
|
||||||
|
{
|
||||||
|
"file_name": "file.txt",
|
||||||
|
"initialized": true,
|
||||||
|
"locked": false,
|
||||||
|
"owner_name": "user",
|
||||||
|
"share_id": "9ae90f06-a751-409c-a9fe-8277575b9914"
|
||||||
|
}
|
||||||
|
|
||||||
|
.. list-table::
|
||||||
|
:header-rows: 1
|
||||||
|
:widths: 25 25 25 50
|
||||||
|
|
||||||
|
* - Property
|
||||||
|
- Type
|
||||||
|
- Limits
|
||||||
|
- Description
|
||||||
|
* - ``file_name``
|
||||||
|
- String
|
||||||
|
-
|
||||||
|
- The file's name (with extension).
|
||||||
|
* - ``initialized``
|
||||||
|
- Boolean
|
||||||
|
- Read-only
|
||||||
|
- Shows if content exists for this share.
|
||||||
|
* - ``locked``
|
||||||
|
- Boolean
|
||||||
|
- Read-only
|
||||||
|
- Shows if share is locked (see :ref:`files_lock_api`.)
|
||||||
|
* - ``owner_name``
|
||||||
|
- string
|
||||||
|
-
|
||||||
|
- Username of the owner.
|
||||||
|
* - ``share_id``
|
||||||
|
- string
|
||||||
|
- Read-only
|
||||||
|
- UUID that uniquely identifies this share.
|
||||||
|
|
||||||
|
.. _files_metadata_api:
|
||||||
|
|
||||||
|
Metadata API
|
||||||
|
------------
|
||||||
|
|
||||||
|
The File Metadata API allows managing file shares' metadata.
|
||||||
|
|
||||||
|
Sachet implements the following endpoints for this API::
|
||||||
|
|
||||||
|
GET /files/<file_uuid>
|
||||||
|
PATCH /files/<file_uuid>
|
||||||
|
PUT /files/<file_uuid>
|
||||||
|
|
||||||
|
GET
|
||||||
|
^^^
|
||||||
|
Requesting ``GET /files/<file_uuid>`` returns a JSON object conforming to the :ref:`File schema<files_schema>`.
|
||||||
|
This contains the information about the share with the specified UUID.
|
||||||
|
|
||||||
|
An example response:
|
||||||
|
|
||||||
|
.. code-block:: json
|
||||||
|
|
||||||
|
{
|
||||||
|
"file_name": "file.txt",
|
||||||
|
"initialized": true,
|
||||||
|
"locked": false,
|
||||||
|
"owner_name": "user",
|
||||||
|
"share_id": "9ae90f06-a751-409c-a9fe-8277575b9914"
|
||||||
|
}
|
||||||
|
|
||||||
|
This method requires the :ref:`read<permissions_table>` permission.
|
||||||
|
|
||||||
|
PATCH
|
||||||
|
^^^^^
|
||||||
|
|
||||||
|
Requesting ``PATCH /files/<file_uuid>`` allows modifying some or all fields of the share's metadata.
|
||||||
|
The request body is JSON conforming to the :ref:`File schema<files_schema>`.
|
||||||
|
Properties may be left out: they won't be modified.
|
||||||
|
|
||||||
|
For example, to modify a share's filename:
|
||||||
|
|
||||||
|
.. code-block:: json
|
||||||
|
|
||||||
|
{
|
||||||
|
"file_name": "foobar.mp3"
|
||||||
|
}
|
||||||
|
|
||||||
|
This method requires the :ref:`modify<permissions_table>` permission.
|
||||||
|
|
||||||
|
PUT
|
||||||
|
^^^
|
||||||
|
|
||||||
|
Requesting ``PUT /files/<file_uuid>`` completely replaces a share's metadata.
|
||||||
|
The request body is JSON conforming to the :ref:`File schema<files_schema>`.
|
||||||
|
No property may be left out.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
.. code-block:: json
|
||||||
|
|
||||||
|
{
|
||||||
|
"file_name": "foobar.mp4",
|
||||||
|
"owner_name": "user"
|
||||||
|
}
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
The permissions from the schema that are missing here are read-only.
|
||||||
|
|
||||||
|
.. _files_list_api:
|
||||||
|
|
||||||
|
List API
|
||||||
|
--------
|
||||||
|
|
||||||
|
The File List API allows listing shares and creating new ones::
|
||||||
|
|
||||||
|
GET /files
|
||||||
|
POST /files
|
||||||
|
|
||||||
|
GET
|
||||||
|
^^^
|
||||||
|
|
||||||
|
``GET /files`` is a :ref:`paginated endpoint<pagination>` that returns a list of shares.
|
||||||
|
|
||||||
|
To access this endpoint, a user needs the :ref:`list shares<permissions_table>` permission.
|
||||||
|
|
||||||
|
POST
|
||||||
|
^^^^
|
||||||
|
|
||||||
|
``POST /files`` creates a new share.
|
||||||
|
The request body must conform to the :ref:`File schema<files_schema>`.
|
||||||
|
|
||||||
|
To access this endpoint, a user needs the :ref:`create shares<permissions_table>` permission.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
The share created here is empty, and only contains metadata.
|
||||||
|
See :ref:`files_content_api` for information on uploading content.
|
||||||
|
|
||||||
|
Upon success, the server will respond like this:
|
||||||
|
|
||||||
|
.. code-block:: json
|
||||||
|
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"url": "/files/d9eafb5e-af48-40ec-b6fd-f7ea99e6d990"
|
||||||
|
}
|
||||||
|
|
||||||
|
The ``url`` field represents the share you just created.
|
||||||
|
It can be used in further requests to upload content to the share.
|
||||||
|
|
||||||
|
.. _files_content_api:
|
||||||
|
|
||||||
|
Content API
|
||||||
|
-----------
|
||||||
|
|
||||||
|
The File Content API allows managing file shares' contents.
|
||||||
|
|
||||||
|
Sachet implements the following endpoints for this API::
|
||||||
|
|
||||||
|
POST /files/<file_uuid>/content
|
||||||
|
PUT /files/<file_uuid>/content
|
||||||
|
GET /files/<file_uuid>/content
|
||||||
|
|
||||||
|
POST
|
||||||
|
^^^^
|
||||||
|
|
||||||
|
``POST /files/<file_uuid>/content`` initializes the content of an empty share.
|
||||||
|
This endpoint requires the :ref:`create shares<permissions_table>` permission.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
You must first create a share before initializing it: see :ref:`files_list_api` for information about creation.
|
||||||
|
|
||||||
|
Uploads must be chunked (see :ref:`files_chunked_upload`).
|
||||||
|
|
||||||
|
To modify the contents of an existing share, use ``PUT`` instead.
|
||||||
|
|
||||||
|
PUT
|
||||||
|
^^^^
|
||||||
|
|
||||||
|
``PUT /files/<file_uuid>/content`` modifies the content of an existing share.
|
||||||
|
This endpoint requires the :ref:`modify shares<permissions_table>` permission.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
You must initialize a share's content using ``POST`` before modifying it.
|
||||||
|
|
||||||
|
Uploads must be chunked (see :ref:`files_chunked_upload`).
|
||||||
|
|
||||||
|
GET
|
||||||
|
^^^^
|
||||||
|
|
||||||
|
``GET /files/<file_uuid>/content`` reads the contents of a share.
|
||||||
|
This endpoint requires the :ref:`read shares<permissions_table>` permission.
|
||||||
|
|
||||||
|
This endpoint supports `HTTP Range <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range>`_ headers.
|
||||||
|
|
||||||
|
.. _files_chunked_upload :
|
||||||
|
|
||||||
|
Chunked upload protocol
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
To allow for uploading large files reliably, Sachet requires that you upload files in chunks.
|
||||||
|
|
||||||
|
Partial uploads do not affect the state of the share;
|
||||||
|
a new file exists only once all chunks are uploaded.
|
||||||
|
|
||||||
|
Chunks are ordered by their index.
|
||||||
|
Once an upload finishes, they are combined in that order to form the new file.
|
||||||
|
|
||||||
|
The server will respond with ``200 OK`` when chunks are sent.
|
||||||
|
When the final chunk is sent, and the upload is completed,
|
||||||
|
the server will instead respond with ``201 Created``.
|
||||||
|
|
||||||
|
Every chunk has the following schema:
|
||||||
|
|
||||||
|
.. _files_chunk_schema:
|
||||||
|
|
||||||
|
.. code-block::
|
||||||
|
|
||||||
|
dztotalchunks = 3
|
||||||
|
dzchunkindex = 2
|
||||||
|
dzuuid = "unique_id"
|
||||||
|
upload = <binary data>
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
This data is sent via a ``multipart/form-data`` request; it's not JSON.
|
||||||
|
|
||||||
|
.. list-table::
|
||||||
|
:header-rows: 1
|
||||||
|
:widths: 25 25 50
|
||||||
|
|
||||||
|
* - Property
|
||||||
|
- Type
|
||||||
|
- Description
|
||||||
|
* - ``dztotalchunks``
|
||||||
|
- Integer
|
||||||
|
- Total number of chunks the client will send.
|
||||||
|
* - ``dzchunkindex``
|
||||||
|
- Integer
|
||||||
|
- Number of the chunk being sent.
|
||||||
|
* - ``dzuuid``
|
||||||
|
- String
|
||||||
|
- ID which is the same for all chunks in a single upload.
|
||||||
|
* - ``upload``
|
||||||
|
- Binary data (file)
|
||||||
|
- Data contained in this chunk.
|
||||||
|
|
||||||
|
.. _files_lock_api:
|
||||||
|
|
||||||
|
Lock API
|
||||||
|
--------
|
||||||
|
|
||||||
|
Files can be locked and unlocked.
|
||||||
|
When locked, a share can not be modified or deleted.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
When attempting illegal actions on a locked share, the server will respond ``423 Locked``.
|
||||||
|
|
||||||
|
The following API is used::
|
||||||
|
|
||||||
|
POST /files/<uuid>/lock
|
||||||
|
POST /files/<uuid>/unlock
|
||||||
|
|
||||||
|
A user needs the :ref:`lock permission<permissions_table>` to access this API.
|
||||||
|
|
||||||
|
To query whether a file is locked or not, see :ref:`files_metadata_api`.
|
||||||
|
@ -11,6 +11,7 @@ Welcome to Sachet's documentation!
|
|||||||
:caption: Contents:
|
:caption: Contents:
|
||||||
|
|
||||||
authentication
|
authentication
|
||||||
|
pagination
|
||||||
permissions
|
permissions
|
||||||
user
|
user
|
||||||
admin
|
admin
|
||||||
|
54
docs/pagination.rst
Normal file
54
docs/pagination.rst
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
.. _pagination:
|
||||||
|
|
||||||
|
Paginated APIs
|
||||||
|
==============
|
||||||
|
|
||||||
|
Some APIs in Sachet might return lots of data.
|
||||||
|
Because this is often a lot to handle, Sachet will use pagination on some endpoints.
|
||||||
|
|
||||||
|
For example, let's say we want to list all shares on the server.
|
||||||
|
To do this, we'll run ``GET /files`` using the following request body:
|
||||||
|
|
||||||
|
.. code-block:: json
|
||||||
|
|
||||||
|
{
|
||||||
|
"page": "1",
|
||||||
|
"per_page": "3"
|
||||||
|
}
|
||||||
|
|
||||||
|
As seen in the above example, paginated APIs on Sachet require the following parameters:
|
||||||
|
|
||||||
|
* ``page`` : the number of the page we want to query;
|
||||||
|
* ``per_page`` : the number of items per page we receive.
|
||||||
|
|
||||||
|
For our example, the server might respond like this (fields removed for brevity):
|
||||||
|
|
||||||
|
.. code-block:: json
|
||||||
|
|
||||||
|
{
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"file_name": "file1",
|
||||||
|
"share_id": "339ce639-cf54-4acf-9620-c915c5dce406"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_name": "file2",
|
||||||
|
"share_id": "9ae90f06-a751-409c-a9fe-8277575b9914"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_name": "file3",
|
||||||
|
"share_id": "4f8e41ab-3327-4fc1-a52b-8951ac5c641f"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"next": 2,
|
||||||
|
"prev": null
|
||||||
|
}
|
||||||
|
|
||||||
|
Our query's result is returned as an array in ``data``.
|
||||||
|
As we requested, we have 3 items on the first page.
|
||||||
|
|
||||||
|
There's also two extra fields ``next`` and ``prev``,
|
||||||
|
which help us navigate to other pages.
|
||||||
|
Since we're on the first page, there is no previous page, which is why ``prev`` is empty.
|
||||||
|
|
||||||
|
If we wished to go to the next page, we'd make the same request with the new page number.
|
@ -1,2 +1,61 @@
|
|||||||
Permissions
|
Permissions
|
||||||
===========
|
===========
|
||||||
|
|
||||||
|
Sachet offers a selection of permissions that can be assigned to users,
|
||||||
|
which manage their access to certain endpoints.
|
||||||
|
|
||||||
|
Serialization
|
||||||
|
-------------
|
||||||
|
In Sachet's JSON API, permissions are serialized as an array of string codes.
|
||||||
|
These codes are documented in :ref:`permissions_table`.
|
||||||
|
|
||||||
|
For instance, here is an example output for ``GET /users/user``:
|
||||||
|
|
||||||
|
.. code-block:: json
|
||||||
|
|
||||||
|
{
|
||||||
|
"permissions": [
|
||||||
|
"CREATE",
|
||||||
|
"DELETE",
|
||||||
|
"LIST",
|
||||||
|
"READ"
|
||||||
|
],
|
||||||
|
"register_date": "2023-05-08T18:57:27.982479",
|
||||||
|
"username": "user"
|
||||||
|
}
|
||||||
|
|
||||||
|
.. _permissions_table:
|
||||||
|
|
||||||
|
Table of permissions
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
The following is a table of permissions Sachet offers, and what they do:
|
||||||
|
|
||||||
|
.. list-table::
|
||||||
|
:widths: 25 25 50
|
||||||
|
:header-rows: 1
|
||||||
|
|
||||||
|
* - Permission
|
||||||
|
- Code
|
||||||
|
- Description
|
||||||
|
* - Create shares
|
||||||
|
- ``CREATE``
|
||||||
|
- Allows uploading files to Sachet.
|
||||||
|
* - Modify shares
|
||||||
|
- ``MODIFY``
|
||||||
|
- Allows users to modify their own shares' contents and metadata.
|
||||||
|
* - Delete shares
|
||||||
|
- ``DELETE``
|
||||||
|
- Allows users to delete any share.
|
||||||
|
* - Lock shares
|
||||||
|
- ``LOCK``
|
||||||
|
- Allows users to lock and unlock shares (see :ref:`files_lock_api`).
|
||||||
|
* - List shares
|
||||||
|
- ``LIST``
|
||||||
|
- Allows users to list all shares from all users.
|
||||||
|
* - Read shares
|
||||||
|
- ``READ``
|
||||||
|
- Allows users to read any share.
|
||||||
|
* - Administration
|
||||||
|
- ``ADMIN``
|
||||||
|
- Allows creating users and managing their permissions.
|
||||||
|
139
docs/user.rst
139
docs/user.rst
@ -1,2 +1,141 @@
|
|||||||
User API
|
User API
|
||||||
========
|
========
|
||||||
|
|
||||||
|
.. _user_schema:
|
||||||
|
|
||||||
|
User Schema
|
||||||
|
-----------
|
||||||
|
|
||||||
|
In JSON, a User object has the following properties:
|
||||||
|
|
||||||
|
.. code-block:: json
|
||||||
|
|
||||||
|
{
|
||||||
|
"username": "<username>",
|
||||||
|
"password": "<password>",
|
||||||
|
"permissions": ["PERMISSION1", "PERMISSION2"],
|
||||||
|
"register_date": "2023-05-08T18:57:27.982479"
|
||||||
|
}
|
||||||
|
|
||||||
|
.. list-table::
|
||||||
|
:header-rows: 1
|
||||||
|
:widths: 25 25 25 50
|
||||||
|
|
||||||
|
* - Property
|
||||||
|
- Type
|
||||||
|
- Limits
|
||||||
|
- Description
|
||||||
|
* - ``username``
|
||||||
|
- String
|
||||||
|
-
|
||||||
|
- User's name. This also acts as an ID.
|
||||||
|
* - ``password``
|
||||||
|
- String
|
||||||
|
- Write-only
|
||||||
|
- Password in plain text.
|
||||||
|
* - ``permissions``
|
||||||
|
- List of String
|
||||||
|
-
|
||||||
|
- List of permissions (see :ref:`permissions_table`).
|
||||||
|
* - ``register_date``
|
||||||
|
- DateTime
|
||||||
|
- Read-only
|
||||||
|
- Time the user registered at.
|
||||||
|
|
||||||
|
.. _user_info_api:
|
||||||
|
|
||||||
|
User Info API
|
||||||
|
-------------
|
||||||
|
|
||||||
|
The User Info API allows managing users and their permissions.
|
||||||
|
|
||||||
|
Sachet implements the following endpoints for this API::
|
||||||
|
|
||||||
|
GET /users/<username>
|
||||||
|
PATCH /users/<username>
|
||||||
|
PUT /users/<username>
|
||||||
|
|
||||||
|
GET
|
||||||
|
^^^
|
||||||
|
Requesting ``GET /users/<username>`` returns a JSON object conforming to the :ref:`User schema<user_schema>`.
|
||||||
|
This contains the information about the specified username.
|
||||||
|
|
||||||
|
An example response:
|
||||||
|
|
||||||
|
.. code-block:: json
|
||||||
|
|
||||||
|
{
|
||||||
|
"permissions": [
|
||||||
|
"CREATE",
|
||||||
|
"DELETE",
|
||||||
|
"LIST",
|
||||||
|
"READ"
|
||||||
|
],
|
||||||
|
"register_date": "2023-05-08T18:57:27.982479",
|
||||||
|
"username": "user"
|
||||||
|
}
|
||||||
|
|
||||||
|
A user can only read information about themselves, unless they have the :ref:`administrator permission<permissions_table>`.
|
||||||
|
|
||||||
|
PATCH
|
||||||
|
^^^^^
|
||||||
|
|
||||||
|
Requesting ``PATCH /users/<username>`` allows modifying some or all fields of a user.
|
||||||
|
The request body is JSON conforming to the :ref:`User schema<user_schema>`.
|
||||||
|
Properties may be left out: they won't be modified.
|
||||||
|
|
||||||
|
For example, to modify a user's permissions:
|
||||||
|
|
||||||
|
.. code-block:: json
|
||||||
|
|
||||||
|
{
|
||||||
|
"permissions": [
|
||||||
|
"CREATE"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
Only :ref:`administrators<permissions_table>` can request this method.
|
||||||
|
|
||||||
|
PUT
|
||||||
|
^^^
|
||||||
|
|
||||||
|
Requesting ``PUT /users/<username>`` completely replaces a user's information.
|
||||||
|
The request body is JSON conforming to the :ref:`User schema<user_schema>`.
|
||||||
|
No property may be left out.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
.. code-block:: json
|
||||||
|
|
||||||
|
{
|
||||||
|
"permissions": [
|
||||||
|
"CREATE"
|
||||||
|
],
|
||||||
|
"password": "123",
|
||||||
|
"username": "user"
|
||||||
|
}
|
||||||
|
|
||||||
|
Only :ref:`administrators<permissions_table>` can request this method.
|
||||||
|
|
||||||
|
.. _user_list_api:
|
||||||
|
|
||||||
|
List API
|
||||||
|
--------
|
||||||
|
|
||||||
|
There is also a User List API::
|
||||||
|
|
||||||
|
GET /users
|
||||||
|
POST /users
|
||||||
|
|
||||||
|
This API is only accessible to administrators (see :ref:`permissions_table`).
|
||||||
|
|
||||||
|
GET
|
||||||
|
^^^
|
||||||
|
|
||||||
|
``GET /users`` is a :ref:`paginated endpoint<pagination>` that returns a list of users.
|
||||||
|
|
||||||
|
POST
|
||||||
|
^^^^
|
||||||
|
|
||||||
|
``POST /users`` creates a new user.
|
||||||
|
The request body must conform to the :ref:`User schema<user_schema>`.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user