Skip to content

Commit b00c034

Browse files
authored
Develop (#17)
New options: --folders, --docx
1 parent 9eeecb9 commit b00c034

11 files changed

+184
-106
lines changed

README.md

+23-12
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,31 @@
11
# Quip-Export
22
Comprehensive full automated export (backup) tool for [Quip](https://quip.com/).
33

4-
Quip-Export uses official [Quip Automation API](https://quip.com/dev/automation/documentation) and exports all folders and referenced files from an Quip-Account.
5-
The documents are exported as HTML files with original Quip styling.
6-
All referenced files and images in Quip document are saved in 'blobs' folder.
7-
Quip-Export allows optionally to export in a zip-file.
4+
Quip-Export uses official [Quip Automation API](https://quip.com/dev/automation/documentation) and exports all documents and folders from an Quip-Account.
85

9-
Slides are not supported (due to poor quality of generated PDFs).
6+
Features:
107

11-
Sometimes the export process goes not smooth and fast enough due to Quip API rate limits (HTTP 503: Over Rate Limit).
8+
* Export in HTML format with original Quip styling
9+
* Export in MS Office format: .docx for documents, .xlsx for spresdsheets _(--docx)_
10+
* Embedded CSS for HTML-export _(--embedded-styles)_
11+
* Embedded images for HTML-export _(--embedded-images)_
12+
* Export of comments and conversations for HTML-export _(--comments)_
13+
* Export of specific folders only _(--folders)_
14+
* Export of referenced files in documents
15+
* Resolving of references between folders and documents to relative paths
1216

13-
Despite Quip-Export is designed as a standalone tool for Node.js environment, it could be also used as Quip export library for any kind of JavaScript applications.
14-
In that case, the Quip-Export project is a good usage example.
17+
Slides are not supported (due to poor quality of generated PDFs). Export in PDF are not supported (due to poor quality of generated PDFs).
18+
19+
Despite Quip-Export is designed as a standalone tool for Node.js environment, it could be also used as Quip export library for any kind of JavaScript applications. In that case, the Quip-Export project is a good usage example.
1520

16-
Quip-Export perfectly works on Windows, Mac OS and Linux/Unix in Node.js or JavaScript (browser) environment.
21+
Quip-Export perfectly works on Windows, Mac OS and Linux/Unix in Node.js or in pure browser environment.
1722

1823
<p align="center">
1924
<img src="https://raw.githubusercontent.com/sonnenkern/quip-export/master/public/example-anim.gif">
2025
</p>
2126

2227
## Online web app and demo
23-
Full featured web app using Quip-Export package with demo mode is available on [www.quip-export.com](https://www.quip-export.com)
28+
Full featured web app using Quip-Export npm package with demo mode is available on [www.quip-export.com](https://www.quip-export.com)
2429

2530
<p align="center">
2631
<img src="https://raw.githubusercontent.com/sonnenkern/quip-export/master/public/demo.gif">
@@ -78,17 +83,23 @@ npm install quip-export
7883
```
7984
-h, --help Usage guide.
8085
-v, --version Print version info
81-
-t, --token string Quip Access Token.
82-
-d, --destination string Destination folder for export files
86+
-t, --token "string" Quip Access Token.
87+
-d, --destination "string" Destination folder for export files
8388
-z, --zip Zip export files
8489
--embedded-styles Embedded in each document stylesheet
8590
--embedded-images Embedded images
91+
--docx Exports documents in MS-Office format (*.docx , *.xlsx)
8692
--comments Includes comments (messages) for the documents
93+
--folders "string" Comma-separated folder's IDs to export
8794
--debug Extended logging
8895
```
8996

9097
To generate a personal access token, visit the page: [https://quip.com/dev/token](https://quip.com/dev/token)
9198

99+
Be aware, the options --comments, --embedded-images, --embedded-styles don't work together with export in MS-Office format (--docx) and will be ignored.
100+
101+
The easiest way to get to know ID of Quip fodler is just to open the folder in Quip web application in browser and look at adress line. For example the adress "https://quip.com/bGG333444111" points to the folder with ID "bGG333444111".
102+
92103
## Usage examples
93104
Export to folder c:\temp
94105
```

__tests__/app.test.js

+18-2
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ function initApp() {
5151
token: 'TOKEN',
5252
['embedded-styles']: true,
5353
['embedded-images']: true,
54-
['messages']: true
54+
['comments']: true,
55+
['docx']: true
5556
});
5657
QuipService.mockImplementation(() => {
5758
return {
@@ -165,7 +166,8 @@ describe('main() tests', () => {
165166
documentTemplate,
166167
documentCSS: documentCSS,
167168
embeddedImages: true,
168-
messages: true
169+
comments: true,
170+
docx: true
169171
}
170172
);
171173
expect(app.quipProcessor.setLogger).toHaveBeenCalledWith(app.Logger);
@@ -213,6 +215,20 @@ describe('main() tests', () => {
213215

214216
expect(console.log).toHaveBeenCalledWith("Zip-file has been saved: ", path.join(app.desinationFolder, 'quip-export.zip'));
215217
});
218+
219+
test('folders option', async () => {
220+
const folders = ['111','222'];
221+
CliArguments.mockReturnValue({
222+
destination: 'c:/temp',
223+
token: 'TOKEN',
224+
['embedded-styles']: false,
225+
['embedded-images']: false,
226+
zip: false,
227+
folders
228+
});
229+
await app.main();
230+
expect(app.quipProcessor.startExport).toHaveBeenCalledWith(folders);
231+
});
216232
});
217233

218234
describe('fileSaver() tests', () => {

app.js

+16-5
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,6 @@ class App {
127127
//current folder as destination, if not set
128128
this.desinationFolder = (this.cliArguments.destination || process.cwd());
129129

130-
131130
if(this.cliArguments.debug) {
132131
this.Logger = new PinoLogger(PinoLogger.LEVELS.DEBUG, `${this.desinationFolder}/export.log`);
133132
} else {
@@ -140,6 +139,10 @@ class App {
140139
utils.cliBox(`!!!! A new version of Quip-Export (v${versionInfo.remoteVersion}) is available.`);
141140
}
142141

142+
if(this.cliArguments['comments'] && this.cliArguments['docx']) {
143+
console.log('Docx export: comments option will be ignored.');
144+
}
145+
143146
//Token verification
144147
const quipService = new QuipService(this.cliArguments.token);
145148
quipService.setLogger(this.Logger);
@@ -161,11 +164,13 @@ class App {
161164
documentTemplate,
162165
documentCSS: this.cliArguments['embedded-styles']? documentCSS : '',
163166
embeddedImages: this.cliArguments['embedded-images'],
164-
comments: this.cliArguments['comments']
167+
comments: this.cliArguments['comments'],
168+
docx: this.cliArguments['docx']
165169
});
170+
166171
this.quipProcessor.setLogger(this.Logger);
167172

168-
if(!this.cliArguments['embedded-styles']) {
173+
if(!this.cliArguments['embedded-styles'] && !this.cliArguments['docx']) {
169174
if(this.cliArguments.zip) {
170175
this.zip.file('document.css', documentCSS);
171176
} else {
@@ -176,11 +181,17 @@ class App {
176181
let foldersToExport = [
177182
//'FOLDER-1'
178183
//'FOLDER-2'
179-
//'EVZAOAW2e6U'
180-
//'UPWAOAAEpFn' //Test
184+
//'EVZAOAW2e6U',
185+
//'UPWAOAAEpFn', //Test
181186
//'bGGAOAKTL4Y' //Test/folder1
187+
//'EJCAOAdY90Y', // Design patterns
188+
//'NBaAOAhFXJJ' //React
182189
];
183190

191+
if(this.cliArguments['folders']) {
192+
foldersToExport = this.cliArguments['folders'];
193+
}
194+
184195
await this.quipProcessor.startExport(foldersToExport);
185196

186197
this.Logger.debug(this.quipProcessor.quipService.stats);

lib/QuipProcessor.js

+28-34
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,20 @@ class QuipProcessor {
280280
return `<div class='messagesBlock'>${html}</div>`;
281281
}
282282

283+
async _processDocumentThreadDocx(quipThread, path) {
284+
const docx = await this.quipService.getDocx(quipThread.thread.id);
285+
if(docx) {
286+
this.saveCallback(docx, sanitizeFilename(`${quipThread.thread.title.trim()}.docx`), 'BLOB', path);
287+
}
288+
}
289+
290+
async _processDocumentThreadXlsx(quipThread, path) {
291+
const xlsx = await this.quipService.getXlsx(quipThread.thread.id);
292+
if(xlsx) {
293+
this.saveCallback(xlsx, sanitizeFilename(`${quipThread.thread.title.trim()}.xlsx`), 'BLOB', path);
294+
}
295+
}
296+
283297
async _processDocumentThread(quipThread, path) {
284298
const pathDeepness = path.split("/").length-1;
285299
let threadHtml = quipThread.html;
@@ -314,53 +328,32 @@ class QuipProcessor {
314328
this.saveCallback(wrappedHtml, sanitizeFilename(`${quipThread.thread.title.trim()}.html`), 'THREAD', path);
315329
}
316330

317-
async _processSlidesThread(quipThread, path) {
318-
const blob = await this.quipService.getPdf(quipThread.thread.id);
319-
if(blob) {
320-
const fileName = sanitizeFilename(`${quipThread.thread.title.trim()}.pdf`);
321-
this.saveCallback(blob, fileName, "BLOB", `${path}`);
322-
} else {
323-
this.logger.warn("Can't load Slides as PDF, thread.id=" + quipThread.thread.id +
324-
", thread.title=" + quipThread.thread.title +
325-
", thread.type=" + quipThread.thread.type + ", path=" + path);
326-
}
327-
}
328-
329-
async _processSpreadsheetThread(quipThread, path) {
330-
const blob = await this.quipService.getXlsx(quipThread.thread.id);
331-
if(blob) {
332-
const fileName = sanitizeFilename(`${quipThread.thread.title.trim()}.xlsx`);
333-
this.saveCallback(blob, fileName, "BLOB", `${path}`);
334-
} else {
335-
this.logger.warn("Can't load Spreadsheet as PDF, thread.id=" + quipThread.thread.id +
336-
", thread.title=" + quipThread.thread.title +
337-
", thread.type=" + quipThread.thread.type + ", path=" + path);
338-
}
339-
}
340-
341331
async _processThread(quipThread, path) {
332+
this.threadsProcessed++;
333+
342334
if(!quipThread.thread) {
343335
const quipThreadCopy = Object.assign({}, quipThread);
344336
quipThreadCopy.html = '...';
345337
this.logger.error("quipThread.thread is not defined, thread=" + JSON.stringify(quipThreadCopy, null, 2) + ", path=" + path);
346338
return;
347339
}
348340

349-
if(['document', 'spreadsheet'].includes(quipThread.thread.type)) {
350-
await this._processDocumentThread(quipThread, path);
351-
}
352-
/*else if(quipThread.thread.type === 'slides') {
353-
this._processSlidesThread(quipThread, path);
354-
} else if(quipThread.thread.type === 'spreadsheet') {
355-
this._processSpreadsheetThread(quipThread, path);
356-
}*/
357-
else {
341+
if(!['document', 'spreadsheet'].includes(quipThread.thread.type)) {
358342
this.logger.warn("Thread type is not supported, thread.id=" + quipThread.thread.id +
359343
", thread.title=" + quipThread.thread.title +
360344
", thread.type=" + quipThread.thread.type + ", path=" + path);
345+
return;
361346
}
362347

363-
this.threadsProcessed++;
348+
if(this.options.docx) {
349+
if(quipThread.thread.type === 'document') {
350+
await this._processDocumentThreadDocx(quipThread, path);
351+
} else {
352+
await this._processDocumentThreadXlsx(quipThread, path);
353+
}
354+
} else {
355+
await this._processDocumentThread(quipThread, path);
356+
}
364357
}
365358

366359
async _processFile(html, file, path, asImage=false) {
@@ -513,6 +506,7 @@ class QuipProcessor {
513506
}
514507

515508
for(const index in quipFolders) {
509+
this.foldersTotal++;
516510
await this._countThreadsAndFolders(quipFolders[index], "");
517511
}
518512

lib/QuipService.js

+6
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ class QuipService {
1515
getBlob_count: 0,
1616
getPdf_count: 0,
1717
getXlsx_count: 0,
18+
getDocx_count: 0,
1819
getCurrentUser_count: 0,
1920
getThreadMessages_count: 0,
2021
getUser_count: 0
@@ -80,6 +81,11 @@ class QuipService {
8081
return this._apiCallBlob(`/threads/${threadId}/export/pdf`);
8182
}
8283

84+
async getDocx(threadId) {
85+
this.stats.getDocx_count++;
86+
return this._apiCallBlob(`/threads/${threadId}/export/docx`);
87+
}
88+
8389
async getXlsx(threadId) {
8490
this.stats.getXlsx_count++;
8591
return this._apiCallBlob(`/threads/${threadId}/export/xlsx`);

0 commit comments

Comments
 (0)