-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 4eaf1ac
Showing
4 changed files
with
253 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
|
||
# BunnyCDN Storage Adapter for Ghost | ||
|
||
BunnyCDNAdapter is a custom storage adapter for Ghost CMS, enabling you to store your media files using [BunnyCDN's storage solutions](https://bunny.net/storage/). | ||
|
||
This adapter implements the [Ghost documentation on storage adapters](https://ghost.org/docs/config/#storage-adapters). | ||
|
||
|
||
## Installation via git | ||
1. Navigate to your content folder within Ghost's root directory (probably `./content`). | ||
2. Create an `adapters/storage` folder inside the content folder, if it doesn't exist. | ||
3. Change into the directory with `cd adapters/storage`. | ||
4. Clone this git repository with `git clone https://github.com/betschki/ghost-bunny-cdn-storage.git bunny-cdn`. | ||
|
||
## Edit your configuration file | ||
In order for Ghost to use this storage adapter, you will need to edit your configuration file (e.g. `config.production.json`) and add the following: | ||
|
||
```json | ||
"storage": { | ||
"active": "bunny-cdn", | ||
"bunny-cdn": { | ||
"endpoint": "https://storage.bunnycdn.com", | ||
"storageZone": "your-storage-zone", | ||
"hostname": "cdn.your-pull-zone-hostname.com", | ||
"folder": "a-folder-you-want-to-use", | ||
"accessKey": "your-access-key" | ||
} | ||
} | ||
``` | ||
|
||
Edit the values according to your BunnyCDN configuration. You can also have a look at the `configuration.sample.json` file in this repository for a sample configuration. | ||
|
||
The final URL with this sample configuration would look like this: | ||
|
||
``` | ||
https://cdn.your-pull-zone-hostname.com/folder/file.jpg | ||
``` | ||
### Setting a folder | ||
This adapter has been developed for the usage within [magicpages.co](https://magicpages.co) customer sites. One requirement was the ability to set a folder, as you can see in the final URL example above. | ||
|
||
If you wish to save your files into the root directory of your storage zone, set `folder` in your configuration file to `null`. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
{ | ||
"storage": { | ||
"active": "bunny-cdn", | ||
"bunny-cdn": { | ||
"endpoint": "https://storage.bunnycdn.com", | ||
"storageZone": "your-storage-zone", | ||
"hostname": "cdn.your-pull-zone-hostname.com", | ||
"folder": "a-folder-you-want-to-use", | ||
"accessKey": "your-access-key" | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,168 @@ | ||
/* eslint-disable ghost/ghost-custom/no-native-error */ | ||
/* eslint-disable max-lines */ | ||
/* eslint-disable ghost/filenames/match-exported-class */ | ||
const BaseAdapter = require('ghost-storage-base'); | ||
const fs = require('fs'); | ||
|
||
class BunnyCDNAdapter extends BaseAdapter { | ||
constructor(config) { | ||
super(); | ||
|
||
this.options = config || {}; | ||
this.options.endpoint = | ||
this.options.endpoint || 'https://storage.bunnycdn.com'; | ||
|
||
this.apiHeaders = { | ||
AccessKey: this.options.accessKey | ||
}; | ||
} | ||
|
||
/** | ||
* Checks whether a file exists on BunnyCDN | ||
* @param {string} filename | ||
* @param {string} targetDir | ||
* @returns {Promise<boolean>} | ||
*/ | ||
async exists(filename, targetDir) { | ||
const files = await this.list(targetDir); | ||
return files.includes(filename); | ||
} | ||
|
||
/** | ||
* Saves a file to BunnyCDN | ||
* @param {object} image - Image object with 'name' and 'path' properties | ||
* @returns {Promise<string>} URL of the saved file | ||
*/ | ||
async save(image) { | ||
if (!this.isValidImage(image)) { | ||
throw new Error( | ||
'Invalid image object. Image must have name and path.' | ||
); | ||
} | ||
|
||
const uniqueFilename = this.generateUniqueFilename(image.name); | ||
const uploadUrl = this.constructBunnyCDNUrl(uniqueFilename); | ||
const fileStream = fs.createReadStream(image.path); | ||
|
||
try { | ||
await this.performUpload(uploadUrl, fileStream); | ||
return this.constructDownloadUrl(uniqueFilename); | ||
} catch (error) { | ||
throw new Error( | ||
`Error during file save operation: ${error.message}` | ||
); | ||
} | ||
} | ||
|
||
/** | ||
* Ghost calls .serve() as part of its middleware stack, | ||
* and mounts the returned function as the middleware for | ||
* serving images | ||
* @returns {function} | ||
*/ | ||
serve() { | ||
return function customServe(req, res, next) { | ||
next(); | ||
}; | ||
} | ||
|
||
/** | ||
* Deletes a file from BunnyCDN | ||
* @param {string} filename | ||
* @returns {Promise<void>} | ||
*/ | ||
async delete(filename) { | ||
try { | ||
await fetch(this.constructBunnyCDNUrl(filename), { | ||
method: 'DELETE', | ||
headers: this.apiHeaders | ||
}); | ||
} catch (error) { | ||
throw new Error(`Error deleting file: ${error.message}`); | ||
} | ||
} | ||
|
||
/** | ||
* Reads a file from BunnyCDN | ||
* @param {string} filename | ||
* @returns {Promise<Buffer>} | ||
*/ | ||
async read(filename) { | ||
try { | ||
return fetch(this.constructBunnyCDNUrl(filename), { | ||
method: 'GET', | ||
headers: this.apiHeaders | ||
}); | ||
} catch (error) { | ||
throw new Error(`Error reading file: ${error.message}`); | ||
} | ||
} | ||
|
||
/** | ||
* Returns a list of files in a directory | ||
* @param {string} targetDir | ||
* @returns {Promise<string[]>} | ||
*/ | ||
async list(targetDir) { | ||
const folderPath = this.options.folder ? `/${this.options.folder}` : ''; | ||
try { | ||
const response = await fetch( | ||
`${this.options.endpoint}/${this.options.storageZone}${folderPath}/${targetDir}`, | ||
{ | ||
method: 'GET', | ||
headers: this.apiHeaders | ||
} | ||
); | ||
|
||
if (!response.ok) { | ||
throw new Error(`Error fetching files: ${response.statusText}`); | ||
} | ||
|
||
const json = await response.json(); | ||
return json; | ||
} catch (error) { | ||
throw new Error(`Error fetching files: ${error.message}`); | ||
} | ||
} | ||
|
||
isValidImage(image) { | ||
return image && image.name && image.path; | ||
} | ||
|
||
generateUniqueFilename(originalName) { | ||
return `${Date.now()}-${originalName}`; | ||
} | ||
|
||
constructBunnyCDNUrl(filename) { | ||
const folderPath = this.options.folder ? `/${this.options.folder}` : ''; | ||
return `${this.options.endpoint}/${this.options.storageZone}${folderPath}/${filename}`; | ||
} | ||
|
||
constructDownloadUrl(filename) { | ||
const folderPath = this.options.folder ? `/${this.options.folder}` : ''; | ||
return `https://${this.options.hostname}${folderPath}/${filename}`; | ||
} | ||
|
||
/** | ||
* Performs the actual upload to BunnyCDN | ||
* @param {string} url | ||
* @param {fs.ReadStream} fileStream | ||
* @returns {Promise<Response>} | ||
*/ | ||
async performUpload(url, fileStream) { | ||
try { | ||
return await fetch(url, { | ||
method: 'PUT', | ||
headers: { | ||
...this.apiHeaders, | ||
'Content-Type': 'application/octet-stream' | ||
}, | ||
body: fileStream | ||
}); | ||
} catch (error) { | ||
throw new Error(`Error during upload: ${error.message}`); | ||
} | ||
} | ||
} | ||
|
||
module.exports = BunnyCDNAdapter; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
{ | ||
"name": "ghost-bunny-cdn-storage", | ||
"version": "1.0.0", | ||
"description": "BunnyCDNAdapter is a custom storage adapter for Ghost CMS, enabling you to store your media files using BunnyCDN's storage solutions.", | ||
"main": "index.js", | ||
"scripts": { | ||
"test": "echo \"Error: no test specified\" && exit 1" | ||
}, | ||
"repository": { | ||
"type": "git", | ||
"url": "git+https://github.com/betschki/ghost-bunny-cdn-storage.git" | ||
}, | ||
"keywords": [ | ||
"ghost", | ||
"ghost", | ||
"cms", | ||
"storage", | ||
"adapter", | ||
"bunny", | ||
"cdn", | ||
"storage", | ||
"bunnycdn", | ||
"bunny", | ||
"magicpages" | ||
], | ||
"author": "Jannis Fedoruk-Betschki", | ||
"license": "MIT", | ||
"bugs": { | ||
"url": "https://github.com/betschki/ghost-bunny-cdn-storage/issues" | ||
}, | ||
"homepage": "https://github.com/betschki/ghost-bunny-cdn-storage#readme" | ||
} |