Skip to content

Commit

Permalink
Add support for batch export and delete
Browse files Browse the repository at this point in the history
  • Loading branch information
kencx committed Jul 12, 2023
1 parent b6fc998 commit a3c6b8e
Show file tree
Hide file tree
Showing 4 changed files with 182 additions and 14 deletions.
117 changes: 117 additions & 0 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ All endpoints are wrappers for `calibredb`
* [POST Empty Book](#post-empty-book)
* [PUT Book](#put-book)
* [DELETE Book](#delete-book)
* [Batch DELETE](#delete-books)
* [Export Book](#export-book)
* [Batch Export](#export-books)

<h3 id="get-book">GET <code>/books/{id}</code></h3>

Expand Down Expand Up @@ -790,6 +792,58 @@ resp = requests.delete("localhost:5000/books/1")
[Return to top](#)
</details>

<h3 id="delete-books">DELETE <code>/books</code></h3>

<details>

<summary>
Batch delete books
</summary>

#### Request

* Methods: `DELETE`

##### Query Parameters

* id (mandatory): Comma separated values or separate parameters.

```
/books?id=1,2
/books?id=1&id=2
```

#### Responses

See DELETE `/books/{id}`.

<details>
<summary>
Examples
</summary>
<br>

Curl

```console
$ curl -X DELETE http://localhost:5000/books?id=1,2
$ curl -X DELETE http://localhost:5000/books?id=1&id=2
```

Python

```python
import requests

resp = requests.delete("localhost:5000/books", params={"id": "1,2"})
```
</details>
<br>

[Return to top](#)

</details>

<h3 id="export-book">GET <code>/export/{id}</code></h3>

<details>
Expand All @@ -808,6 +862,8 @@ resp = requests.delete("localhost:5000/books/1")
##### Success

* Code: `200 OK`
* Content:
* A single file object

##### Error

Expand Down Expand Up @@ -857,3 +913,64 @@ with open("foo.epub", "wb") as f:
<br>

[Return to top](#)
</details>

<h3 id="export-books">GET <code>/export</code></h3>

<details>

<summary>
Batch export books
</summary>

#### Request

* Methods: `GET`

##### Query Parameters

* id (mandatory): Comma separated values or separate parameters.

```
/export?id=1,2
/export?id=1&id=2
```

#### Responses

##### Success

* Code: `200 OK`
* Content:
* A zip file object `exports.zip` containing all files

##### Error

See GET `/export/{id}`.

<details>
<summary>
Examples
</summary>
<br>

Curl

```console
$ curl http://localhost:5000/export?id=1,2 -o foo.zip
$ curl http://localhost:5000/export?id=1&id=2 -o foo.zip
```

Python

```python
import requests
import zipfile

resp = requests.get("localhost:5000/export", params={"id": "1,2"}, stream=True)
z = zipfile.ZipFile(BytesIO(resp.content))
```
</details>
<br>

[Return to top](#)
7 changes: 3 additions & 4 deletions calibre_rest/calibre.py
Original file line number Diff line number Diff line change
Expand Up @@ -559,7 +559,7 @@ def _handle_add_flags(self, cmd: str, book: Book = None) -> str:
cmd += f" --{flag_name} {quote(str(value))}"
return cmd

def remove(self, ids: list[int], permanent: bool = False) -> str:
def remove(self, ids: list[int], permanent: bool = False) -> None:
"""Remove book from calibre database.
Fails silently with no output if given IDs do not exist.
Expand All @@ -579,8 +579,7 @@ def remove(self, ids: list[int], permanent: bool = False) -> str:
if permanent:
cmd += " --permanent"

out, _ = self._run(cmd)
return out
self._run(cmd)

def add_format(
self, id: int, replace: bool = False, data_file: bool = False
Expand Down Expand Up @@ -732,7 +731,7 @@ def export(
if formats is not None:
cmd += f" --formats {','.join(formats)}"

cmd += f' {",".join([str(i) for i in ids])}'
cmd += f' {" ".join([str(i) for i in ids])}'

try:
self._run(cmd)
Expand Down
47 changes: 39 additions & 8 deletions calibre_rest/routes.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import json
import os
import os.path as path
import shutil
import tempfile

from flask import abort
Expand Down Expand Up @@ -168,26 +169,48 @@ def update_book(id):
return response(200, jsonify(books=book))


@app.route("/books", methods=["DELETE"])
@app.route("/books/<int:id>", methods=["DELETE"])
def delete_book(id):
def delete_book(id=None):
"""Remove existing book in calibre library."""

calibredb.remove([id])
ids = request.args.getlist("id") or None
if ids is not None:
if len(ids) == 1:
ids = [int(i) for i in ids[0].split(",")]
else:
ids = [int(i) for i in ids]
elif id is not None:
ids = [id]
else:
abort(400)

# check if book still exists
book = calibredb.get_book(id)
if book:
abort(500, f"book {id} was not deleted")
calibredb.remove(ids)

for id in ids:
# check if book still exists
book = calibredb.get_book(id)
if book:
abort(500, f"book {id} was not deleted")

return response(200, "")


@app.route("/export", methods=["GET"])
@app.route("/export/<int:id>", methods=["GET"])
def export_book(id=None):
"""Export existing book from calibre library to file."""

ids = request.args.getlist("id") or None
if ids is None:
if ids is not None:
if len(ids) == 1:
ids = [int(i) for i in ids[0].split(",")]
else:
ids = [int(i) for i in ids]
elif id is not None:
ids = [id]
else:
abort(400)

with tempfile.TemporaryDirectory() as exports_dir:
try:
Expand All @@ -202,8 +225,16 @@ def export_book(id=None):

if len(files) <= 0:
abort(500)
else:
elif len(files) == 1:
return send_from_directory(exports_dir, files[0], as_attachment=True)
else:
# zip exports_dir/* to /tmp/exports.zip
zipfile = path.join(tempfile.gettempdir(), "exports")
zipfile = shutil.make_archive(zipfile, "zip", exports_dir)

return send_from_directory(
tempfile.gettempdir(), "exports.zip", as_attachment=True
)


# export --all
Expand Down
25 changes: 23 additions & 2 deletions tests/integration/routes_test.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import json
import os
import zipfile
from enum import IntEnum
from http import HTTPStatus
from io import BytesIO

import pytest
import requests
Expand Down Expand Up @@ -42,8 +44,8 @@ def seed_books(url, test_txt):
files = [("file", (f"foo{str(i)}.txt", f"hello {str(i)}")) for i in range(1, 6)]
ids = post(f"{url}/books", HTTPStatus.CREATED, files=files)
yield ids
for i in ids:
delete(url, i)
resp = requests.delete(f"{url}/books", params={"id": ",".join(ids)})
assert resp.status_code == HTTPStatus.OK


def test_version(url):
Expand Down Expand Up @@ -99,6 +101,14 @@ def test_delete(url, seed_book):
)


def test_delete_multiple(url, seed_books):
resp = requests.delete(f"{url}/books", params={"id": ",".join(seed_books)})
assert resp.status_code == HTTPStatus.OK

get_resp = requests.get(f"{url}/books")
assert get_resp.status_code == HTTPStatus.NO_CONTENT


def test_get_books_empty(url):
resp = requests.get(f"{url}/books")

Expand Down Expand Up @@ -617,6 +627,17 @@ def test_export_book(url, seed_book):
assert resp.text == "hello world!\n"


def test_export_books(url, seed_books):
resp = requests.get(
f"{url}/export", params={"id": ",".join(seed_books)}, stream=True
)
assert resp.status_code == HTTPStatus.OK

z = zipfile.ZipFile(BytesIO(resp.content))
for i in range(len(z.namelist())):
assert f"foo{i+1}" in z.namelist()[i]


def get(url: str, id: int | str, code: IntEnum, mappings: dict = None):
resp = requests.get(f"{url}/books/{id}")
assert resp.status_code == code
Expand Down

0 comments on commit a3c6b8e

Please sign in to comment.