Skip to content

Commit 28f1407

Browse files
committed
Added php-generate-tree script
1 parent 8e94a53 commit 28f1407

File tree

3 files changed

+245
-0
lines changed

3 files changed

+245
-0
lines changed

php-generate-tree/example.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
├── BOOKSHELF 1: My wonderful shelf of notes
2+
│ └── BOOK 39: My lovely book in my notes
3+
│ ├── PAGE 2745: A page within the book
4+
│ ├── CHAPTER 643: A lone chapter
5+
│ └── CHAPTER 644: My chapter with page
6+
│ └── PAGE 47830: My new great page
7+
├── BOOK 239: Scratch notes
8+
│ ├── PAGE 47870: Note A
9+
│ └── PAGE 47872: Note B
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
#!/usr/bin/env php
2+
<?php
3+
4+
// API Credentials
5+
// You can either provide them as environment variables
6+
// or hard-code them in the empty strings below.
7+
$baseUrl = getenv('BS_URL') ?: '';
8+
$clientId = getenv('BS_TOKEN_ID') ?: '';
9+
$clientSecret = getenv('BS_TOKEN_SECRET') ?: '';
10+
11+
// Script logic
12+
////////////////
13+
14+
// Define the time we wait in between making API requests,
15+
// to help keep within rate limits and avoid exhausting resources.
16+
$apiPauseMicrosecs = 100;
17+
18+
// Clean up the base path
19+
$baseUrl = rtrim($baseUrl, '/');
20+
21+
// Get all items from the system keyed by ID
22+
$shelvesById = keyById(getAllOfAtListEndpoint("api/shelves", []));
23+
$booksById = keyById(getAllOfAtListEndpoint("api/books", []));
24+
25+
// Fetch books that are on each shelf
26+
foreach ($shelvesById as $id => $shelf) {
27+
$shelvesById[$id]['books'] = getBooksForShelf($id);
28+
usleep($apiPauseMicrosecs);
29+
}
30+
31+
// For each book, fetch its contents list
32+
foreach ($booksById as $id => $book) {
33+
$booksById[$id]['contents'] = apiGetJson("api/books/{$id}")['contents'] ?? [];
34+
usleep($apiPauseMicrosecs);
35+
}
36+
37+
// Cycle through the shelves and display their contents
38+
$isBookShownById = [];
39+
foreach ($shelvesById as $id => $shelf) {
40+
output($shelf, 'bookshelf', [false]);
41+
$bookCount = count($shelf['books']);
42+
for ($i=0; $i < $bookCount; $i++) {
43+
$bookId = $shelf['books'][$i];
44+
$book = $booksById[$bookId] ?? null;
45+
if ($book) {
46+
outputBookAndContents($book, [false, $i === $bookCount - 1]);
47+
$isBookShownById[strval($book['id'])] = true;
48+
}
49+
}
50+
}
51+
52+
// Cycle through books and display any that have not been
53+
// part of a shelve's output
54+
foreach ($booksById as $id => $book) {
55+
if (isset($isBookShownById[$id])) {
56+
continue;
57+
}
58+
59+
outputBookAndContents($book, [false]);
60+
}
61+
62+
/**
63+
* Output a book for display, along with its contents.
64+
*/
65+
function outputBookAndContents(array $book, array $depthPath): void
66+
{
67+
output($book, 'book', $depthPath);
68+
$childCount = count($book['contents']);
69+
for ($i=0; $i < $childCount; $i++) {
70+
$child = $book['contents'][$i];
71+
$childPath = array_merge($depthPath, [($i === $childCount - 1)]);
72+
output($child, $child['type'], $childPath);
73+
$pages = $child['pages'] ?? [];
74+
$pageCount = count($pages);
75+
for ($j=0; $j < count($pages); $j++) {
76+
$page = $pages[$j];
77+
$innerPath = array_merge($childPath, [($j === $pageCount - 1)]);
78+
output($page, 'page', $innerPath);
79+
}
80+
}
81+
}
82+
83+
/**
84+
* Output a single item for display.
85+
*/
86+
function output(array $item, string $type, array $depthPath): void
87+
{
88+
$upperType = strtoupper($type);
89+
$prefix = '';
90+
$depth = count($depthPath);
91+
for ($i=0; $i < $depth; $i++) {
92+
$isLastAtDepth = $depthPath[$i];
93+
$end = ($i === $depth - 1);
94+
if ($end) {
95+
$prefix .= $isLastAtDepth ? '' : '';
96+
} else {
97+
$prefix .= $isLastAtDepth ? ' ' : '';
98+
}
99+
}
100+
echo $prefix . "── {$upperType} {$item['id']}: {$item['name']}\n";
101+
}
102+
103+
/**
104+
* Key an array of array-based data objects by 'id' value.
105+
*/
106+
function keyById(array $data): array
107+
{
108+
$byId = [];
109+
foreach ($data as $item) {
110+
$id = $item['id'];
111+
$byId[$id] = $item;
112+
}
113+
return $byId;
114+
}
115+
116+
/**
117+
* Get the books for the given shelf ID.
118+
* Returns an array of the book IDs.
119+
*/
120+
function getBooksForShelf(int $shelfId): array
121+
{
122+
$resp = apiGetJson("api/shelves/{$shelfId}");
123+
return array_map(function ($bookData) {
124+
return $bookData['id'];
125+
}, $resp['books'] ?? []);
126+
}
127+
128+
/**
129+
* Consume all items from the given API listing endpoint.
130+
*/
131+
function getAllOfAtListEndpoint(string $endpoint, array $params): array
132+
{
133+
global $apiPauseMicrosecs;
134+
$count = 100;
135+
$offset = 0;
136+
$all = [];
137+
138+
do {
139+
$endpoint = $endpoint . '?' . http_build_query(array_merge($params, ['count' => $count, 'offset' => $offset]));
140+
$resp = apiGetJson($endpoint);
141+
142+
$total = $resp['total'] ?? 0;
143+
$new = $resp['data'] ?? [];
144+
array_push($all, ...$new);
145+
$offset += $count;
146+
usleep($apiPauseMicrosecs);
147+
} while ($offset < $total);
148+
149+
return $all;
150+
}
151+
152+
/**
153+
* Make a simple GET HTTP request to the API.
154+
*/
155+
function apiGet(string $endpoint): string
156+
{
157+
global $baseUrl, $clientId, $clientSecret;
158+
$url = rtrim($baseUrl, '/') . '/' . ltrim($endpoint, '/');
159+
$opts = ['http' => ['header' => "Authorization: Token {$clientId}:{$clientSecret}"]];
160+
$context = stream_context_create($opts);
161+
return @file_get_contents($url, false, $context);
162+
}
163+
164+
/**
165+
* Make a simple GET HTTP request to the API &
166+
* decode the JSON response to an array.
167+
*/
168+
function apiGetJson(string $endpoint): array
169+
{
170+
$data = apiGet($endpoint);
171+
$array = json_decode($data, true);
172+
173+
if (!is_array($array)) {
174+
dd("Failed request to {$endpoint}", $data);
175+
}
176+
177+
return $array;
178+
}
179+
180+
/**
181+
* DEBUG: Dump out the given variables and exit.
182+
*/
183+
function dd(...$args)
184+
{
185+
foreach ($args as $arg) {
186+
var_dump($arg);
187+
}
188+
exit(1);
189+
}

php-generate-tree/readme.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Generate Tree
2+
3+
This script will scan through all pages, chapters books and shelves via the API to generate a big tree structure list in plaintext.
4+
5+
**This is a very simplistic single-script-file example of using the endpoints API together**
6+
, it is not a fully-featured & validated script, it error handling is very limited.
7+
8+
Keep in mind, The tree generated will reflect content visible to the API user used when running the script.
9+
10+
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.
11+
12+
## Requirements
13+
14+
You will need php (~8.1+) installed on the machine you want to run this script on.
15+
You will also need BookStack API credentials (TOKEN_ID & TOKEN_SECRET) at the ready.
16+
17+
## Running
18+
19+
```bash
20+
# Downloading the script
21+
# ALTERNATIVELY: Clone the project from GitHub and run locally.
22+
curl https://raw.githubusercontent.com/BookStackApp/api-scripts/main/php-generate-tree/generate-tree.php > generate-tree.php
23+
24+
# Setup
25+
# ALTERNATIVELY: Open the script and edit the variables at the top.
26+
export BS_URL=https://bookstack.example.com # Set to be your BookStack base URL
27+
export BS_TOKEN_ID=abc123 # Set to be your API token_id
28+
export BS_TOKEN_SECRET=123abc # Set to be your API token_secret
29+
30+
# Running the script
31+
php generate-tree.php
32+
```
33+
34+
## Examples
35+
36+
```bash
37+
# Generate out the tree to the command line
38+
php generate-tree.php
39+
40+
# Generate & redirect output to a file
41+
php generate-tree.php > bookstack-tree.txt
42+
43+
# Generate with the output shown on the command line and write to a file
44+
php generate-tree.php | tee bookstack-tree.txt
45+
```
46+
47+
An example of the output can be seen in the [example.txt](./example.txt) file within the directory of this readme.

0 commit comments

Comments
 (0)