Site icon WebDevStudios

How to Download FTP Files in WordPress

WordPress tutorials, WordPress how to, downloading FTP files, FTP files, downloading FTP files in WordPress, how to download FTP files in WordPress, learn to download FTP files in WordPress, WordPress FTP files, WordPress education, learn WordPress, WordPress 101, WordPress experts
Recently, I was given the opportunity to work on a really cool importing project that involved us pulling data from an FTP server and importing that into WordPress. It led me to look deeper into the Filesystem API; WordPress does this already, and I wanted to learn more.
There’s a couple of hurdles you have to get over if you’re not familiar with file manipulations, so let’s jump into it, and hopefully I can show you some neat tricks!

How does it work?

As of WordPress 4.5.2, the WP_Filesystem_FTPext class is located at wp-admin/includes/class-wp-filesystem-ftpext.php. The FTP class allows the developer to connect to an FTP server and abstracts out some of the common FTP commands you may not be aware of.

With this class, you can do the following directly on the server you’re connected to:

There are also some file attribute-related methods as well that allow you to:

Simply put, this is a really robust class for abstracting typical FTP operations.

Okay, so what to do with it?

As you know, WordPress already utilizes this FTP class to do updates, install plugins, etc. However, in this special case, we needed to get the file from a remote server, insert it as an attachment, and later import that file.

The first immediate goal is to connect to FTP so we can get what we need. For the purposes of this article, we’re going to just use a simple singleton class. So here you go!

<?php

class JW_Tests {

    /**
     * Instance of JW_Tests
     * @var JW_Tests
     */
    public static $instance = null;

    public static function init() {
        if ( null == self::$instance ) {
            self::$instance = new self();
        }

        return self::$instance;
    }

    public function hooks() {
        // All hooks here.
        add_action( 'init', array( $this, 'main' ) );
    }

    public function main() {
        // Fancy stuff here
    }
}

function jw_tests() {
    return JW_Tests::init();
}
add_action( 'plugins_loaded', array( jw_tests(), 'hooks' ) );

Now that we have the basic scaffolding setup, we need to jump into actually making the connection. We can do that in the main method of our new singleton.

I cannot stress this enough: You absolutely should NOT store the FTP credentials to this or ANY connection, either in the code, or even in the database. You have been warned!

We don’t want to fire this on EVERY init of WordPress, so adding a $_GET flag will allow us to execute a download for testing purposes.

        if ( ! isset( $_GET['download'] ) ) {
            return;
        }

Next, we have to make sure we have access to the classes we need, so we first check if these classes are available, and if not, we load them in. We’re going to need wp_tempnam() specifically for this occasion. Since we obviously need the FTPext class, it extends Filesystem_Base, and we can’t have one without the other.

        // First load the necessary classes
        if ( ! function_exists( 'wp_tempnam' ) ) {
            require_once( ABSPATH . 'wp-admin/includes/file.php' );
        }

        if ( ! class_exists( 'WP_Filesystem_Base' ) ) {
            require_once( ABSPATH . 'wp-admin/includes/class-wp-filesystem-base.php' );
        }

        if ( ! class_exists( 'WP_Filesystem_FTPext' ) ) {
            require_once( ABSPATH . 'wp-admin/includes/class-wp-filesystem-ftpext.php' );
        }

Now, in the past I had issues with the FS_CONNECT_TIMEOUT constant not being defined, so I went ahead and included that here (it’s required by the FTP class). I really didn’t see any sense in loading another entire class into memory just for one constant.

        // Typically this is not defined, so we set it up just in case
        if ( ! defined( 'FS_CONNECT_TIMEOUT' ) ) {
            define( 'FS_CONNECT_TIMEOUT', 30 );
        }

Once all that fancy stuff is done, and all checks have passed we setup our connection arguments, we can finally connect() to the ftp server (or bail if for some reason the connection failed).

                /**
         * You DO NOT want to hard-code this, these values are here specifically for
         * testing purposes.
         */
        $connection_arguments = array(
            'port' => 21,
            'hostname' => '127.0.0.1',
            'username' => '',
            'password' => '',
        );

        $connection = new WP_Filesystem_FTPext( $connection_arguments );
        $connected = $connection->connect();
        if ( ! $connected ) {
            return false;
        }

Great, we’re connected. Now what?

If you’re this far, you probably know of a file you want to download. Let’s download that file and import it as an attachment! Might want to grab a drink and a snack, because there’s going to be a decent amount of code ahead.

First things first, we need to make sure the file is indeed a file. To do this, we make use of the is_file() method located in our connection we made previously, and if not, we bail:

        $remote_file = "x.csv";
        // Yep, you can use paths as well.
        // $remote_file = "some/remote-file.txt";
        if ( ! $connection->is_file( $remote_file ) ) {
            return;
        }

Great, we have a file. Now you need to use the get_contents() method to grab the remote file’s contents into PHP memory, so we can push it elsewhere later.

        // Get the contents of the file into memory.
        $remote_contents = $connection->get_contents( $remote_file );
        if ( empty( $remote_contents ) ) {
            return;
        }

Now before I go further, let me rant a little.

For some context, let’s look at the get_contents()method of the WP_Filesystem_FTPextclass.

The method does the following in order:

  1. Uses wp_tempnam() to create a temporary file
  2. Opens the file with fopen, if not, it will bail
  3. Runs ftp_fget (PHP’s native FTP method) to stream the remote file into the new temporary file, bail if it cannot
  4. Rewinds the temp file pointer back to zero (kinda like a VHS tape…yeah, I said it!)
  5. Now, it reads the entire temp file into memory and returns it, kinda like file_get_contents()

So, what’s my gripe? We don’t have access to this temp file name, at all. Sure, we could re-create the entire method of streaming the file just as the method currently does, but why re-invent the wheel if it’s already spinning? I smell a patch request coming! (View the WP_Filesystem_FTPext::get_contents() source to see what I mean.) You’ll see why this is a problem below.

Now that we have the remote contents in memory, we need to create our OWN file… despite the fact it was already created by core in the above step. We just can’t access it, which sucks. You’ll notice in the below snippet that we’re checking if the file is writable; this is because we’re also using file_put_contents() to push our data in memory into the file. If, for some reason, pushing the content fails, or the temp file isn’t writable, we need to bail and cleanup after ourselves with unlink() –be kind to the server.

        // Create a temporary file to store our data.
        $temp_file = wp_tempnam( $remote_file );
        if ( ! is_writable( $temp_file ) || false === file_put_contents( $temp_file, $remote_contents ) ) {
            unlink( $temp_file );
            return;
        }

For those who may not know, unlink() will delete a file in PHP given the absolute path. Always make sure you clean up after yourself!

We need a bit more data for the side-load functionality we’re getting ready to dive into. The codex for wp_handle_sideload() DOES actually hard-code the mime type, but in this example, I’m using wp_check_filetype() so we can check the mime type against any extra registered types. If for some reason the type isn’t found for our file extension (in this case CSV), we gracefully exit, and again, clean up after ourselves.

        // Optimally you want to check the filetype against a WordPress method, or you can hard-code it.
        $mime_data = wp_check_filetype( $remote_file );
        if ( ! isset( $mime_data['type'] ) ) {
            // WE just don't have a type registered for this attachment
            unlink( $temp_file ); // Cleanup
            return;
        }

Woohoo, we’re almost there! Next up is the easy part: The side-load into the uploads directory.

These file arrays were copied from the sideload example in the codex; once again, no need to re-invent the wheel. That said, there are exceptions in the code. For instance, $temp_file, which we created earlier, already exists, and we also grabbed the mime-type from core, instead of hard-coding it, and finally we can also use the pre-made file string which we defined above, as the name of the side-loaded file.

        /**
         * The following arrays are pretty much a copy/paste from the Codex, no need
         * to re-invent the wheel.
         * @link https://codex.wordpress.org/Function_Reference/wp_handle_sideload#Examples
         */
        $file_array = array(
            'name'     => basename( $remote_file ),
            'type'     => $mime_data['type'],
            'tmp_name' => $temp_file,
            'error'    => 0,
            'size'     => filesize( $temp_file ),
        );

        $overrides = array(
            'test_form'   => false,
            'test_size'   => true,
            'test_upload' => true,
        );

        // Side loads the content into the wp-content/uploads directory.
        $sideloaded = wp_handle_sideload( $file_array, $overrides );
        if ( ! empty( $sideloaded['error'] ) ) {
            return;
        }

If no errors happened, you should get an array back like this from the wp_handle_sideload() method:

array(
    'file' => '/var/www/example.com/wp-content/uploads/2016/08/x.csv'
    'url' => 'http://example.com/wp-content/uploads/2016/08/x.csv'
    'type' => 'text/csv'
)

Finally, we’re at the end of the line. Up to this point we’ve successfully downloaded a file from an FTP server, stored it as a temporary file, and utilized WordPress core methods to side-load it. But wait, there’s more….what?!?! I know that’s what you’re thinking! Bear with me here.

WordPress does not automatically ‘know’ files are in the uploads directory. This is what the database is for–so in order for you to use your newly downloaded file as an attachment, we need to create an attachment. It can be done in just a few lines of code:

        // Will return a 0 if for some reason insertion fails.
        $attachment_id = wp_insert_attachment( array(
            'guid'           => $sideloaded['url'], // wp_handle_sideload() will have a URL array key which is the absolute URL including HTTP
            'post_mime_type' => $sideloaded['type'], // wp_handle_sideload() will have a TYPE array key, so we use this in case it was filtered somewhere
            'post_title'     => preg_replace( '/\.[^.]+$/', '', basename( $remote_file ) ), // Again copy/paste from codex
            'post_content'   => '',
            'post_status'    => 'inherit',
        ), $sideloaded['file'] ); // wp_handle_sideload() will have a file array key, so we use this in case it was filtered

        // SUCCESSSSSSSSS!!!!!!!!!!!

This is where we make use of the array data we got back from wp_handle_sideload(). After all, we want to use what WordPress gives us–if for some reason down the road something changes, or is filterable.

That’s all, folks!

So we’ve successfully downloaded from a remote FTP server and created an attachment for the downloaded file. This article only skims the surface of what’s possible with FTP downloads (especially in WordPress). You can do SO much more–download remote product CSV’s for import, FTP image downloads…just think about it!

Overall, I hope this at least opened your eyes to some possibilities and give you that spark of interest to create something awesome. At the very least I’d like to hear from you: What would you do differently? Did you like this article? Let me know! Let’s grow as developers, together!

Full source

You didn’t think I forgot to give you the source did you? Enjoy.

Exit mobile version