can't update new cover of shelf and book by API token #5490

Closed
opened 2026-02-05 10:06:45 +03:00 by OVERLORD · 3 comments
Owner

Originally created by @shmbatom on GitHub (Nov 11, 2025).

API Endpoint or Feature

I failed to update the cover of shelf
I learned API Doc as below:
PUT
http://192.168.0.101/api/shelves/{id}
update
Update the details of a single shelf. An array of books IDs can be provided in the request. These will be added to the shelf in the same order as provided and overwrite any existing book assignments. The cover image of a shelf can be set by sending a file via an 'image' property within a 'multipart/form-data' request. If the 'image' property is null then the shelf cover image will be removed.

Body Parameters
Param Name Value Rules
name string min:1 max:255
description string max:1900
description_html string max:2000
books array
tags array
image nullable image_extension mimes:jpeg,png,gif,webp,avif max:50000
Example Request
{
"name": "My updated shelf",
"description_html": "

This is my updated shelf with some books

",
"books": [5,1,3]
}
Example Response
{
"id": 20,
"name": "My updated shelf",
"slug": "my-updated-shelf",
"description": "This is my updated shelf with some books",
"created_by": 1,
"updated_by": 1,
"created_at": "2023-12-22T14:33:52.000000Z",
"updated_at": "2023-12-22T14:35:00.000000Z",
"owned_by": 1,
"description_html": "

This is my updated shelf</em> with some books</p>",
"tags": [
{
"name": "Category",
"value": "Learning",
"order": 0
}
],
"cover": null
}

Is it a bug? Or somebody knows how to slove it.
If have successful cod , It is better

Use-Case

this is a example
import requests
import base64

正确配置(端口80,认证格式符合BookStack官方要求)

SHELF_ID = 34
COVER_PATH = "/mnt/d/finance-knowledge/cover.jpg"
API_ID = "KEJLTXMUokeefy709ELFEEEAkT3rc"
API_SECRET = "s9brmh86jWWW5liXP5rh2DR"
URL = f"http://192.168.0.101/api/shelves/{SHELF_ID}"

auth_str = f"{API_ID}:{API_SECRET}".strip()
auth_base64 = base64.b64encode(auth_str.encode("utf-8")).decode("utf-8")
headers = {
"Authorization": f"Basic {auth_base64}"
}

try:
with open(COVER_PATH, "rb") as f:
files = {
"image": ("cover.jpg", f, "image/jpeg"),
"name": (None, "en_shelf")
}
# 发送PUT请求(BookStack更新书架的标准方法)
response = requests.put(
url=URL,
headers=headers,
files=files,
timeout=30,
verify=False
)

print("状态码:", response.status_code)
print("响应内容:", response.json() if response.text else "Empty")

except FileNotFoundError:
print(f"错误:封面文件不存在 → {COVER_PATH}")
except Exception as e:
print(f"请求错误:{str(e)}")

Additional context

No response

Originally created by @shmbatom on GitHub (Nov 11, 2025). ### API Endpoint or Feature I failed to update the cover of shelf I learned API Doc as below: PUT http://192.168.0.101/api/shelves/{id} update Update the details of a single shelf. An array of books IDs can be provided in the request. These will be added to the shelf in the same order as provided and overwrite any existing book assignments. The cover image of a shelf can be set by sending a file via an 'image' property within a 'multipart/form-data' request. If the 'image' property is null then the shelf cover image will be removed. Body Parameters Param Name Value Rules name string min:1 max:255 description string max:1900 description_html string max:2000 books array tags array image nullable image_extension mimes:jpeg,png,gif,webp,avif max:50000 Example Request { "name": "My updated shelf", "description_html": "<p>This is my <em>updated shelf</em> with some books</p>", "books": [5,1,3] } Example Response { "id": 20, "name": "My updated shelf", "slug": "my-updated-shelf", "description": "This is my updated shelf with some books", "created_by": 1, "updated_by": 1, "created_at": "2023-12-22T14:33:52.000000Z", "updated_at": "2023-12-22T14:35:00.000000Z", "owned_by": 1, "description_html": "<p>This is my <em>updated shelf<\/em> with some books<\/p>", "tags": [ { "name": "Category", "value": "Learning", "order": 0 } ], "cover": null } Is it a bug? Or somebody knows how to slove it. If have successful cod , It is better ### Use-Case this is a example import requests import base64 # 正确配置(端口80,认证格式符合BookStack官方要求) SHELF_ID = 34 COVER_PATH = "/mnt/d/finance-knowledge/cover.jpg" API_ID = "KEJLTXMUokeefy709ELFEEEAkT3rc" API_SECRET = "s9brmh86jWWW5liXP5rh2DR" URL = f"http://192.168.0.101/api/shelves/{SHELF_ID}" auth_str = f"{API_ID}:{API_SECRET}".strip() auth_base64 = base64.b64encode(auth_str.encode("utf-8")).decode("utf-8") headers = { "Authorization": f"Basic {auth_base64}" } try: with open(COVER_PATH, "rb") as f: files = { "image": ("cover.jpg", f, "image/jpeg"), "name": (None, "en_shelf") } # 发送PUT请求(BookStack更新书架的标准方法) response = requests.put( url=URL, headers=headers, files=files, timeout=30, verify=False ) print("状态码:", response.status_code) print("响应内容:", response.json() if response.text else "Empty") except FileNotFoundError: print(f"错误:封面文件不存在 → {COVER_PATH}") except Exception as e: print(f"请求错误:{str(e)}") ### Additional context _No response_
OVERLORD added the 🔩 API Request label 2026-02-05 10:06:45 +03:00
Author
Owner

@ssddanbrown commented on GitHub (Nov 11, 2025):

Hi @shmbatom,

Due to a limitation in how PHP currently handles form data, you'd need to send the request as a POST request, then include a _method parameter in the data set to PUT to indicate that this is a PUT request.
This is mentioned in the request format part of the API docs.
I appreciate that this is strange/awkward, and hopefully something we'll be able to change in future PHP versions.

So, just to confirm, the latter part of your code may look something like:

files = {
  "image": ("cover.jpg", f, "image/jpeg"),
  "name": (None, "en_shelf"),
  "_method": (None, "PUT")
}
# 发送PUT请求(BookStack更新书架的标准方法)
response = requests.post(
  url=URL,
  headers=headers,
  files=files,
  timeout=30,
  verify=False
)
@ssddanbrown commented on GitHub (Nov 11, 2025): Hi @shmbatom, Due to a limitation in how PHP currently handles form data, you'd need to send the request as a `POST` request, then include a `_method` parameter in the data set to `PUT` to indicate that this is a PUT request. This is mentioned in the request format part of the API docs. I appreciate that this is strange/awkward, and hopefully something we'll be able to change in future PHP versions. So, just to confirm, the latter part of your code may look something like: ```python files = { "image": ("cover.jpg", f, "image/jpeg"), "name": (None, "en_shelf"), "_method": (None, "PUT") } # 发送PUT请求(BookStack更新书架的标准方法) response = requests.post( url=URL, headers=headers, files=files, timeout=30, verify=False ) ```
Author
Owner

@shmbatom commented on GitHub (Nov 17, 2025):

Thanks for your answer , and I'm very happy to see your reply.
I solves this matter by sql before I got your code ,and finally I successed.
My py code like as below: @ssddanbrown

Details

def update_cover_via_sql(cover_path: Path, target_id: int, mysql_client: MySQLClient, site_url: str,
                         is_shelf: bool = True) -> bool:
    if not cover_path or not cover_path.exists():
        logger.warning("【封面更新】无有效封面文件,跳过更新")
        return False
    # 1. 复制封面到容器
    copy_success, copy_result = copy_cover_to_container(cover_path, is_shelf)
    if not copy_success:
        logger.error(f"封面更新失败:{copy_result}")  # 这里会输出具体原因
        return False
    container_dir, unique_filename = copy_result

    # 2. 生成SQL(更新bookshelves或books表的image_id)
    target_table = "bookshelves" if is_shelf else "books"
    db_path = container_dir.replace("/var/www/html/public", "") + unique_filename
    db_url = f"{site_url}{db_path}"

    # 获取旧封面ID
    success, data = mysql_client.execute_sql(f"SELECT image_id FROM {target_table} WHERE id = {target_id}")
    old_image_id = data[0]['image_id'] if success and data else None

    # 获取新封面ID(最大ID+1)
    success, data = mysql_client.execute_sql("SELECT MAX(id) AS max_id FROM images")
    new_image_id = int(data[0]['max_id']) + 1 if (success and data and data[0]['max_id']) else 1

    # 插入新封面记录
    insert_sql = f"""
    INSERT INTO images (id, name, url, path, type, uploaded_to, created_by, updated_by, created_at, updated_at)
    VALUES ({new_image_id}, '{unique_filename}', '{db_url}', '{db_path}', 'cover_bookshelf', {target_id}, 1, 1, NOW(), NOW())
    """
    if not mysql_client.execute_sql(insert_sql)[0]:
        return False

    # 更新目标表(书架/书籍)
    update_sql = f"UPDATE {target_table} SET image_id = {new_image_id} WHERE id = {target_id}"
    if not mysql_client.execute_sql(update_sql)[0]:
        return False

    logger.info(f"封面更新成功:新ID={new_image_id}(旧ID={old_image_id})")
    return True

@shmbatom commented on GitHub (Nov 17, 2025): Thanks for your answer , and I'm very happy to see your reply. I solves this matter by sql before I got your code ,and finally I successed. My py code like as below: @ssddanbrown <details><summary>Details</summary> <p> ```python def update_cover_via_sql(cover_path: Path, target_id: int, mysql_client: MySQLClient, site_url: str, is_shelf: bool = True) -> bool: if not cover_path or not cover_path.exists(): logger.warning("【封面更新】无有效封面文件,跳过更新") return False # 1. 复制封面到容器 copy_success, copy_result = copy_cover_to_container(cover_path, is_shelf) if not copy_success: logger.error(f"封面更新失败:{copy_result}") # 这里会输出具体原因 return False container_dir, unique_filename = copy_result # 2. 生成SQL(更新bookshelves或books表的image_id) target_table = "bookshelves" if is_shelf else "books" db_path = container_dir.replace("/var/www/html/public", "") + unique_filename db_url = f"{site_url}{db_path}" # 获取旧封面ID success, data = mysql_client.execute_sql(f"SELECT image_id FROM {target_table} WHERE id = {target_id}") old_image_id = data[0]['image_id'] if success and data else None # 获取新封面ID(最大ID+1) success, data = mysql_client.execute_sql("SELECT MAX(id) AS max_id FROM images") new_image_id = int(data[0]['max_id']) + 1 if (success and data and data[0]['max_id']) else 1 # 插入新封面记录 insert_sql = f""" INSERT INTO images (id, name, url, path, type, uploaded_to, created_by, updated_by, created_at, updated_at) VALUES ({new_image_id}, '{unique_filename}', '{db_url}', '{db_path}', 'cover_bookshelf', {target_id}, 1, 1, NOW(), NOW()) """ if not mysql_client.execute_sql(insert_sql)[0]: return False # 更新目标表(书架/书籍) update_sql = f"UPDATE {target_table} SET image_id = {new_image_id} WHERE id = {target_id}" if not mysql_client.execute_sql(update_sql)[0]: return False logger.info(f"封面更新成功:新ID={new_image_id}(旧ID={old_image_id})") return True ``` </p> </details>
Author
Owner

@ssddanbrown commented on GitHub (Nov 17, 2025):

Okay, glad you found a solution.
I'll therefore close this off.

Just a warning, those database commands will break on the latest versions of BookStack, and manual changes to the database are not advised for common/regular actions.

@ssddanbrown commented on GitHub (Nov 17, 2025): Okay, glad you found a solution. I'll therefore close this off. Just a warning, those database commands will break on the latest versions of BookStack, and manual changes to the database are not advised for common/regular actions.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/BookStack#5490