commit 4fe23e688f1cb66fe83948f1b3c912b62ebc5212 Author: Simon C Date: Thu Jan 27 12:41:28 2022 +0100 feat: Create library diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/README.md b/README.md new file mode 100644 index 0000000..492c7f3 --- /dev/null +++ b/README.md @@ -0,0 +1,91 @@ +# Directus To Markdown + +This library export [Directus](https://directus.io) items collections to markdown files with assets. + +I used it to export article from Directus to Hugo website. + +## Configuration + +### Directus + +This library export data from an Directus so you should specify an url and token. + +With environment variables: + +``` +export DIRECTUS_URL=https://your.directus.url +export DIRECTUS_TOKEN=your-token +``` + +or on configuration parameters : + +```js +const config = { + url: 'https://your.directus.url', + token: 'your-token', + ... +} +``` + +### Collection Name + +The key of collections object should be the name of Directus Collection : + +```js +const config = { + collections: { + news: { ... }, + pages: { ... } + } +} +``` + +### Content key + +_default: content_ + +You can modify the field of the content : + +```js +const config = { + contentKey: 'body', + ... +} +``` + +### readManyOption + +`readManyOption` match https://docs.directus.io/reference/sdk/#read-multiple-items + +### Export to specific path + +For each collection you should an `pathBuilder`. + +## Example + +```js +import DirectusToMarkdown from '@resilien/directus-to-markdown' +import urlslug from 'url-slug' + +const config = { + url: 'https://your.directus.url', + token: 'your-token', + contentKey: 'body', + collections: { + news: { + readManyOption: { + fields: ['title', 'slug', 'date', 'image', 'image_credit', 'draft', 'body'], + filter: { draft: { _eq: 'false' } } + }, + pathBuilder: (article) => { + if (article.slug) { + return `./content/news/${article.slug}` + } + return `./content/news/${article.date}-${urlslug(article.title, { remove: /\./g })}`; + } + } + } +} + +new DirectusToMarkdown(config).export(); +``` diff --git a/index.js b/index.js new file mode 100644 index 0000000..29ec700 --- /dev/null +++ b/index.js @@ -0,0 +1,94 @@ +import { Directus } from '@directus/sdk' +import yaml from 'js-yaml' +import fs from 'fs' +import Axios from 'axios' + +export default class DirectusToMarkdown { + constructor(config) { + this.url = config.url || process.env.DIRECTUS_URL + this.token = config.token || process.env.DIRECTUS_TOKEN + this.contentKey = config.contentKey || 'content' + this.collections = config.collections + this.directus = new Directus(this.url, { auth: { staticToken: this.token }}); + } + + _formatFrontMatter(item) { + const front = { ...item } // copie item + delete front[this.contentKey] + return `---\r\n${yaml.dump(front).trim()}\r\n---\r\n\r\n` + } + + async _writeIndex(item, itemPath) { + const frontMatter = this._formatFrontMatter(item) + const content = item[this.contentKey] ? item[this.contentKey].toString() : '' + const itemContent = `${frontMatter}${content}` + const indexName = 'index' // TODO: index or _index ? + fs.writeFileSync(`${itemPath}/${indexName}.md`, itemContent) + } + + async _writeFile(response, path) { + const writer = fs.createWriteStream(path) + + response.data.pipe(writer) + + return new Promise((resolve, reject) => { + writer.on('finish', resolve) + writer.on('error', reject) + }) + } + + async _downloadAssets(item, itemPath) { + const uuidregex = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/; + for (const [key, uuid] of Object.entries(item)){ + // TODO: instead of trying to pull asset and hope for the best, + // crosscheck things with the /fields and play by the rules + if (typeof uuid === 'string' && uuid.match(uuidregex)) { + const filename = await this._downloadAsset(uuid, itemPath) + item[key] = filename // Update field with filename instead of uuid + } + } + } + + async _downloadAsset(uuid, itemPath) { + const response = await Axios({url: `${this.url}/assets/${uuid}?download&access_token=${this.token}`, method: 'GET', responseType: 'stream'}) + const disposition = response.headers['content-disposition'].match(/filename="(.*)"/); + const filename = disposition ? disposition[1] : `${value}.${mime.extension(response.headers['content-type'])}`; + const savePath = `${itemPath}/${filename}`; + await this._writeFile(response, savePath) + return filename + } + + async _downloadAssetsFromContent(item, itemPath) { + const uuidregex = '[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}' + const regEx = new RegExp(`!\\[.*\\]\\(${this.url}/assets/(${uuidregex})\\)`, "ig") + const uuids = [] + let asset + do { + asset = regEx.exec(item[this.contentKey]) + if (asset) uuids.push(asset[1]) + } while (asset) + + for (const uuid of uuids) { + const filename = await this._downloadAsset(uuid, itemPath) + const url = `${this.url}/assets/${uuid}` + item[this.contentKey] = item[this.contentKey].replace(url, filename) + } + } + + async export() { + for (const collectionName in this.collections) { + const collection = this.collections[collectionName] + const readManyOption = collection.readManyOption + const items = (await this.directus.items(collectionName).readMany(readManyOption)).data + for (const item of items) { + const itemPath = collection.pathBuilder(item) + if (!fs.existsSync(itemPath)) { + fs.mkdirSync(itemPath, { recursive: true }); + } + await this._downloadAssets(item, itemPath) + await this._downloadAssetsFromContent(item, itemPath) + await this._writeIndex(item, itemPath) + } + } + } +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..48616b4 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,104 @@ +{ + "name": "@resilien/directus-to-markdown", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "@resilien/directus-to-markdown", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@directus/sdk": "^9.5.0", + "js-yaml": "^4.1.0" + } + }, + "node_modules/@directus/sdk": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/@directus/sdk/-/sdk-9.5.0.tgz", + "integrity": "sha512-zrQmE8Wde5ITKhTpeYgJgb+QhFc8ySq/mFPfLq1+vO/xzvx5mg7v1OER1tMs7Byq3X/78euZCN4rxrvVRWQGNA==", + "dependencies": { + "axios": "^0.24.0" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/axios": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz", + "integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==", + "dependencies": { + "follow-redirects": "^1.14.4" + } + }, + "node_modules/follow-redirects": { + "version": "1.14.7", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.7.tgz", + "integrity": "sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + } + }, + "dependencies": { + "@directus/sdk": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/@directus/sdk/-/sdk-9.5.0.tgz", + "integrity": "sha512-zrQmE8Wde5ITKhTpeYgJgb+QhFc8ySq/mFPfLq1+vO/xzvx5mg7v1OER1tMs7Byq3X/78euZCN4rxrvVRWQGNA==", + "requires": { + "axios": "^0.24.0" + } + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "axios": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz", + "integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==", + "requires": { + "follow-redirects": "^1.14.4" + } + }, + "follow-redirects": { + "version": "1.14.7", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.7.tgz", + "integrity": "sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ==" + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "requires": { + "argparse": "^2.0.1" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..9b007a5 --- /dev/null +++ b/package.json @@ -0,0 +1,27 @@ +{ + "name": "@resilien/directus-to-markdown", + "version": "0.1.0", + "description": "Export Directus items to markdown files with assets", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "https://git.weko.io/resilien/directus-to-markdown.git" + }, + "keywords": [ + "directus", + "export", + "markdown", + "assets", + "hugo" + ], + "author": "RĂ©siLien", + "license": "ISC", + "type": "module", + "dependencies": { + "@directus/sdk": "^9.5.0", + "js-yaml": "^4.1.0" + } +}