forked from yannbolliger/notion-exporter
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathNotionExporter.ts
169 lines (155 loc) · 5.18 KB
/
NotionExporter.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
import axios, { AxiosInstance } from "axios"
import AdmZip from "adm-zip"
import { blockIdFromUrl, validateUuid } from "./blockId"
interface Task {
id: string
state: string
status: { exportURL?: string }
}
/** Lightweight client to export ZIP, Markdown or CSV files from a Notion block/page. */
export class NotionExporter {
protected readonly client: AxiosInstance
private readonly recursiveExport: boolean
private readonly noFilesIncluded: boolean
/**
* Create a new NotionExporter client. To export any blocks/pages from
* Notion.so one needs to provide the token of a user who has read access to
* the corresponding pages.
*
* @param tokenV2 – the Notion `token_v2` Cookie value
* @param fileToken – the Notion `file_token` Cookie value
*/
constructor(
tokenV2: string,
fileToken: string,
noFiles: boolean,
recursive: boolean = false
) {
this.client = axios.create({
baseURL: "https://www.notion.so/api/v3/",
headers: {
Cookie: `token_v2=${tokenV2};file_token=${fileToken}`,
},
})
this.recursiveExport = recursive
this.noFilesIncluded = noFiles
}
/**
* Adds a an 'exportBlock' task to the Notion API's queue of tasks.
*
* @param idOrUrl BlockId or URL of the page/block/DB to export
* @returns The task's id
*/
async getTaskId(idOrUrl: string): Promise<string> {
const id = validateUuid(blockIdFromUrl(idOrUrl))
if (!id) return Promise.reject(`Invalid URL or blockId: ${idOrUrl}`)
const exportOptions = Object.assign(
{
exportType: "markdown",
timeZone: "Europe/Zurich",
locale: "en",
collectionViewExportType: "currentView",
},
this.noFilesIncluded ? { includeContents: "no_files" } : {}
)
console.error(this.recursiveExport, exportOptions)
const res = await this.client.post("enqueueTask", {
task: {
eventName: "exportBlock",
request: {
block: { id },
recursive: this.recursiveExport,
exportOptions,
},
},
})
return res.data.taskId
}
private getTask = async (taskId: string): Promise<Task> => {
const res = await this.client.post("getTasks", { taskIds: [taskId] })
return res.data.results.find((t: Task) => t.id === taskId)
}
private pollTask = (
taskId: string,
pollInterval: number = 5000
): Promise<string> =>
new Promise((resolve, reject) => {
const poll = async () => {
const task = await this.getTask(taskId)
if (task.state === "success" && task.status.exportURL)
resolve(task.status.exportURL)
else if (task.state === "in_progress" || task.state === "not_started") {
setTimeout(poll, pollInterval)
} else {
console.error(taskId, task)
reject(`Export task failed: ${taskId}.`)
}
}
setTimeout(poll, pollInterval)
})
/**
* Starts an export of the given block and
*
* @param idOrUrl BlockId or URL of the page/block/DB to export
* @returns The URL of the exported ZIP archive
*/
getZipUrl = (idOrUrl: string): Promise<string> =>
this.getTaskId(idOrUrl).then(this.pollTask)
/**
* Downloads the ZIP at the given URL.
*
* @returns The ZIP as an 'AdmZip' object
*/
getZip = async (url: string): Promise<AdmZip> => {
const res = await this.client.get(url, { responseType: "arraybuffer" })
return new AdmZip(res.data)
}
/**
* Exports the given block as ZIP and downloads it. Returns the matched file
* in the ZIP as a string.
*
* @param idOrUrl BlockId or URL of the page/block/DB to export
* @param predicate - Returns true for the zip entry to be extracted
* @returns The matched file as string
*/
async getFileString(
idOrUrl: string,
predicate: (entry: AdmZip.IZipEntry) => boolean
): Promise<string> {
const zip = await this.getZipUrl(idOrUrl).then(this.getZip)
const entry = zip.getEntries().find(predicate)
const payload: string | undefined = entry?.getData().toString().trim()
return payload || Promise.reject("Could not find file in ZIP.")
}
/**
* Downloads and extracts the first CSV file of the exported block as string.
*
* @param idOrUrl BlockId or URL of the page/block/DB to export
* @returns The extracted CSV string
*/
getCsvString = (
idOrUrl: string,
onlyCurrentView?: boolean
): Promise<string> =>
this.getFileString(idOrUrl, (e) =>
e.name.endsWith(onlyCurrentView ? ".csv" : "_all.csv")
)
/**
* Downloads and extracts the first Markdown file of the exported block as string.
*
* @param idOrUrl BlockId or URL of the page/block/DB to export
* @returns The extracted Markdown string
*/
getMdString = (idOrUrl: string): Promise<string> =>
this.getFileString(idOrUrl, (e) => e.name.endsWith(".md"))
/**
* Downloads ane extracts into a folder all files in the exported zip file.
*
* @param idOrUrl BlockId or URL of the page/block/DB to export
* @param folder The folder where the files are going to be unzipped
*/
getMdFiles = async (idOrUrl: string, folder: string): Promise<void> => {
const zip = await this.getZipUrl(idOrUrl).then(this.getZip)
zip.extractAllTo(folder)
}
}