2019-10-09 06:47:37 -04:00
const r2 = require ( 'r2' )
2019-12-10 07:45:09 -05:00
const path = require ( 'path' )
const fs = require ( 'fs' )
2020-01-01 11:01:13 -05:00
const { once } = require ( 'events' )
2019-10-11 06:55:52 -04:00
const getPost = require ( 'mediumexporter' ) . getPost
2019-12-19 07:20:24 -05:00
const { createClient } = require ( 'webdav' )
2019-12-24 04:20:48 -05:00
const readline = require ( 'readline' )
2019-12-24 12:08:38 -05:00
const { markdown } = require ( 'markdown' )
2019-12-24 11:52:58 -05:00
const GhostAdminAPI = require ( '@tryghost/admin-api' )
2020-01-01 11:01:13 -05:00
const Rembrandt = require ( 'rembrandt' )
2019-10-09 06:47:37 -04:00
2020-04-24 11:50:41 -04:00
const config = require ( './config' )
2019-12-24 10:00:01 -05:00
const MEDIUM _IMG _CDN = 'https://miro.medium.com/fit/c/'
2020-04-26 11:41:15 -04:00
try {
const ghostAdmin = new GhostAdminAPI ( {
url : config . ghost . url ,
version : config . ghost . version ,
key : config . ghost . admin _key ,
} )
} catch ( err ) {
console . error ( 'Your Ghost isn\'t configured. Please run `seance setup` to fix this!' )
}
2019-10-06 09:57:08 -04:00
/ * *
* function [ fetchFromMedium ]
* @ returns [ string ] status
* /
2019-12-10 07:45:09 -05:00
2019-10-11 06:55:52 -04:00
const fetchFromMedium = async ( mediumUrl ) => {
2019-12-10 07:45:09 -05:00
console . info ( ` Fetching: ${ mediumUrl } ` ) ;
output = path . join ( process . env . PWD , 'content' )
// use mediumexporter's getPost function to fetch a Medium post
const post = await getPost ( mediumUrl , {
returnObject : true ,
output : output ,
} ) . catch ( ( err ) => {
return {
error : err ,
}
} )
// set output folder path
// this is based on what mediumexporter chooses as the output folder
outputFolder = path . join ( output , post . slug )
console . info ( ` Saving to: ${ outputFolder } ` )
if ( ! fs . existsSync ( path . join ( outputFolder , post . slug ) ) ) {
fs . mkdirSync ( outputFolder , { recursive : true } )
}
// mediumexporter writes a plain .md file if the post has no media
// if that is the case, we should create the subfolder manually
// and copy the data there.
if ( fs . existsSync ( path . join ( output , post . slug + '.md' ) ) ) {
fs . renameSync (
path . join ( output , post . slug + '.md' ) ,
path . join ( outputFolder , 'index.md' )
)
}
// generate metadata
const metadata = JSON . stringify ( {
title : post . title ,
subtitle : post . subtitle ,
2019-12-24 12:07:00 -05:00
author : post . author || "" ,
authors : post . authors || [ ] ,
2019-12-10 07:45:09 -05:00
date : new Date ( post . date ) ,
tags : post . tags ,
url : post . url ,
slug : post . slug ,
images : post . images ,
featuredImage : post . featuredImage ,
} )
// write metadata to output folder
2019-12-24 12:07:00 -05:00
fs . writeFileSync ( path . join ( outputFolder , 'metadata.json' ) , metadata )
2019-12-10 07:45:09 -05:00
return post
2019-10-06 09:57:08 -04:00
} ;
/ * *
* function [ pushToGhost ]
* @ returns [ string ] status
* /
2020-01-01 11:01:13 -05:00
const pushToGhost = async ( postSlug ) => {
2019-10-06 09:57:08 -04:00
console . info ( 'Pushing: ' + postSlug ) ;
2019-12-24 04:20:48 -05:00
// Decide working path
postFolder = path . resolve ( 'content/' + postSlug )
// Verify file exists
if ( ! fs . existsSync ( postFolder ) ) {
console . error ( 'Could not find post folder! Is it fetched?' )
return false
}
// Decide file
const postContent = path . join ( postFolder , 'index.md' )
const postOutput = path . join ( postFolder , 'ghost.md' )
// Verify post exists
if ( ! fs . existsSync ( postContent ) ) {
console . error ( "Could not find 'index.md' in " + postSlug + "! Is it fetched?" )
return false
}
// Decide WebDAV upload path
current _date = new Date ( )
2020-01-01 11:03:57 -05:00
const uploadPath = path . join (
2019-12-24 04:20:48 -05:00
current _date . getUTCFullYear ( ) . toString ( ) ,
2020-01-01 11:03:57 -05:00
( current _date . getUTCMonth ( ) + 1 ) . toString ( ) ,
2019-12-24 04:20:48 -05:00
postSlug
)
// Path where WebDAV files will be placed (eg. https://example.com:2078)
2020-04-24 12:50:18 -04:00
const davPath = path . join ( config . webdav . path _prefix , uploadPath )
2019-12-24 04:20:48 -05:00
// Public path to upload those files (eg. https://media.example.com/uploads)
// We'll do it directly since path.join mangles the protocol
2020-04-24 12:50:18 -04:00
const uploadedPath = config . webdav . uploaded _path _prefix + '/' + uploadPath
2020-01-01 11:05:23 -05:00
// load metadata file
console . debug ( 'Loading metadata' )
postMetaFile = path . join ( postFolder , 'metadata.json' )
let postMeta = await JSON . parse ( fs . readFileSync ( postMetaFile ) )
2019-12-24 04:20:48 -05:00
// Process lines
const readInterface = readline . createInterface ( {
input : fs . createReadStream ( postContent ) ,
output : process . stdout ,
terminal : false
} )
const outStream = fs . createWriteStream ( postOutput , { encoding : 'utf-8' } )
2020-01-01 11:06:29 -05:00
var titleSkipped = false ;
2019-12-24 12:08:38 -05:00
let reImage = new RegExp ( '^!\\[(.*)\\]\\((\\S+?)\\)(.*)' )
let reTitle = new RegExp ( '^#\ .*' )
2020-01-01 11:07:36 -05:00
// Note down uploaded images
var uploadedImages = [ ]
2019-12-24 04:20:48 -05:00
2020-01-01 11:01:13 -05:00
for await ( const line of readInterface ) {
2019-12-24 12:08:38 -05:00
// Line to output
// Default is to make it same as input
2019-12-24 04:20:48 -05:00
var newLine = line
2019-12-24 12:08:38 -05:00
// Skip the header
2020-01-01 11:06:29 -05:00
if ( ! titleSkipped && await reTitle . exec ( line ) ) {
titleSkipped = true
}
2019-12-24 12:08:38 -05:00
2019-12-24 04:20:48 -05:00
// check for images
2020-01-01 11:01:13 -05:00
var m = await reImage . exec ( line )
2019-12-24 04:20:48 -05:00
if ( m ) {
// Get image name
var imageAlt = m [ 1 ]
var imageName = m [ 2 ] . replace ( '*' , '' )
var imagePath = path . join ( postFolder , 'images' , imageName )
if ( ! fs . existsSync ( imagePath ) ) {
console . warn ( 'Skipping missing image: ' + imageName )
} else {
2020-01-01 11:01:13 -05:00
// check for separator image
var isScissors = await checkScissors ( imagePath )
if ( isScissors ) {
newLine = '\n---\n'
} else {
// upload pic to server
console . debug ( ` Adding to upload queue: ${ imageName } ` )
uploadedImages . push ( imageName )
uploadDav ( davPath , imagePath )
2019-12-24 04:20:48 -05:00
2020-01-01 11:01:13 -05:00
newLine = '![' + imageAlt + '](' + uploadedPath + '/' + imageName + ')'
}
2019-12-24 04:20:48 -05:00
}
}
outStream . write ( newLine + '\n' )
2020-01-01 11:01:13 -05:00
}
2019-12-24 12:08:38 -05:00
2020-01-01 11:07:36 -05:00
// Upload feature_image, if required
var featuredImagePath
if ( ! ! postMeta . featuredImage ) {
var imageName = postMeta . featuredImage . replace ( '*' , '' )
2019-12-24 04:20:48 -05:00
2020-01-01 11:07:36 -05:00
// if the image is listed in postMeta.images, it would have
// already been uploaded
if ( uploadedImages . indexOf ( imageName ) != - 1 ) {
console . log ( ` Skipping feature image ${ imageName } : already listed for upload ` )
} else {
var imagePath = path . join ( postFolder , 'images' , imageName )
2019-12-24 12:08:38 -05:00
2020-01-01 11:07:36 -05:00
// We can only upload if the file exists!
if ( ! fs . existsSync ( imagePath ) ) {
console . warn ( ` Skipping feature image " ${ imageName } ": file not found ` )
} else {
console . log ( ` Uploading feature image: ${ imageName } ` )
uploadDav ( davPath , imagePath )
featuredImagePath = uploadedPath + '/' + imageName
2019-12-24 12:08:38 -05:00
}
2020-01-01 11:01:13 -05:00
}
}
2019-12-24 12:08:38 -05:00
2020-01-01 11:01:13 -05:00
// calculate users
let users = [ ]
postMeta . authors . forEach ( ( user ) => {
users . push ( { slug : user . username } )
} )
// This will happen once all the line reading is finished
// Uploads will continue in paralell though
console . debug ( 'Adding to Ghost' )
ghostAdmin . posts . add ( {
title : postMeta . title ,
custom _excerpt : postMeta . subtitle || null ,
tags : postMeta . tags ,
authors : users ,
html : markdown . toHTML ( fs . readFileSync ( postOutput , mode = 'utf-8' ) ) ,
feature _image : featuredImagePath
} , { source : 'html' } )
. then ( ( res ) => {
// Check if user was added
if ( res . primary _author . id == 1 ) {
console . warn ( ` WARNING: The admin editor, " ${ res . primary _author . name } ", is set as author for this post. If this is incorrect, there was some problem matching usernames. Please check and set it manually. ` )
}
console . log ( 'Post conveyed successfully.' )
2019-12-24 12:08:38 -05:00
} )
2019-10-06 09:57:08 -04:00
} ;
/ * *
* function [ mediumToGhost ]
* @ returns [ string ] status
* /
const mediumToGhost = ( mediumUrl ) => {
console . info ( 'Copying: ' + mediumUrl ) ;
} ;
2019-10-09 06:47:37 -04:00
async function fetchMediumJSON ( mediumUrl ) {
console . debug ( ` Fetching: ${ mediumUrl } ` )
const response = await fetch ( mediumUrl )
const text = await response . text ( )
const json = await JSON . parse ( text . substr ( text . indexOf ( '{' ) ) )
return json ;
}
2020-01-01 11:01:13 -05:00
/ * *
* function [ checkScissors ]
* @ returns [ boolean ] matchStatus
* /
const checkScissors = async ( imagePath ) => {
// Decide "separator" image
// If set, images matching this will be ignored and replaced
// with a horizontal-rule ("---" in markdown) instead.
2020-04-24 12:50:18 -04:00
let scissors = config . scissors
2020-01-01 11:01:13 -05:00
// if scissors not set, return false
// (it's never a scissors since it never matches)
if ( ! scissors ) {
console . warn ( '[scissors] No scissors set, so rejecting all images' )
return false
} else {
// Check if given image matches the scissors
try {
let isScissors = new Rembrandt ( {
imageA : scissors ,
imageB : imagePath ,
thresholdType : Rembrandt . THRESHOLD _PERCENT ,
maxThreshold : 0.1
} )
let result = await isScissors . compare ( )
return result . passed
} catch ( err ) {
console . warn ( '[scissors] Skipping scissors check:' , err . message )
return false
}
}
}
2019-10-06 09:57:08 -04:00
/ * *
* function [ createUser ]
2019-12-19 07:20:24 -05:00
* @ returns [ object ] ghost data json
2019-10-06 09:57:08 -04:00
* /
2019-10-09 06:47:37 -04:00
const generateUserData = async ( mediumUsername , email ) => {
console . debug ( 'Creating: @' + mediumUsername + '(email: ' + email + ')' ) ;
const mediumUrl = ` https://medium.com/@ ${ mediumUsername } /?format=json ` ;
const json = await fetchMediumJSON ( mediumUrl ) ;
if ( ! json . success ) {
console . error ( ` Error: ${ json . error } ` )
return false
}
console . debug ( ` Name: ${ json . payload . user . name } ` )
console . debug ( ` Bio: ${ json . payload . user . bio } ` )
2019-12-24 10:00:01 -05:00
// Download and upload image
let imageId = json . payload . user . imageId
console . log ( ` Image: ${ imageId } ` )
let imagePath = MEDIUM _IMG _CDN + '256/256/' + imageId
let filetype = imageId . split ( '.' ) [ imageId . split ( '.' ) . length - 1 ]
let fileName = ` ${ mediumUsername } . ${ filetype } `
let filePath = path . join ( process . env . PWD , fileName )
console . log ( ` Fetching: ${ imagePath } ` )
const response = await ( await r2 . get ( imagePath ) . response ) . buffer ( )
await fs . writeFileSync ( filePath , response , 'base64' )
console . log ( "Uploading to server" )
2020-04-24 12:50:18 -04:00
await uploadDav ( path . join ( config . webdav . path _prefix , 'avatars' ) ,
2019-12-24 10:00:01 -05:00
filePath )
2019-10-09 06:47:37 -04:00
// Generate Ghost JSON
const ghostData = {
data : {
users : [
{
id : 1 ,
slug : json . payload . user . username ,
bio : json . payload . user . bio ,
email : email ,
name : json . payload . user . name ,
2020-04-24 12:50:18 -04:00
profile _image : config . webdav . uploaded _path _prefix + '/avatars/' + fileName
2019-10-09 06:47:37 -04:00
}
]
} ,
meta : {
exported _on : new Date ,
version : '2.14.0'
}
}
return ( JSON . stringify ( ghostData ) )
2019-10-06 09:57:08 -04:00
} ;
2019-12-19 07:20:24 -05:00
const createDirIfNotExist = async ( client , folder ) => {
// recursively create subfolders if they don't exist.
//safety: don't touch directories outside WEBDAV_PATH_PREFIX
2020-04-24 12:50:18 -04:00
if ( ! folder . startsWith ( config . webdav . path _prefix ) ) {
throw new Error ( ` Cannot create directories outside ${ config . webdav . path _prefix } ` )
2019-12-19 07:20:24 -05:00
}
// check the folder
await client . stat ( folder )
. catch ( async ( err ) => {
2020-01-01 11:08:34 -05:00
if ( ! err . response ) {
// no response! Maybe a network error or something :P
console . error ( ` [dav-upload:folder] Error creating folder " ${ folder } " ` )
console . error ( ` [dav-upload:folder] ${ err . toJSON ( ) . message } ` )
console . error ( '[dav-upload:folder] Please check your Internet connection and try again' )
return false
} else if ( err . response . status == 404 ) {
2019-12-19 07:20:24 -05:00
// it's a 404, so we'll create the directory
console . debug ( ` Noting missing subdirectory: ${ folder } ` )
// first, create the parent directory (if required)
2020-01-03 05:34:25 -05:00
if ( ! await createDirIfNotExist ( client , path . dirname ( folder ) ) ) {
2020-01-01 11:08:34 -05:00
// if not created, we fail too :-/
return false
}
2019-12-19 07:20:24 -05:00
console . debug ( ` Creating missing subdirectory: ${ folder } ` )
// then, create the current directory
await client . createDirectory ( folder )
. catch ( async ( err ) => {
if ( err . response . status == 405 ) { // Method Not Allowed
// Maybe the directory's already been created in the meantime?
await client . stat ( folder )
. catch ( ( err2 ) => {
// Bad guess. Panic (and raise the original error)
2020-01-01 11:08:34 -05:00
console . error ( ` Error: ${ err . toJSON ( ) . message } ` )
console . error ( "We're not sure what went wrong. Help!" )
2019-12-19 07:20:24 -05:00
throw err
} )
} else {
// what's this? Panic!
2020-01-01 11:08:34 -05:00
console . error ( ` Error: ${ err . toJSON ( ) . message } ` )
console . error ( "We're not sure what went wrong. Help!" )
2019-12-19 07:20:24 -05:00
throw err
}
} )
} else {
// it's not a 404; we don't know how to handle this. Panic!
2020-01-01 11:08:34 -05:00
console . error ( err . toJSON ( ) )
2019-12-19 07:20:24 -05:00
throw err
}
} )
2020-01-01 11:08:34 -05:00
return true
2019-12-19 07:20:24 -05:00
}
/ * *
2020-01-01 11:08:34 -05:00
* function [ uploadDav ]
2019-12-19 07:20:24 -05:00
* @ returns [ string ] status
* /
2019-12-24 04:20:13 -05:00
const uploadDav = async ( dirPath , filePath ) => {
2019-12-19 07:20:24 -05:00
// connect to webdav
client = createClient (
2020-04-24 12:50:18 -04:00
config . webdav . server _url ,
2019-12-19 07:20:24 -05:00
{
2020-04-24 12:50:18 -04:00
username : config . webdav . username ,
password : config . webdav . password ,
digest : config . webdav . use _digest
2019-12-19 07:20:24 -05:00
} )
// create directory if not exists
2020-01-01 11:08:34 -05:00
console . debug ( ` [dav-upload] Loading ${ dirPath } ` )
if ( ! await createDirIfNotExist ( client , dirPath ) ) {
console . error ( ` [dav-upload] Could not upload ${ path . basename ( filePath ) } :( ` )
return false
}
2019-12-19 07:20:24 -05:00
// upload a file
console . debug ( 'Uploading file' )
2020-01-01 11:08:34 -05:00
outStream = client . createWriteStream (
2019-12-24 04:20:13 -05:00
path . join ( dirPath , path . basename ( filePath ) )
2020-01-01 11:08:34 -05:00
)
outStream . on ( 'finish' , ( ) => console . debug ( 'Uploaded successfully.' ) )
inStream = fs . createReadStream ( filePath )
. pipe ( outStream )
2019-12-19 07:20:24 -05:00
return true
}
2019-10-06 09:57:08 -04:00
module . exports = {
fetchFromMedium ,
pushToGhost ,
mediumToGhost ,
2019-10-09 06:47:37 -04:00
generateUserData ,
2020-01-01 11:01:13 -05:00
checkScissors ,
2019-12-24 04:20:13 -05:00
uploadDav
2019-10-06 09:57:08 -04:00
}