]> BookStack Code Mirror - api-scripts/commitdiff
Added php-generate-tree script
authorDan Brown <redacted>
Wed, 24 Jul 2024 15:42:00 +0000 (16:42 +0100)
committerDan Brown <redacted>
Wed, 24 Jul 2024 15:42:00 +0000 (16:42 +0100)
php-generate-tree/example.txt [new file with mode: 0644]
php-generate-tree/generate-tree.php [new file with mode: 0755]
php-generate-tree/readme.md [new file with mode: 0644]

diff --git a/php-generate-tree/example.txt b/php-generate-tree/example.txt
new file mode 100644 (file)
index 0000000..aabf7ac
--- /dev/null
@@ -0,0 +1,9 @@
+├── BOOKSHELF 1: My wonderful shelf of notes
+│   └── BOOK 39: My lovely book in my notes
+│       ├── PAGE 2745: A page within the book
+│       ├── CHAPTER 643: A lone chapter
+│       └── CHAPTER 644: My chapter with page
+│           └── PAGE 47830: My new great page
+├── BOOK 239: Scratch notes
+│   ├── PAGE 47870: Note A
+│   └── PAGE 47872: Note B
diff --git a/php-generate-tree/generate-tree.php b/php-generate-tree/generate-tree.php
new file mode 100755 (executable)
index 0000000..76dc316
--- /dev/null
@@ -0,0 +1,189 @@
+#!/usr/bin/env php
+<?php
+
+// API Credentials
+// You can either provide them as environment variables
+// or hard-code them in the empty strings below.
+$baseUrl = getenv('BS_URL') ?: '';
+$clientId = getenv('BS_TOKEN_ID') ?: '';
+$clientSecret = getenv('BS_TOKEN_SECRET') ?: '';
+
+// Script logic
+////////////////
+
+// Define the time we wait in between making API requests,
+// to help keep within rate limits and avoid exhausting resources.
+$apiPauseMicrosecs = 100;
+
+// Clean up the base path
+$baseUrl = rtrim($baseUrl, '/');
+
+// Get all items from the system keyed by ID
+$shelvesById = keyById(getAllOfAtListEndpoint("api/shelves", []));
+$booksById = keyById(getAllOfAtListEndpoint("api/books", []));
+
+// Fetch books that are on each shelf
+foreach ($shelvesById as $id => $shelf) {
+    $shelvesById[$id]['books'] = getBooksForShelf($id);
+    usleep($apiPauseMicrosecs);
+}
+
+// For each book, fetch its contents list
+foreach ($booksById as $id => $book) {
+    $booksById[$id]['contents'] = apiGetJson("api/books/{$id}")['contents'] ?? [];
+    usleep($apiPauseMicrosecs);
+}
+
+// Cycle through the shelves and display their contents
+$isBookShownById = [];
+foreach ($shelvesById as $id => $shelf) {
+    output($shelf, 'bookshelf', [false]);
+    $bookCount = count($shelf['books']);
+    for ($i=0; $i < $bookCount; $i++) {
+        $bookId = $shelf['books'][$i];
+        $book = $booksById[$bookId] ?? null;
+        if ($book) {
+            outputBookAndContents($book, [false, $i === $bookCount - 1]);
+            $isBookShownById[strval($book['id'])] = true;
+        }
+    }
+}
+
+// Cycle through books and display any that have not been
+// part of a shelve's output
+foreach ($booksById as $id => $book) {
+    if (isset($isBookShownById[$id])) {
+        continue;
+    }
+
+    outputBookAndContents($book, [false]);
+}
+
+/**
+ * Output a book for display, along with its contents.
+ */
+function outputBookAndContents(array $book, array $depthPath): void
+{
+    output($book, 'book', $depthPath);
+    $childCount = count($book['contents']);
+    for ($i=0; $i < $childCount; $i++) {
+        $child = $book['contents'][$i];
+        $childPath = array_merge($depthPath, [($i === $childCount - 1)]);
+        output($child, $child['type'], $childPath);
+        $pages = $child['pages'] ?? [];
+        $pageCount = count($pages);
+        for ($j=0; $j < count($pages); $j++) { 
+            $page = $pages[$j];
+            $innerPath = array_merge($childPath, [($j === $pageCount - 1)]);
+            output($page, 'page', $innerPath);
+        }
+    }
+}
+
+/**
+ * Output a single item for display.
+ */
+function output(array $item, string $type, array $depthPath): void
+{
+    $upperType = strtoupper($type);
+    $prefix = '';
+    $depth = count($depthPath);
+    for ($i=0; $i < $depth; $i++) { 
+        $isLastAtDepth = $depthPath[$i];
+        $end = ($i === $depth - 1);
+        if ($end) {
+            $prefix .= $isLastAtDepth ? '└' : '├';
+        } else {
+            $prefix .= $isLastAtDepth ? '    ' : '│   ';
+        }
+    }
+    echo $prefix . "── {$upperType} {$item['id']}: {$item['name']}\n";
+}
+
+/**
+ * Key an array of array-based data objects by 'id' value. 
+ */
+function keyById(array $data): array 
+{
+    $byId = [];
+    foreach ($data as $item) { 
+        $id = $item['id'];
+        $byId[$id] = $item;
+    }
+    return $byId;
+}
+
+/**
+ * Get the books for the given shelf ID.
+ * Returns an array of the book IDs.
+ */
+function getBooksForShelf(int $shelfId): array
+{
+    $resp = apiGetJson("api/shelves/{$shelfId}");
+    return array_map(function ($bookData) {
+        return $bookData['id'];
+    }, $resp['books'] ?? []);
+}
+
+/**
+ * Consume all items from the given API listing endpoint.
+ */
+function getAllOfAtListEndpoint(string $endpoint, array $params): array
+{
+    global $apiPauseMicrosecs;
+    $count = 100;
+    $offset = 0;
+    $all = [];
+
+    do {
+        $endpoint = $endpoint . '?' . http_build_query(array_merge($params, ['count' => $count, 'offset' => $offset]));
+        $resp = apiGetJson($endpoint);
+
+        $total = $resp['total'] ?? 0;
+        $new = $resp['data'] ?? [];
+        array_push($all, ...$new);
+        $offset += $count;
+        usleep($apiPauseMicrosecs);
+    } while ($offset < $total);
+
+    return $all;
+}
+
+/**
+ * Make a simple GET HTTP request to the API.
+ */
+function apiGet(string $endpoint): string
+{
+    global $baseUrl, $clientId, $clientSecret;
+    $url = rtrim($baseUrl, '/') . '/' . ltrim($endpoint, '/');
+    $opts = ['http' => ['header' => "Authorization: Token {$clientId}:{$clientSecret}"]];
+    $context = stream_context_create($opts);
+    return @file_get_contents($url, false, $context);
+}
+
+/**
+ * Make a simple GET HTTP request to the API &
+ * decode the JSON response to an array.
+ */
+function apiGetJson(string $endpoint): array
+{
+    $data = apiGet($endpoint);
+    $array = json_decode($data, true);
+
+    if (!is_array($array)) {
+        dd("Failed request to {$endpoint}", $data);
+    }
+
+    return $array;
+}
+
+/**
+ * DEBUG: Dump out the given variables and exit.
+ */
+function dd(...$args)
+{
+    foreach ($args as $arg) {
+        var_dump($arg);
+    }
+    exit(1);
+}
\ No newline at end of file
diff --git a/php-generate-tree/readme.md b/php-generate-tree/readme.md
new file mode 100644 (file)
index 0000000..a054297
--- /dev/null
@@ -0,0 +1,47 @@
+# Generate Tree
+
+This script will scan through all pages, chapters books and shelves via the API to generate a big tree structure list in plaintext.
+
+**This is a very simplistic single-script-file example of using the endpoints API together**
+, it is not a fully-featured & validated script, it error handling is very limited.
+
+Keep in mind, The tree generated will reflect content visible to the API user used when running the script.
+
+This script follows a `((Shelves > Books > (Chapters > Pages | Pages)) | Books)` structure so books and their contents may be repeated if on multiple shelves. Books not on any shelves will be shown at the end.
+
+## Requirements
+
+You will need php (~8.1+) installed on the machine you want to run this script on.
+You will also need BookStack API credentials (TOKEN_ID & TOKEN_SECRET) at the ready.
+
+## Running
+
+```bash
+# Downloading the script
+# ALTERNATIVELY: Clone the project from GitHub and run locally.
+curl https://raw.githubusercontent.com/BookStackApp/api-scripts/main/php-generate-tree/generate-tree.php > generate-tree.php
+
+# Setup
+# ALTERNATIVELY: Open the script and edit the variables at the top.
+export BS_URL=https://bookstack.example.com # Set to be your BookStack base URL
+export BS_TOKEN_ID=abc123 # Set to be your API token_id
+export BS_TOKEN_SECRET=123abc # Set to be your API token_secret
+
+# Running the script
+php generate-tree.php
+```
+
+## Examples
+
+```bash
+# Generate out the tree to the command line
+php generate-tree.php
+
+# Generate & redirect output to a file
+php generate-tree.php > bookstack-tree.txt
+
+# Generate with the output shown on the command line and write to a file
+php generate-tree.php | tee bookstack-tree.txt
+```
+
+An example of the output can be seen in the [example.txt](./example.txt) file within the directory of this readme.
\ No newline at end of file