Compare commits

...

3 commits

Author SHA1 Message Date
Hippo ab5ced7c6a Allow newer versions of node in package.json
Till now, they had to be strictly equal to 12.x. Now you can also
use newer versions of node (and hopefully they'll work 😛)
2021-07-25 17:26:42 +05:30
Hippo 42f96c00b9 Allow uploading files locally instead of through WebDAV
This gives us more flexibility: after all how often does one have
a WebDAV server handy (unless you're smart enough to use HelioHost
or some other awesome server)?
2021-07-21 22:24:45 +05:30
Hippo 991899a1df Bump convict to version 6 (so we can use new features later)
We want the `nullable` property: coming up in the next commit 😉
2021-07-21 21:59:56 +05:30
6 changed files with 239 additions and 87 deletions

View file

@ -40,10 +40,15 @@ files, and for your Ghost API interface. The parameters to set are:
* `WEBDAV_SERVER_URL` - location of your WebDAV server
* `WEBDAV_USERNAME` - username for signing in
* `WEBDAV_PASSWORD` - password, likewise
* `WEBDAV_UPLOADED_PATH` - path where uploaded images will be served (it
could end up being different from `WEBDAV_SERVER_URL`: say you go to
`https://myhost.com:1234/dav/[folder]` to upload, but the public sees
it as `https://media.mysite.com/[folder]`.
* `WEBDAV_PATH_PREFIX` - prefix to add to all WebDAV paths: no uploads
will happen outside of this path
* `UPLOADED_PATH_PREFIX` - path where uploaded images will be
served (it could end up being different from `WEBDAV_SERVER_URL`: say
you go to `https://myhost.com:1234/dav/[folder]` to upload, but the
public sees it as `https://media.mysite.com/[folder]`—or, more
significantly, when you're doing a local-directory upload!
* `LOCAL_UPLOAD_PATH_PREFIX` - path where uploaded images will be copied
locally, if you choose not to use WebDAV
* `GHOST_URL` - URL of your Ghost installation
* `GHOST_VERSION` - 'v2' or 'v3' depending on which version you're using
* `GHOST_ADMIN_KEY` - 'Admi API key for Ghost'
@ -51,7 +56,11 @@ files, and for your Ghost API interface. The parameters to set are:
In case you're wondering about the WebDAV server: that's the setup we
use at Snipette. We'd like to eventually let you upload directly through
Ghost as well, but we're prioritising our setup first to get running
before we think of anything else. Pull requests are welcome!
before we think of anything else.
Now, we've got a "local upload" option as well which basically copies
the file to a specified directory on the system. Pull requests for
anything else are welcome!
## Pull a post from Medium

93
cli.js
View file

@ -42,39 +42,82 @@ program.command('setup')
'\n\nWe\'re going to take you through some steps' +
' to set up your system.\n'
)
console.log('First up: WebDAV details.')
console.log(
'Please enter your server url (including the port), ' +
'username, and password\n'
)
var res
prompt.start()
res = await prompt.get([
{ name: 'server_url', default: config.webdav.server_url || '' },
{ name: 'username', default: config.webdav.username || '' },
{ name: 'password', default: config.webdav.password || '' , hidden: true},
])
config.webdav.server_url = res.server_url
config.webdav.username = res.username
config.webdav.password = res.password
console.log(`\nOkay. So we have ${config.webdav.username} on ${config.webdav.server_url} with [ the password you set]`)
console.log('First up: File uploads.')
console.log(
'\nA couple more settings for your WebDAV: ' +
'we need to know the path prefix and the uploaded path prefix.\n' +
'The path prefix is the subfolder to which you upload, like ' +
'`/seance-uploads`, while the uploaded path prefix is what '+
'you\'d stick in front of the filename after uploading ' +
'(like `https://media.mysite.com/seance-uploads`).\n'
'Would you like to upload your files via WebDAV, or just ' +
'copy them to a local folder on your filesystem? Type ' +
'"webdav" or "local" to choose.\n'
)
res = await prompt.get([
{ name: 'path_prefix', default: config.webdav.path_prefix || '' },
{ name: 'uploaded_path_prefix', default: config.webdav.uploaded_path_prefix || '' },
{
name: 'upload_mode',
default: 'webdav',
pattern: /^(webdav|local)$/ig,
message: 'Please enter "webdav" or "local"',
},
])
config.webdav.path_prefix = res.path_prefix
config.webdav.uploaded_path_prefix = res.uploaded_path_prefix
console.log(`Cool. So uploads to ${config.webdav.path_prefix} will go to ${config.webdav.uploaded_path_prefix}.`)
if (res.upload_mode == 'webdav') {
console.log('You\'re going with WebDAV? Awesome!')
console.log(
'Please enter your server url (including the port), ' +
'username, and password\n'
)
res = await prompt.get([
{ name: 'server_url', default: config.webdav.server_url || '' },
{ name: 'username', default: config.webdav.username || '' },
{ name: 'password', default: config.webdav.password || '' , hidden: true},
])
config.webdav.server_url = res.server_url
config.webdav.username = res.username
config.webdav.password = res.password
console.log(`\nOkay. So we have ${config.webdav.username} on ${config.webdav.server_url} with [ the password you set]`)
console.log(
'\nA couple more settings for your WebDAV: ' +
'we need to know the path prefix and the uploaded path prefix.\n' +
'The path prefix is the subfolder to which you upload, like ' +
'`/seance-uploads`, while the uploaded path prefix is what '+
'you\'d stick in front of the filename after uploading ' +
'(like `https://media.mysite.com/seance-uploads`).\n'
)
res = await prompt.get([
{ name: 'path_prefix', default: config.webdav.path_prefix || '' },
{
name: 'uploaded_path_prefix',
default: config.uploaded_path_prefix || config.webdav.uploaded_path_prefix || ''
},
])
config.webdav.path_prefix = res.path_prefix
config.uploaded_path_prefix = res.uploaded_path_prefix
console.log(`Cool. So uploads to ${config.webdav.path_prefix} will be visible at ${config.uploaded_path_prefix}.`)
} else if (res.upload_mode == 'local') {
console.log('You\'re saving files locally? Smart!')
console.log(
'Two settings we need to know to get things running ' +
'smoothly: we need the local path/folder where you\'ll be ' +
'uploading the files, and the uploaded path prefix.\n' +
'The local path is the folder to which you upload, like ' +
'`/var/www/seance-uploads`, while the uploaded path prefix ' +
'is what you\'d stick in front of the filename after ' +
'uploading (like `https://media.mysite.com/seance-uploads`).\n'
)
res = await prompt.get([
{ name: 'path_prefix', default: config.local_upload.path_prefix || '' },
{ name: 'uploaded_path_prefix', default: config.uploaded_path_prefix || '' },
])
config.local_upload.path_prefix = res.path_prefix
config.uploaded_path_prefix = res.uploaded_path_prefix
console.log(`Cool. So uploads to ${config.local_upload.path_prefix} will be visible at ${config.uploaded_path_prefix}.`)
}
console.log('\n\nNext up: Ghost settings.')
console.log(

View file

@ -1,26 +1,36 @@
// load configuraton from .env file (if exists)
require('dotenv').config()
const convict = require('convict')
const convict_format_with_validator = require('convict-format-with-validator')
const os = require('os')
const path = require('path')
const yaml = require('js-yaml')
const fs = require('fs')
convict.addFormats(convict_format_with_validator)
convict.addParser({ extension: ['yml', 'yaml'], parse: yaml.safeLoad })
let config = convict({
uploaded_path_prefix: {
doc: 'URL where files are uploaded (eg. https://mysitem.com/media)',
format: 'url',
env: 'UPLOADED_PATH_PREFIX',
default: null,
},
webdav: {
server_url: {
doc: 'WebDAV server URL (eg. https://myhost.com:2078)',
format: 'url',
env: 'WEBDAV_SERVER_URL',
default: null,
nullable: true,
},
username: {
doc: 'Username for WebDAV server',
format: 'String',
env: 'WEBDAV_USERNAME',
default: null,
nullable: true,
},
password: {
doc: 'Password for WebDAV server',
@ -28,18 +38,21 @@ let config = convict({
env: 'WEBDAV_PASSWORD',
default: null,
sensitive: true,
nullable: true,
},
path_prefix: {
doc: 'Where to upload files (eg. /seance-uploads)',
format: 'String',
env: 'WEBDAV_PATH_PREFIX',
default: null,
nullable: true,
},
uploaded_path_prefix: {
uploaded_path_prefix: { // FIXME: Deprecated; remove
doc: 'URL where files are uploaded (eg. https://mysitem.com/media)',
format: 'url',
env: 'WEBDAV_UPLOADED_PATH_PREFIX',
default: null,
nullable: true,
},
use_digest: {
doc: 'Whether to use digest authentication',
@ -48,6 +61,14 @@ let config = convict({
default: false,
}
},
local_upload: {
path_prefix: {
doc: 'Where to upload files locally (eg. /media/seance-uploads)',
format: 'String',
env: 'LOCAL_UPLOAD_PATH_PREFIX',
default: null,
},
},
ghost: {
url: {
doc: 'URL of Ghost installation',
@ -73,7 +94,7 @@ let config = convict({
format: '*', // TODO: validate by checking path
env: 'SEPARATOR_IMAGE',
default: null,
}
},
})
// Load configs from home directory, if present
@ -98,6 +119,17 @@ try {
validated = false
}
// Update deprecated value: config.webdav.uploaded_path_prefix
if (!!config.webdav && !!config.webdav.uploaded_path_prefix) {
console.warn(
'Warning: config.webdav.uploaded_path_prefix and the ' +
'WEBDAV_UPLOADED_PATH_PREFIX environment variable are ' +
'deprecated! Please use config.uploaded_path_prefix or ' +
'the UPLOADED_PATH_PREFIX environment variable instead.'
)
config.uploaded_path_prefix = config.webdav.uploaded_path_prefix
}
allConf = config.getProperties()
allConf.validated = validated

View file

@ -21,7 +21,8 @@
"body-parser": "^1.19.0",
"bulma": "^0.8.2",
"commander": "^3.0.2",
"convict": "^5.2.0",
"convict": "6",
"convict-format-with-validator": "^6.2.0",
"css-loader": "^3.5.3",
"dotenv": "^8.2.0",
"express": "^4.17.1",
@ -46,6 +47,6 @@
"webpack-node-externals": "^1.7.2"
},
"engines": {
"node": "12.x"
"node": ">=12.x"
}
}

109
seance.js
View file

@ -159,7 +159,7 @@ class Seance {
return false
}
// Decide WebDAV upload path
// Decide file/WebDAV upload path
var current_date = new Date()
const uploadPath = path.join(
@ -168,12 +168,9 @@ class Seance {
postSlug
)
// Path where WebDAV files will be placed (eg. https://example.com:2078)
const davPath = path.join(config.webdav.path_prefix, uploadPath)
// Public path to upload those files (eg. https://media.example.com/uploads)
// Public path to upload those files (eg. https://example.com:2078)
// We'll do it directly since path.join mangles the protocol
const uploadedPath = config.webdav.uploaded_path_prefix + '/' + uploadPath
const uploadedPath = config.uploaded_path_prefix + '/' + uploadPath
// load metadata file
this.emit('update', {
@ -270,7 +267,7 @@ class Seance {
// Let's wait for the upload, just to avoid conflicts
if (!options.noUpload) {
await this.uploadDav(davPath, imagePath)
await this.upload(uploadPath, imagePath)
}
newLine = '![' + imageAlt + '](' + uploadedPath + '/' + imageName + ')'
@ -320,7 +317,7 @@ class Seance {
})
if (!options.noUpload) {
this.uploadDav(davPath, imagePath)
this.upload(uploadPath, imagePath)
}
featuredImagePath = uploadedPath + '/' + imageName
@ -567,8 +564,7 @@ class Seance {
loglevel: 'info'
})
await this.uploadDav(path.join(config.webdav.path_prefix,'avatars'),
filePath)
await this.upload('avatars', filePath)
// Generate Ghost JSON
@ -581,7 +577,7 @@ class Seance {
bio: json.payload.user.bio,
email: email,
name: json.payload.user.name,
profile_image: config.webdav.uploaded_path_prefix + '/avatars/' + fileName
profile_image: config.uploaded_path_prefix + '/avatars/' + fileName
}
]
},
@ -680,6 +676,9 @@ class Seance {
* @returns [string] status
*/
async uploadDav (dirPath, filePath) {
// Set uploadPath
// We'll do it directly since path.join mangles the protocol
let uploadPath = path.join(config.webdav.path_prefix, dirPath)
// connect to webdav
const client = createClient(
@ -692,7 +691,7 @@ class Seance {
// create directory if not exists
console.debug(`[dav-upload] Loading ${dirPath}`)
if (!await this.createDirIfNotExist(client, dirPath)) {
if (!await this.createDirIfNotExist(client, uploadPath)) {
console.error(`[dav-upload] Could not upload ${path.basename(filePath)} :(`)
return false
}
@ -700,7 +699,7 @@ class Seance {
// upload a file
console.debug('Uploading file')
const outStream = client.createWriteStream(
path.join(dirPath, path.basename(filePath))
path.join(uploadPath, path.basename(filePath))
)
outStream.on('finish', () => console.debug('Uploaded successfully.'))
@ -710,6 +709,90 @@ class Seance {
return true
}
/**
* function [exists]
* @returns [boolean]
*
* check if the given file exists or not
*/
async fsExists (path) {
try {
await fs.promises.access(path)
return true
} catch {
return false
}
}
/**
* function [uploadLocal]
* @returns [string] status
*
* upload to a local file path. This should technically be
* called "copy" and not "upload", but the equivalent
* WebDAV one is actually an upload so ¯\_()_/¯
*/
async uploadLocal (dirPath, filePath) {
// Set uploadPath
// We'll do it directly since path.join mangles the protocol
let uploadPath = path.join(config.local_upload.path_prefix, dirPath)
// safety: don't touch directories outside LOCAL_UPLOAD_PATH_PREFIX
if (!uploadPath.startsWith(config.local_upload.path_prefix)) {
console.error(`[local-upload] Cannot create directories outside ${config.local_upload.path_prefix}`)
return false
}
// create directory if not exists
console.debug(`[local-upload] Loading ${uploadPath}`)
if (
!(await this.fsExists(uploadPath)) ||
!(await fs.promises.lstat(uploadPath)).isDirectory()
) {
if (!(await fs.promises.mkdir(uploadPath,
{ recursive: true }))) {
console.error(`[local-upload] Could not upload ${path.basename(filePath)} :(`)
return false
}
}
// actually do the copying
console.debug('Uploading file')
try {
await fs.promises.copyFile(filePath,
path.join(uploadPath, path.basename(filePath)))
} catch (err) {
console.error(`Upload error: ${err}`)
}
return true
}
/**
* function [upload]
* @returns [boolean] status
*
* upload to WebDAV or a local folder, whichever is configured.
* If both are considered, WebDAV will be preferred.
*/
async upload (dirPath, filePath) {
if (
!!config.webdav &&
!!config.webdav.server_url &&
!!config.webdav.path_prefix
) {
return await this.uploadDav(dirPath, filePath)
} else if (
!!config.local_upload &&
!!config.local_upload.path_prefix
) {
return await this.uploadLocal(dirPath, filePath)
} else {
throw { error: 'Either webdav or local_upload settings must be configured!' }
}
}
}
// Make Seance an EventEmitter

View file

@ -1661,16 +1661,20 @@ convert-source-map@^1.5.1:
dependencies:
safe-buffer "~5.1.1"
convict@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/convict/-/convict-5.2.0.tgz#4c01fa06885b8c4a4ffc98b7de43222fe6c876dc"
integrity sha512-C3cdUwo47cCikZNzu5Vv8AL0MuXVVeg9t/Gyr9qyK5ZpCjOkMPmJ85KUF3CowNeSfj4UtztHxS+hoO9wGRh6kg==
convict-format-with-validator@^6.2.0:
version "6.2.0"
resolved "https://registry.yarnpkg.com/convict-format-with-validator/-/convict-format-with-validator-6.2.0.tgz#3cab7779e83a89351b9157685cbf20a941414192"
integrity sha512-2LIL3yEZY27M13UHLIP4mGivusP9h2M+X4mYsRBLexwUp8+0sgVk2MgB2b2bnQwkn293lkbkxgdevzn0nZdyzQ==
dependencies:
json5 "2.1.0"
lodash.clonedeep "4.5.0"
moment "2.24.0"
validator "11.1.0"
yargs-parser "13.0.0"
validator "^13.6.0"
convict@6:
version "6.2.0"
resolved "https://registry.yarnpkg.com/convict/-/convict-6.2.0.tgz#d227aef6d77a4d518ca6a6a3b863bdfdb6dc5665"
integrity sha512-aCk1+VWt3TG6SJV59u+wwuza7lvtlJfj6zH/fmE1xzx5yZnNby1lPYkccq1mKaJJXHjk9cuVCFWVVIhbkpmwRw==
dependencies:
lodash.clonedeep "^4.5.0"
yargs-parser "^20.2.7"
cookie-signature@1.0.6:
version "1.0.6"
@ -3269,13 +3273,6 @@ json-stringify-safe@~5.0.1:
resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=
json5@2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.0.tgz#e7a0c62c48285c628d20a10b85c89bb807c32850"
integrity sha512-8Mh9h6xViijj36g7Dxi+Y4S6hNGV96vcJZr/SrlHh1LR/pEn/8j/+qIBbs44YKl69Lrfctp4QD+AdWLTMqEZAQ==
dependencies:
minimist "^1.2.0"
json5@^0.5.1:
version "0.5.1"
resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821"
@ -3406,7 +3403,7 @@ lodash.camelcase@^4.3.0:
resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6"
integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY=
lodash.clonedeep@4.5.0:
lodash.clonedeep@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=
@ -3731,16 +3728,11 @@ minimist@0.0.8:
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=
minimist@^1.1.3, minimist@^1.2.5:
minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.5:
version "1.2.5"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
minimist@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=
minipass@^2.6.0, minipass@^2.8.6, minipass@^2.9.0:
version "2.9.0"
resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.9.0.tgz#e713762e7d3e32fed803115cf93e04bca9fcc9a6"
@ -3802,11 +3794,6 @@ mkdirp@^0.5.0, mkdirp@^0.5.1:
dependencies:
minimist "0.0.8"
moment@2.24.0:
version "2.24.0"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b"
integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==
move-concurrently@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"
@ -6516,10 +6503,10 @@ validate-npm-package-license@^3.0.1:
spdx-correct "^3.0.0"
spdx-expression-parse "^3.0.0"
validator@11.1.0:
version "11.1.0"
resolved "https://registry.yarnpkg.com/validator/-/validator-11.1.0.tgz#ac18cac42e0aa5902b603d7a5d9b7827e2346ac4"
integrity sha512-qiQ5ktdO7CD6C/5/mYV4jku/7qnqzjrxb3C/Q5wR3vGGinHTgJZN/TdFT3ZX4vXhX2R1PXx42fB1cn5W+uJ4lg==
validator@^13.6.0:
version "13.6.0"
resolved "https://registry.yarnpkg.com/validator/-/validator-13.6.0.tgz#1e71899c14cdc7b2068463cb24c1cc16f6ec7059"
integrity sha512-gVgKbdbHgtxpRyR8K0O6oFZPhhB5tT1jeEHZR0Znr9Svg03U0+r9DXWMrnRAB+HtCStDQKlaIZm42tVsVjqtjg==
vary@~1.1.2:
version "1.1.2"
@ -6793,14 +6780,6 @@ yallist@^3.0.0, yallist@^3.0.2, yallist@^3.0.3:
resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==
yargs-parser@13.0.0:
version "13.0.0"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.0.0.tgz#3fc44f3e76a8bdb1cc3602e860108602e5ccde8b"
integrity sha512-w2LXjoL8oRdRQN+hOyppuXs+V/fVAYtpcrRxZuF7Kt/Oc+Jr2uAcVntaUTNT6w5ihoWfFDpNY8CPx1QskxZ/pw==
dependencies:
camelcase "^5.0.0"
decamelize "^1.2.0"
yargs-parser@^13.1.2:
version "13.1.2"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38"
@ -6809,6 +6788,11 @@ yargs-parser@^13.1.2:
camelcase "^5.0.0"
decamelize "^1.2.0"
yargs-parser@^20.2.7:
version "20.2.9"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"
integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==
yargs@^13.3.2:
version "13.3.2"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd"