Skip to content

Commit

Permalink
created storage adapter
Browse files Browse the repository at this point in the history
  • Loading branch information
betschki committed Dec 11, 2023
0 parents commit 4eaf1ac
Show file tree
Hide file tree
Showing 4 changed files with 253 additions and 0 deletions.
41 changes: 41 additions & 0 deletions README.md
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`.
12 changes: 12 additions & 0 deletions configuration.sample.json
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"
}
}
}
168 changes: 168 additions & 0 deletions index.js
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;
32 changes: 32 additions & 0 deletions package.json
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"
}

0 comments on commit 4eaf1ac

Please sign in to comment.