forked from bookstack/api-scripts
Added php-generate-tree script
This commit is contained in:
parent 8e94a530b0
commit 28f1407c82
3 changed files with 245 additions and 0 deletions
9 php-generate-tree/example.txt Normal file
9
php-generate-tree/example.txt Normal file | @ -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 |
189 php-generate-tree/generate-tree.php Executable file
189
php-generate-tree/generate-tree.php Executable file | @ -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); | ||||
} |
47 php-generate-tree/readme.md Normal file
47
php-generate-tree/readme.md Normal file | @ -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. |
Loading…
Add table
Add a link
Reference in a new issue