feat: SFTP storage module + sensitive field option

This commit is contained in:
NGPixel
2019-12-25 01:47:19 -05:00
parent 4a2f1d045b
commit 0d6676c19b
17 changed files with 412 additions and 59 deletions

View File

@@ -0,0 +1,71 @@
key: sftp
title: SFTP
description: SFTP (SSH File Transfer Protocol) is a secure file transfer protocol. It runs over the SSH protocol. It supports the full security and authentication functionality of SSH.
author: requarks.io
logo: https://static.requarks.io/logo/ssh.svg
website: https://www.ssh.com/ssh/sftp
isAvailable: true
supportedModes:
- push
defaultMode: push
schedule: false
props:
host:
type: String
title: Host
default: ''
hint: Hostname or IP of the remote SSH server.
order: 1
port:
type: Number
title: Port
default: 22
hint: SSH port of the remote server.
order: 2
authMode:
type: String
title: Authentication Method
default: 'privateKey'
hint: Whether to use Private Key or Password-based authentication. A private key is highly recommended for best security.
enum:
- privateKey
- password
order: 3
username:
type: String
title: Username
default: ''
hint: Username for authentication.
order: 4
privateKey:
type: String
title: Private Key Contents
default: ''
hint: (Private Key Authentication Only) - Contents of the private key
multiline: true
sensitive: true
order: 5
passphrase:
type: String
title: Private Key Passphrase
default: ''
hint: (Private Key Authentication Only) - Passphrase if the private key is encrypted, leave empty otherwise
sensitive: true
order: 6
password:
type: String
title: Password
default: ''
hint: (Password-based Authentication Only) - Password for authentication
sensitive: true
order: 6
basePath:
type: String
title: Base Directory Path
default: '/root/wiki'
hint: Base directory where files will be transferred to. The path must already exists and be writable by the user.
actions:
- handler: exportAll
label: Export All
hint: Output all content from the DB to the remote SSH server, overwriting any existing data. If you enabled SFTP after content was created or you temporarily disabled it, you'll want to execute this action to add the missing content.

View File

@@ -0,0 +1,160 @@
const SSH2Promise = require('ssh2-promise')
const _ = require('lodash')
const path = require('path')
const stream = require('stream')
const Promise = require('bluebird')
const pipeline = Promise.promisify(stream.pipeline)
const pageHelper = require('../../../helpers/page.js')
/* global WIKI */
const getFilePath = (page, pathKey) => {
const fileName = `${page[pathKey]}.${pageHelper.getFileExtension(page.contentType)}`
const withLocaleCode = WIKI.config.lang.namespacing && WIKI.config.lang.code !== page.localeCode
return withLocaleCode ? `${page.localeCode}/${fileName}` : fileName
}
module.exports = {
client: null,
sftp: null,
async activated() {
},
async deactivated() {
},
async init() {
WIKI.logger.info(`(STORAGE/SFTP) Initializing...`)
this.client = new SSH2Promise({
host: this.config.host,
port: this.config.port || 22,
username: this.config.username,
password: (this.config.authMode === 'password') ? this.config.password : null,
privateKey: (this.config.authMode === 'privateKey') ? this.config.privateKey : null,
passphrase: (this.config.authMode === 'privateKey') ? this.config.passphrase : null
})
await this.client.connect()
this.sftp = this.client.sftp()
try {
await this.sftp.readdir(this.config.basePath)
} catch (err) {
WIKI.logger.warn(`(STORAGE/SFTP) ${err.message}`)
throw new Error(`Unable to read specified base directory: ${err.message}`)
}
WIKI.logger.info(`(STORAGE/SFTP) Initialization completed.`)
},
async created(page) {
WIKI.logger.info(`(STORAGE/SFTP) Creating file ${page.path}...`)
const filePath = getFilePath(page, 'path')
await this.ensureDirectory(filePath)
await this.sftp.writeFile(path.posix.join(this.config.basePath, filePath), page.injectMetadata())
},
async updated(page) {
WIKI.logger.info(`(STORAGE/SFTP) Updating file ${page.path}...`)
const filePath = getFilePath(page, 'path')
await this.ensureDirectory(filePath)
await this.sftp.writeFile(path.posix.join(this.config.basePath, filePath), page.injectMetadata())
},
async deleted(page) {
WIKI.logger.info(`(STORAGE/SFTP) Deleting file ${page.path}...`)
const filePath = getFilePath(page, 'path')
await this.sftp.unlink(path.posix.join(this.config.basePath, filePath))
},
async renamed(page) {
WIKI.logger.info(`(STORAGE/SFTP) Renaming file ${page.path} to ${page.destinationPath}...`)
let sourceFilePath = getFilePath(page, 'path')
let destinationFilePath = getFilePath(page, 'destinationPath')
if (WIKI.config.lang.namespacing) {
if (WIKI.config.lang.code !== page.localeCode) {
sourceFilePath = `${page.localeCode}/${sourceFilePath}`
}
if (WIKI.config.lang.code !== page.destinationLocaleCode) {
destinationFilePath = `${page.destinationLocaleCode}/${destinationFilePath}`
}
}
await this.ensureDirectory(destinationFilePath)
await this.sftp.rename(path.posix.join(this.config.basePath, sourceFilePath), path.posix.join(this.config.basePath, destinationFilePath))
},
/**
* ASSET UPLOAD
*
* @param {Object} asset Asset to upload
*/
async assetUploaded (asset) {
WIKI.logger.info(`(STORAGE/SFTP) Creating new file ${asset.path}...`)
await this.ensureDirectory(asset.path)
await this.sftp.writeFile(path.posix.join(this.config.basePath, asset.path), asset.data)
},
/**
* ASSET DELETE
*
* @param {Object} asset Asset to delete
*/
async assetDeleted (asset) {
WIKI.logger.info(`(STORAGE/SFTP) Deleting file ${asset.path}...`)
await this.sftp.unlink(path.posix.join(this.config.basePath, asset.path))
},
/**
* ASSET RENAME
*
* @param {Object} asset Asset to rename
*/
async assetRenamed (asset) {
WIKI.logger.info(`(STORAGE/SFTP) Renaming file from ${asset.path} to ${asset.destinationPath}...`)
await this.ensureDirectory(asset.destinationPath)
await this.sftp.rename(path.posix.join(this.config.basePath, asset.path), path.posix.join(this.config.basePath, asset.destinationPath))
},
/**
* HANDLERS
*/
async exportAll() {
WIKI.logger.info(`(STORAGE/SFTP) Exporting all content to the remote server...`)
// -> Pages
await pipeline(
WIKI.models.knex.column('path', 'localeCode', 'title', 'description', 'contentType', 'content', 'isPublished', 'updatedAt').select().from('pages').where({
isPrivate: false
}).stream(),
new stream.Transform({
objectMode: true,
transform: async (page, enc, cb) => {
const filePath = getFilePath(page, 'path')
WIKI.logger.info(`(STORAGE/SFTP) Adding page ${filePath}...`)
await this.ensureDirectory(filePath)
await this.sftp.writeFile(path.posix.join(this.config.basePath, filePath), pageHelper.injectPageMetadata(page))
cb()
}
})
)
// -> Assets
const assetFolders = await WIKI.models.assetFolders.getAllPaths()
await pipeline(
WIKI.models.knex.column('filename', 'folderId', 'data').select().from('assets').join('assetData', 'assets.id', '=', 'assetData.id').stream(),
new stream.Transform({
objectMode: true,
transform: async (asset, enc, cb) => {
const filename = (asset.folderId && asset.folderId > 0) ? `${_.get(assetFolders, asset.folderId)}/${asset.filename}` : asset.filename
WIKI.logger.info(`(STORAGE/SFTP) Adding asset ${filename}...`)
await this.ensureDirectory(filename)
await this.sftp.writeFile(path.posix.join(this.config.basePath, filename), asset.data)
cb()
}
})
)
WIKI.logger.info('(STORAGE/SFTP) All content has been pushed to the remote server.')
},
async ensureDirectory(filePath) {
if (filePath.indexOf('/') >= 0) {
try {
const folderPaths = _.dropRight(filePath.split('/'))
for (let i = 1; i <= folderPaths.length; i++) {
const folderSection = _.take(folderPaths, i).join('/')
await this.sftp.mkdir(path.posix.join(this.config.basePath, folderSection))
}
} catch (err) {}
}
}
}