Skip to content

Commit 29922fc

Browse files
committed
Added go-export-page-content example
1 parent d22d2eb commit 29922fc

File tree

8 files changed

+403
-0
lines changed

8 files changed

+403
-0
lines changed

go-export-page-content/.gitignore

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
bookstack-export/
2+
page-export/
3+
bookstack-export
4+
bookstack-export.exe
5+
.idea/
6+
bin/

go-export-page-content/api.go

+140
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
package main
2+
3+
import (
4+
"crypto/tls"
5+
"encoding/json"
6+
"fmt"
7+
"io/ioutil"
8+
"net/http"
9+
"net/url"
10+
"strconv"
11+
"strings"
12+
)
13+
14+
type BookStackApi struct {
15+
BaseURL string
16+
TokenID string
17+
TokenSecret string
18+
}
19+
20+
func NewBookStackApi(baseUrl string, tokenId string, tokenSecret string) *BookStackApi {
21+
api := &BookStackApi{
22+
BaseURL: baseUrl,
23+
TokenID: tokenId,
24+
TokenSecret: tokenSecret,
25+
}
26+
27+
return api
28+
}
29+
30+
func (bs BookStackApi) authHeader() string {
31+
return fmt.Sprintf("Token %s:%s", bs.TokenID, bs.TokenSecret)
32+
}
33+
34+
func (bs BookStackApi) getRequest(method string, urlPath string, data map[string]string) *http.Request {
35+
method = strings.ToUpper(method)
36+
completeUrlStr := fmt.Sprintf("%s/api/%s", strings.TrimRight(bs.BaseURL, "/"), strings.TrimLeft(urlPath, "/"))
37+
38+
queryValues := url.Values{}
39+
for k, v := range data {
40+
queryValues.Add(k, v)
41+
}
42+
encodedData := queryValues.Encode()
43+
44+
r, err := http.NewRequest(method, completeUrlStr, strings.NewReader(encodedData))
45+
if err != nil {
46+
panic(err)
47+
}
48+
49+
r.Header.Add("Authorization", bs.authHeader())
50+
51+
if method != "GET" && method != "HEAD" {
52+
r.Header.Add("Content-Type", "application/x-www-form-urlencoded")
53+
r.Header.Add("Content-Length", strconv.Itoa(len(encodedData)))
54+
} else {
55+
r.URL.RawQuery = encodedData
56+
}
57+
58+
return r
59+
}
60+
61+
func (bs BookStackApi) doRequest(method string, urlPath string, data map[string]string) []byte {
62+
client := &http.Client{
63+
Transport: &http.Transport{
64+
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
65+
},
66+
}
67+
r := bs.getRequest(method, urlPath, data)
68+
res, err := client.Do(r)
69+
if err != nil {
70+
panic(err)
71+
}
72+
73+
defer res.Body.Close()
74+
75+
body, err := ioutil.ReadAll(res.Body)
76+
if err != nil {
77+
panic(err)
78+
}
79+
80+
return body
81+
}
82+
83+
func (bs BookStackApi) getFromListResponse(responseData []byte, models any) ListResponse {
84+
var response ListResponse
85+
86+
if err := json.Unmarshal(responseData, &response); err != nil {
87+
panic(err)
88+
}
89+
90+
if err := json.Unmarshal(response.Data, models); err != nil {
91+
panic(err)
92+
}
93+
94+
return response
95+
}
96+
97+
func (bs BookStackApi) GetBooks(count int, page int) ([]Book, int) {
98+
var books []Book
99+
100+
data := bs.doRequest("GET", "/books", getPagingParams(count, page))
101+
response := bs.getFromListResponse(data, &books)
102+
103+
return books, response.Total
104+
}
105+
106+
func (bs BookStackApi) GetChapters(count int, page int) ([]Chapter, int) {
107+
var chapters []Chapter
108+
109+
data := bs.doRequest("GET", "/chapters", getPagingParams(count, page))
110+
response := bs.getFromListResponse(data, &chapters)
111+
112+
return chapters, response.Total
113+
}
114+
115+
func (bs BookStackApi) GetPages(count int, page int) ([]Page, int) {
116+
var pages []Page
117+
118+
data := bs.doRequest("GET", "/pages", getPagingParams(count, page))
119+
response := bs.getFromListResponse(data, &pages)
120+
121+
return pages, response.Total
122+
}
123+
124+
func (bs BookStackApi) GetPage(id int) Page {
125+
var page Page
126+
127+
data := bs.doRequest("GET", fmt.Sprintf("/pages/%d", id), nil)
128+
if err := json.Unmarshal(data, &page); err != nil {
129+
panic(err)
130+
}
131+
132+
return page
133+
}
134+
135+
func getPagingParams(count int, page int) map[string]string {
136+
return map[string]string{
137+
"count": strconv.Itoa(count),
138+
"offset": strconv.Itoa(count * (page - 1)),
139+
}
140+
}

go-export-page-content/build.sh

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#!/bin/sh
2+
3+
GOOS=windows GOARCH=amd64 go build -ldflags "-s -w" -o bin/bookstack-export.exe
4+
GOOS=linux GOARCH=amd64 go build -ldflags "-s -w" -o bin/bookstack-export
5+
GOOS=darwin GOARCH=amd64 go build -ldflags "-s -w" -o bin/bookstack-export-macos
6+
7+
upx bin/*
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package main
2+
3+
import (
4+
"time"
5+
)
6+
7+
func getBookMap(api *BookStackApi) map[int]Book {
8+
var books []Book
9+
var byId = make(map[int]Book)
10+
11+
page := 1
12+
hasMoreBooks := true
13+
for hasMoreBooks {
14+
time.Sleep(time.Second / 2)
15+
newBooks, _ := api.GetBooks(200, page)
16+
hasMoreBooks = len(newBooks) == 200
17+
page++
18+
books = append(books, newBooks...)
19+
}
20+
21+
for _, book := range books {
22+
byId[book.Id] = book
23+
}
24+
25+
return byId
26+
}
27+
28+
func getChapterMap(api *BookStackApi) map[int]Chapter {
29+
var chapters []Chapter
30+
var byId = make(map[int]Chapter)
31+
32+
page := 1
33+
hasMoreChapters := true
34+
for hasMoreChapters {
35+
time.Sleep(time.Second / 2)
36+
newChapters, _ := api.GetChapters(200, page)
37+
hasMoreChapters = len(newChapters) == 200
38+
page++
39+
chapters = append(chapters, newChapters...)
40+
}
41+
42+
for _, chapter := range chapters {
43+
byId[chapter.Id] = chapter
44+
}
45+
46+
return byId
47+
}
48+
49+
func getPageMap(api *BookStackApi) map[int]Page {
50+
var pages []Page
51+
var byId = make(map[int]Page)
52+
53+
page := 1
54+
hasMorePages := true
55+
for hasMorePages {
56+
time.Sleep(time.Second / 2)
57+
newPages, _ := api.GetPages(200, page)
58+
hasMorePages = len(newPages) == 200
59+
page++
60+
pages = append(pages, newPages...)
61+
}
62+
63+
for _, page := range pages {
64+
byId[page.Id] = page
65+
}
66+
67+
return byId
68+
}

go-export-page-content/export.go

+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package main
2+
3+
import (
4+
"flag"
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
"time"
9+
)
10+
11+
func main() {
12+
13+
baseUrlPtr := flag.String("baseurl", "", "The base URL of your BookStack instance")
14+
tokenId := flag.String("tokenid", "", "Your BookStack API Token ID")
15+
tokenSecret := flag.String("tokensecret", "", "Your BookStack API Token Secret")
16+
exportDir := flag.String("exportdir", "./page-export", "The directory to store exported data")
17+
18+
flag.Parse()
19+
20+
if *baseUrlPtr == "" || *tokenId == "" || *tokenSecret == "" {
21+
panic("baseurl, tokenid and tokensecret arguments are required")
22+
}
23+
24+
api := NewBookStackApi(*baseUrlPtr, *tokenId, *tokenSecret)
25+
26+
// Grab all content from BookStack
27+
fmt.Println("Fetching books...")
28+
bookIdMap := getBookMap(api)
29+
fmt.Printf("Fetched %d books\n", len(bookIdMap))
30+
fmt.Println("Fetching chapters...")
31+
chapterIdMap := getChapterMap(api)
32+
fmt.Printf("Fetched %d chapters\n", len(chapterIdMap))
33+
fmt.Println("Fetching pages...")
34+
pageIdMap := getPageMap(api)
35+
fmt.Printf("Fetched %d pages\n", len(pageIdMap))
36+
37+
// Track progress when going through our pages
38+
pageCount := len(pageIdMap)
39+
currentCount := 1
40+
41+
// Cycle through each of our fetches pages
42+
for _, p := range pageIdMap {
43+
fmt.Printf("Exporting page %d/%d [%s]\n", currentCount, pageCount, p.Name)
44+
// Get the full page content
45+
fullPage := api.GetPage(p.Id)
46+
47+
// Work out a book+chapter relative path
48+
book := bookIdMap[fullPage.BookId]
49+
path := book.Slug
50+
if chapter, ok := chapterIdMap[fullPage.ChapterId]; ok {
51+
path = "/" + chapter.Slug
52+
}
53+
54+
// Get the html, or markdown, content from our page along with the file name
55+
// based upon the page slug
56+
content := fullPage.Html
57+
fName := fullPage.Slug + ".html"
58+
if fullPage.Markdown != "" {
59+
content = fullPage.Markdown
60+
fName = fullPage.Slug + ".md"
61+
}
62+
63+
// Create our directory path
64+
absExportPath, err := filepath.Abs(*exportDir)
65+
if err != nil {
66+
panic(err)
67+
}
68+
69+
absPath := filepath.Join(absExportPath, path)
70+
err = os.MkdirAll(absPath, 0744)
71+
if err != nil {
72+
panic(err)
73+
}
74+
75+
// Write the content to the filesystem
76+
fPath := filepath.Join(absPath, fName)
77+
err = os.WriteFile(fPath, []byte(content), 0644)
78+
if err != nil {
79+
panic(err)
80+
}
81+
82+
// Wait to avoid hitting rate limits
83+
time.Sleep(time.Second / 4)
84+
currentCount++
85+
}
86+
87+
}

go-export-page-content/go.mod

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module bookstack-export
2+
3+
go 1.18

go-export-page-content/models.go

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"time"
6+
)
7+
8+
type ListResponse struct {
9+
Data json.RawMessage `json:"data"`
10+
Total int `json:"total"`
11+
}
12+
13+
type Book struct {
14+
Id int `json:"id"`
15+
Name string `json:"name"`
16+
Slug string `json:"slug"`
17+
Description string `json:"description"`
18+
CreatedAt time.Time `json:"created_at"`
19+
UpdatedAt time.Time `json:"updated_at"`
20+
CreatedBy int `json:"created_by"`
21+
UpdatedBy int `json:"updated_by"`
22+
OwnedBy int `json:"owned_by"`
23+
ImageId int `json:"image_id"`
24+
}
25+
26+
type Chapter struct {
27+
Id int `json:"id"`
28+
BookId int `json:"book_id"`
29+
Name string `json:"name"`
30+
Slug string `json:"slug"`
31+
Description string `json:"description"`
32+
Priority int `json:"priority"`
33+
CreatedAt string `json:"created_at"`
34+
UpdatedAt time.Time `json:"updated_at"`
35+
CreatedBy int `json:"created_by"`
36+
UpdatedBy int `json:"updated_by"`
37+
OwnedBy int `json:"owned_by"`
38+
}
39+
40+
type Page struct {
41+
Id int `json:"id"`
42+
BookId int `json:"book_id"`
43+
ChapterId int `json:"chapter_id"`
44+
Name string `json:"name"`
45+
Slug string `json:"slug"`
46+
Html string `json:"html"`
47+
Priority int `json:"priority"`
48+
CreatedAt time.Time `json:"created_at"`
49+
UpdatedAt time.Time `json:"updated_at"`
50+
Draft bool `json:"draft"`
51+
Markdown string `json:"markdown"`
52+
RevisionCount int `json:"revision_count"`
53+
Template bool `json:"template"`
54+
}

0 commit comments

Comments
 (0)