Search code examples
herokufile-uploadsftpstrapi

How can I use SFTP as file upload provider for strapi?


I am hosting Strapi on Heroku which means I cannot have any files saved locally on the server as Heroku does not allow persisting them.

Since I already have another server with enough space for files I would like to upload the data there via sftp.

I have found the following npm package: strapi-provider-upload-sftp-v2 but I realized some of the readme seems to be outdated as the config does not have to be placed in config/settings.json but rather into config/plugins.json as stated in the strapi docs

But using this package the files where not saved into the appropriate folder (according to baseUrl).

Ex.: If baseUrl = '/my/folder/' => File is uploaded to root folder with the name 'C:\my\folder\filename.ext'

Also I cannot connect using SSH Keys, only using username and password.

And finally, the uploaded files do not have any preview images shown in strapi even though all different sizes had been generated and uploaded via sftp.


Solution

  • Here is how I solved it now:

    I wrote my own provider based on the npm package strapi-provider-upload-sftp-v2 with the following changes:

    • Instead of using 'path.resolve()' to build the path I simply concatenate the basePath with the file name which results in the relative path that sftp needs
    • In order to enable sftp authentication through ssh keys I included the privateKey and passphrase in the options argument of sftp.connect(). This works directly as the used library 'ssh2-sftp-client' supports these options.
    • In order to have thumbnails show up you need to update the content security policy in 'middlewares.js'.
    • I also added a function for the upload provider called 'uploadStream' as strapi seems to expect and by default use this one instead of the existing 'upload' function.

    Here is the code:

    providers/sftp-upload-provider/index.js

    const getSFTPConnection = require('./getSFTPConnection');
    
    module.exports = {
      init: config => {
        const { host, port, user, password, baseUrl, basePath } = config;
    
        const connection = async () => getSFTPConnection(host, port, user, password);
    
        const uploadFile = async (file, isBuffer) => {
          const sftp = await connection();
          const files = await sftp.list(basePath);
    
          let fileName = `${file.hash}${file.ext}`;
          let c = 0;
    
          const hasName = f => f.name === fileName;
    
          // scans directory files to prevent files with the same name
          while (files.some(hasName)) {
            c += 1;
            fileName = `${file.hash}(${c})${file.ext}`;
          }
    
          const path = basePath + fileName
          const fileContent = isBuffer ? file.buffer : file.stream
          try {
            await sftp.put(fileContent, path)
          } catch (e) {
            console.error(e);
          }
    
          /* eslint-disable no-param-reassign */
          file.public_id = fileName;
          file.url = `${baseUrl}${fileName}`;
          /* eslint-enable no-param-reassign */
    
          await sftp.end();
    
          return file;
        }
    
        return {
          upload: file => {
            return uploadFile(file, true)
          },
          uploadStream: file => {
            return uploadFile(file, false)
          },
          delete: async file => {
            const sftp = await connection();
    
            try {
              await sftp.delete(`${basePath}/${file.hash}${file.ext}`);
            } catch (e) {
              console.error(e);
            }
    
            await sftp.end();
    
            return file;
          },
        };
      },
    };
    

    providers/sftp-upload-provider/getSFTPConnection.js

    const SFTP = require('ssh2-sftp-client');
    
    /**
     * Returns the connection with a SFTP host.
     *
     * @param {string} host
     * @param {string | number} port
     * @param {string} username
     * @param {string} privateKey
     * @param {string} passphrase
     *
     * @returns {Promise}
     */
    
    const getSFTPConnection = async (host, port, username, privateKey, passphrase) => {
      const sftp = new SFTP();
    
      try {
        await sftp.connect({ host, port, username, privateKey, passphrase});
      } catch (e) {
        console.error(e);
      }
    
      return sftp;
    }
    
    module.exports = getSFTPConnection;
    

    providers/sftp-upload-provider/package.json

    {
      "name": "sftp-upload-provider",
      "version": "1.0.2",
      "description": "SFTP provider for Strapi CMS file upload.",
      "main": "index.js",
      "dependencies": {
        "ssh2-sftp-client": "^2.5.0"
      }
    }
    

    config/middlewares.js

    module.exports = [
      'strapi::errors',
      {
        name: 'strapi::security',
        config: {
          contentSecurityPolicy: {
            useDefaults: true,
            directives: {
              'connect-src': ["'self'", 'https:'],
              'img-src': ["'self'", 'data:', 'blob:', `${process.env.SFTP_BASEURL}`],
              'media-src': ["'self'", 'data:', 'blob:', `${process.env.SFTP_BASEURL}`],
              upgradeInsecureRequests: null,
            },
          },
        },
      },
      'strapi::cors',
      'strapi::poweredBy',
      'strapi::logger',
      'strapi::query',
      'strapi::body',
      'strapi::session',
      'strapi::favicon',
      'strapi::public',
    ];
    

    In package.json you need to add your new provider as dependency

    "sftp-upload-provider": "file:providers/sftp-upload-provider"