ghost-storage-adapter-s3
Version:
An AWS S3 storage adapter for Ghost 0.10+
151 lines (130 loc) • 4.87 kB
JavaScript
import AWS from 'aws-sdk'
import BaseStore from 'ghost-storage-base'
import { join } from 'path'
import { readFile } from 'fs'
const readFileAsync = fp => new Promise((resolve, reject) => readFile(fp, (err, data) => err ? reject(err) : resolve(data)))
const stripLeadingSlash = s => s.indexOf('/') === 0 ? s.substring(1) : s
const stripEndingSlash = s => s.indexOf('/') === (s.length - 1) ? s.substring(0, s.length - 1) : s
class Store extends BaseStore {
constructor (config = {}) {
super(config)
const {
accessKeyId,
assetHost,
bucket,
pathPrefix,
region,
secretAccessKey,
endpoint,
serverSideEncryption,
forcePathStyle,
signatureVersion,
acl
} = config
// Compatible with the aws-sdk's default environment variables
this.accessKeyId = accessKeyId
this.secretAccessKey = secretAccessKey
this.region = process.env.AWS_DEFAULT_REGION || region
this.bucket = process.env.GHOST_STORAGE_ADAPTER_S3_PATH_BUCKET || bucket
// Optional configurations
this.host = process.env.GHOST_STORAGE_ADAPTER_S3_ASSET_HOST || assetHost || `https://s3${this.region === 'us-east-1' ? '' : `-${this.region}`}.amazonaws.com/${this.bucket}`
this.pathPrefix = stripLeadingSlash(process.env.GHOST_STORAGE_ADAPTER_S3_PATH_PREFIX || pathPrefix || '')
this.endpoint = process.env.GHOST_STORAGE_ADAPTER_S3_ENDPOINT || endpoint || ''
this.serverSideEncryption = process.env.GHOST_STORAGE_ADAPTER_S3_SSE || serverSideEncryption || ''
this.s3ForcePathStyle = Boolean(process.env.GHOST_STORAGE_ADAPTER_S3_FORCE_PATH_STYLE) || Boolean(forcePathStyle) || false
this.signatureVersion = process.env.GHOST_STORAGE_ADAPTER_S3_SIGNATURE_VERSION || signatureVersion || 'v4'
this.acl = process.env.GHOST_STORAGE_ADAPTER_S3_ACL || acl || 'public-read'
}
delete (fileName, targetDir) {
const directory = targetDir || this.getTargetDir(this.pathPrefix)
return new Promise((resolve, reject) => {
this.s3()
.deleteObject({
Bucket: this.bucket,
Key: stripLeadingSlash(join(directory, fileName))
}, (err) => err ? resolve(false) : resolve(true))
})
}
exists (fileName, targetDir) {
return new Promise((resolve, reject) => {
this.s3()
.getObject({
Bucket: this.bucket,
Key: stripLeadingSlash(join(targetDir, fileName))
}, (err) => err ? resolve(false) : resolve(true))
})
}
s3 () {
const options = {
bucket: this.bucket,
region: this.region,
signatureVersion: this.signatureVersion,
s3ForcePathStyle: this.s3ForcePathStyle
}
// Set credentials only if provided, falls back to AWS SDK's default provider chain
if (this.accessKeyId && this.secretAccessKey) {
options.credentials = new AWS.Credentials(this.accessKeyId, this.secretAccessKey)
}
if (this.endpoint !== '') {
options.endpoint = this.endpoint
}
return new AWS.S3(options)
}
save (image, targetDir) {
const directory = targetDir || this.getTargetDir(this.pathPrefix)
return new Promise((resolve, reject) => {
Promise.all([
this.getUniqueFileName(image, directory),
readFileAsync(image.path)
]).then(([ fileName, file ]) => {
let config = {
ACL: this.acl,
Body: file,
Bucket: this.bucket,
CacheControl: `max-age=${30 * 24 * 60 * 60}`,
ContentType: image.type,
Key: stripLeadingSlash(fileName)
}
if (this.serverSideEncryption !== '') {
config.ServerSideEncryption = this.serverSideEncryption
}
this.s3()
.putObject(config, (err, data) => err ? reject(err) : resolve(`${this.host}/${fileName}`))
})
.catch(err => reject(err))
})
}
serve () {
return (req, res, next) =>
this.s3()
.getObject({
Bucket: this.bucket,
Key: stripLeadingSlash(stripEndingSlash(this.pathPrefix) + req.path)
})
.on('httpHeaders', (statusCode, headers, response) => res.set(headers))
.createReadStream()
.on('error', err => {
res.status(404)
next(err)
})
.pipe(res)
}
read (options) {
options = options || {}
return new Promise((resolve, reject) => {
// remove trailing slashes
let path = (options.path || '').replace(/\/$|\\$/, '')
// check if path is stored in s3 handled by us
if (!path.startsWith(this.host)) {
reject(new Error(`${path} is not stored in s3`))
}
path = path.substring(this.host.length)
this.s3()
.getObject({
Bucket: this.bucket,
Key: stripLeadingSlash(path)
}, (err, data) => err ? reject(err) : resolve(data.Body))
})
}
}
export default Store