diff --git a/README.md b/README.md index e4189f1..48b6849 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,13 @@ This repository contains different examples of BookStack API scripts that you might find useful to use or modify. - Each folder within this repo is a different script. Each script has it's own readme. Click into a folder to see the readme for detail about the script. +_These scripts are not part an officially supported part of the BookStack project itself and therefore may be outdated or more likely to have bugs._ + +## Contributing -*These scripts are not part an officially supported part of the BookStack project itself and therefore may be outdated or more likely to have bugs.* +This repository is only meant to provide starting-off points as examples. Feel free to contribute fixes for existing scripts where needed, but we're relatively strict about keeping scope limited to ease maintenance so we're not looking to expand features of the scripts directly contained in this repository. If you build upon a script and want to share that back, feel free to open a PR to add it to the "Community Projects & Scripts" section below. We may be open to new scripts but only where the scope is fixed & narrow, and where there's value in something new it's demonstrating, otherwise PRs may be rejected. If you're unsure feel free to open an issue first to query a new addition before spending time on building out a script. ## Community Projects & Scripts @@ -24,4 +26,20 @@ A Python wrapper for the BookStack API. ### [Szwendacz99/BookStack-Python-exporter](https://github.com/Szwendacz99/BookStack-Python-exporter) [Python] -A customizable Python script for exporting notes from BookStack through API. \ No newline at end of file +A customizable Python script for exporting notes from BookStack through API. + +### [My-Random-Thoughts/psBookStack](https://github.com/My-Random-Thoughts/psBookStack) [PowerShell] + +A PowerShell module for interacting with BookStack via the API. + +### [chris-devel0per/FlutterBookstackApiExample](https://github.com/chris-devel0per/FlutterBookstackApiExample) [Flutter/Dart] + +A Flutter & Dart example project which showcases the use of many BookStack API endpoints. + +### [IceWreck/BookStack2Site](https://github.com/IceWreck/BookStack2Site) [Go] + +CLI tool which generates static sites (using Mdbook) from Bookstack Wikis. + +### [jaypyles/obisidan_to_bookstack](https://github.com/jaypyles/obsidian-to-bookstack) [Python] + +CLI tool to update and sync a Bookstack instance with an Obsidian Vault diff --git a/php-export-all-books/export-books.php b/php-export-all-books/export-books.php index a0f9ff9..d2561b7 100644 --- a/php-export-all-books/export-books.php +++ b/php-export-all-books/export-books.php @@ -17,15 +17,22 @@ // Script logic //////////////// +// Get all list of all books in the system $books = getAllBooks(); +// Get a reference to our output location $outDir = realpath($exportLocation); +// Mapping for export formats to the resulting export file extensions $extensionByFormat = [ 'pdf' => 'pdf', 'html' => 'html', 'plaintext' => 'txt', + 'markdown' => 'md', ]; +// Loop over each book, exporting each one-by-one and saving its +// contents into the output location, using the books slug as +// the file name. foreach ($books as $book) { $id = $book['id']; $extension = $extensionByFormat[$exportFormat] ?? $exportFormat; @@ -37,7 +44,7 @@ /** * Get all books from the system API. */ -function getAllBooks() { +function getAllBooks(): array { $count = 100; $offset = 0; $total = 0; @@ -47,12 +54,7 @@ function getAllBooks() { $endpoint = 'api/books?' . http_build_query(['count' => $count, 'offset' => $offset]); $resp = apiGetJson($endpoint); - // Only set total on first request, due to API bug: - // https://github.com/BookStackApp/BookStack/issues/2043 - if ($offset == 0) { - $total = $resp['total'] ?? 0; - } - + $total = $resp['total'] ?? 0; $newBooks = $resp['data'] ?? []; array_push($allBooks, ...$newBooks); $offset += $count; diff --git a/php-export-all-books/readme.md b/php-export-all-books/readme.md index b076a39..fbb0cf5 100644 --- a/php-export-all-books/readme.md +++ b/php-export-all-books/readme.md @@ -1,6 +1,6 @@ # Export All Books -This script will export all books in your preferred format (PDF, HTML or TXT). +This script will export all books in your preferred format (PDF, HTML, Markdown or TXT). ## Requirements @@ -34,4 +34,7 @@ php export-books.php pdf ./ # Export as HTML to an existing "html" directory php export-books.php html ./html + +# Export as Markdown to an existing "md-files" directory +php export-books.php markdown ./md-files ``` \ No newline at end of file diff --git a/php-generate-tree/example.txt b/php-generate-tree/example.txt new file mode 100644 index 0000000..aabf7ac --- /dev/null +++ b/php-generate-tree/example.txt @@ -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 index 0000000..76dc316 --- /dev/null +++ b/php-generate-tree/generate-tree.php @@ -0,0 +1,189 @@ +#!/usr/bin/env php + $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 index 0000000..a054297 --- /dev/null +++ b/php-generate-tree/readme.md @@ -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