Error Uploading Images with Admin API: 'Please select an image.'

Thanks Hannah! For some reason I was thinking that images weren’t in the API Client, but that was my mistake…

I tried it with the API Client and it worked, but I’m actually porting the code over to Google App Script so I can’t bring in packages. (that also made the JWT token generation pretty challenging!)

Good idea though to compare between my code and the API Client - I was able to finally get it working by pulling out the applicable code from there. I’m still not sure where my mistake was but must be in one of the headers as you suggested or how the form data is constructed.

Here’s the code that works in case anyone else comes across the same issue and can’t use the API Client for some reason:

// Create a token without the client
const jwt = require('jsonwebtoken');
const axios = require('axios');
const fs = require('fs');
const FormData = require('form-data');

// Admin API key goes here
const key = 'ADMIN_API';

// Split the key into ID and SECRET
const [id, secret] = key.split(':');

// Create the token (including decoding secret)
const token = jwt.sign({}, Buffer.from(secret, 'hex'), {
    keyid: id,
    algorithm: 'HS256',
    expiresIn: '5m',
    audience: `/v4/admin/`
});


const makeRequest = ({url, method, data, params = {}, headers = {}}) => {
    return axios({
        url,
        method,
        params,
        data,
        headers,
        maxContentLength: Infinity,
        paramsSerializer(parameters) {
            return Object.keys(parameters).reduce((parts, key) => {
                const val = encodeURIComponent([].concat(parameters[key]).join(','));
                return parts.concat(`${key}=${val}`);
            }, []).join('&');
        }
    }).then((res) => {
        return res.data;
    });
}


function getFormData(data) {
    let formData;

    if (data instanceof FormData) {
        return data;
    }

    if (data.file) {
        formData = new FormData();
        formData.append('file', fs.createReadStream(data.file));
        formData.append('purpose', data.purpose || 'image');

        if (data.ref) {
            formData.append('ref', data.ref);
        }

        return formData;
    }
}


function makeUploadRequest(resourceType, data, url) {
    const headers = {
        'Content-Type': `multipart/form-data; boundary=${data._boundary}`
    };

    return makeApiRequest({
        url: url,
        method: 'POST',
        body: data,
        headers
    }).then((apiData) => {
        if (!Array.isArray(apiData[resourceType])) {
            return apiData[resourceType];
        }
        if (apiData[resourceType].length === 1 && !apiData.meta) {
            return apiData[resourceType][0];
        }
    });
}


function makeApiRequest({url, method, body, queryParams = {}, headers = {}}) {

        headers = Object.assign({}, headers, {
            Authorization: `Ghost ${token}`
        });

        return makeRequest({
            url,
            method,
            data: body,
            params: queryParams,
            headers
        }).catch((err) => {
            /**
             * @NOTE:
             *
             * If you are overriding `makeRequest`, we can't garantee that the returned format is the same, but
             * we try to detect & return a proper error instance.
             */
            if (err.response && err.response.data && err.response.data.errors) {
                const props = err.response.data.errors[0];
                const toThrow = new Error(props.message);
                const keys = Object.keys(props);

                toThrow.name = props.type;

                keys.forEach((k) => {
                    toThrow[k] = props[k];
                });

                // @TODO: bring back with a better design idea. if you log the error, the stdout is hard to read
                //        if we return the full response object, which includes also the request etc.
                // toThrow.response = err.response;
                throw toThrow;
            } else {
                delete err.request;
                delete err.config;
                delete err.response;
                throw err;
            }
        });
    }

    let formData = getFormData({file: './image.jpg', ref: 'image-1.jpg'});
    makeUploadRequest('images', formData, 'http://localhost:2368/ghost/api/v4/admin/images/upload/')
    .then(response => console.log(response))
    .catch(error => console.error(error));