I'm working on putting together some handlers for pulling in lists of static files (such as static HTML) for migration. In the interest of making this as reusable as possible, it'd be great to know a few things.

1) Has anyone already begun work on this? How are you structuring it and what are you naming the files?
2) Would this be useful as an addition directly to the Migrate module, or should it be handled elsewhere? (migrate extras, elsewhere in contrib, etc.)
3) What would be the set of naming standards you'd suggest for classes?

Thus far I've put together a simple implementation that builds off of the MigrateList and MigrateItem classes to facilitate passing in a path and other options which will invoke file_scan_directory() to provide an index for migration and files to iterate through, keying off the relative path to the files as a unique identifier. I'll likely add a couple of other methods including some different ways of keying the index and using a http source.

Jeremiah

Comments

jerdavis’s picture

Here's what I've got thus far.


/**
 * @file
 * Support for migration from static file sources. 
 */

/**
 * Implementation of MigrateList, for retrieving a list of IDs to be migrated
 * from a directory. 
 */
class MigrateListStatic extends MigrateList {
  /**
   * A URI pointing to a path to begin scanning. 
   *
   * @var string
   */
  protected $listUri;

  protected $fileMask;

  protected $directoryOptions;


  public function __construct($list_uri, $file_mask = NULL, $options = array()) {
    parent::__construct();
    $this->listUri = $list_uri;
    $this->fileMask = $file_mask;
    $this->directoryOptions = $options;
  }

  /**
   * Our public face is the URI we're getting items from
   *
   * @return string
   */
  public function __toString() {
    return $this->listUri;
  }

  /**
   * Retrieve a list of files based on parameters passed for the migration.
   *
   * @return array
   */
  public function getIdList() {
    migrate_instrument_start("Retrieve $this->listUri");
    $files = file_scan_directory($this->listUri, $this->fileMask, $this->directoryOptions);
    migrate_instrument_stop("Retrieve $this->listUri");
    if (isset($files)) {
      return $this->getIDsFromFiles($files);
    }
    $migration = Migration::currentMigration();
    $migration->showMessage(t('Loading of !listuri failed:',
        array('!listuri' => $this->listUri)));
    return NULL;
  }

  /**
   * Given an array generated from file_scan_directory(), parse out the IDs for processing
   * and return them as an array.
   *
   * @param array $files
   *
   * @return array
   */
  protected function getIDsFromFiles(array $files) {
    $ids = array();
    foreach ($files as $file) {
      $ids[] = str_replace($this->listUri, '', (string) $file->uri);
    }
    return array_unique($ids);
  }

  /**
   * Return a count of all available IDs from the source listing.
   */
  public function computeCount() {
    $count = 0;
    $files = $this->getIdList();
    if ($files) {
      $count = count($files);
    }
    return $count;
  }
}

/**
 * Implementation of MigrateItem, for retrieving a file from the file system based on source directory and
 * an ID provided by a MigrateList class.
 */
class MigrateItemStatic extends MigrateItem {

  protected $sourceUri;

  protected $fileMask;

  public function __construct($source_uri) {
    parent::__construct();
    $this->sourceUri = $source_uri;
  }

  /**
   * Implementors are expected to return an object representing a source item.
   *
   * @param mixed $id
   *
   * @return stdClass
   */
  public function getItem($id) {
    $item_uri = $this->sourceUri . $id;
    // Get the file data at the specified URI
    $data = $this->loadFile($item_uri);
    if ($data) {
      $return = new stdClass;
      $return->html = $data;
      return $return;
    }
    else {
      $migration = Migration::currentMigration();
      $message =  t('Loading of !objecturi failed:', array('!objecturi' => $item_uri));
      $migration->getMap()->saveMessage(
        array($this->id), $message, MigrationBase::MESSAGE_ERROR);
      return NULL;
    }
  }

  /**
   * Default File loader.
   */
  protected function loadFile($item_uri) {
    $data = file_get_contents($item_uri);
    return $data;
  }
}

And an example implementation..


class TestMigration extends Migration {
  public function __construct() {
    parent::__construct();
    $this->description = t('Data migration from static shtml.');

    $this->map = new MigrateSQLMap($this->machineName,
      array(
        'sourceid' => array(
          'type' => 'varchar',
          'length' => 255,
          'not null' => TRUE,
        )
      ),
      MigrateDestinationNode::getKeySchema()
    );

    $fields = array(
      'title' => t('Title'),
      'body' => t('Body'),
    );

    $list_url = DRUPAL_ROOT . '/' . drupal_get_path('module', 'test_migrate') . '/htdocs';

    $this->source = new MigrateSourceList(new MigrateListStatic($list_url, '/index\..html$/'),
      new MigrateItemStatic($list_url), $fields);

    $this->destination = new MigrateDestinationNode('page');

    $this->addFieldMapping('title', 'title');
    $this->addFieldMapping('body', 'body')
         ->arguments(array('format' => 'full_html'));
  }

  public function prepareRow($row) {
    require_once drupal_get_path('module', 'test_migrate') . '/libraries/QueryPath/QueryPath.php';
    $row->title = htmlqp($row->html)->find('h1')->text();
    $row->body = htmlqp($row->html)->find('p')->html();
  }
}
moshe weitzman’s picture

I think that we have come across situations like this where we monitor a directory for files to process. Maybe Mike can recall. Kayak?

Anyway, this sounds like a worthy enhancement.

mikeryan’s picture

Yes, the idea of a file-based source plugin has been around for a while, just never quite got around to it... But, let's please use File rather than "Static", which may make sense for your particular application of "static" HTML files but doesn't reflect the generality of the concept. For example, MigrateListFile would be useful for reading a directory full of XML files.

Thanks!

Mark Theunissen’s picture

jerdavis: That's a coincidence, we're evaluating migration modules for importing a very large static HTML site, and obviously would love to use Migrate V2.

Let's see what we decide on and possibly work together with you on creating this functionality.

jerdavis’s picture

Sounds good!

Mark Theunissen’s picture

jerdavis: We needed to implement a proof of concept so I've taken your code above and done Mike's suggested changes, committing it to a sandbox fork of Migrate, branch html-files-source:

http://drupal.org/sandbox/marktheunissen/1242682

Not sure where to go from here, should we stabilise it in the fork before supplying patches to Migrate?

jerdavis’s picture

Mark,

What a coincidence, I'd done the same thing ;). http://drupal.org/sandbox/jerdavis/1232588

Yeah, we should stabilize in one of these and then provide patches back to Mike.

Jer

Mark Theunissen’s picture

Jer,

I've created a patch to the current files.inc implementation, and posted it here in an issue on your sandbox:

http://drupal.org/node/1279024

It allows the implementer to specify multiple directories to be scanned (in the case where you don't want an entire folder and all it's subfolders, but only a selection).

Should we continue there or here?

jerdavis’s picture

Thanks for the follow up. I'll review there and we can continue work in that issue queue if that works for you.

Mark Theunissen’s picture

StatusFileSize
new5.64 KB

On second thought, I'd like to see if Mike or Moshe will accept this patch in it's current form, as it's come a little way and we're using it quite successfully so perhaps it's time to move it into the mainstream.

Patch attached! This provides the original functionality plus it allows multiple directories to be specified, in the case where you want a single Migration class to pull files from separate directories.

Thanks.

jerdavis’s picture

Status: Active » Needs review

Updating to needs review, I'll try to get this in my personal list to check too.

sylus’s picture

Hello,

I have used this patch successfully to scan a directory and create a bunch of multilingual nodes (french and english language).

However I was wondering however if it is possible to extend this patch to include the ability to link the "tnid" between two different nodes for node translation. Conceptually I am at a bit of a loss for how to do this as when using the DestinationHandler I only have access to the current node being processed so can't link with a previous"tnid" value from the previous node.

I was curious if anyone thought about this problem and would have a potential solution?

Mark Theunissen’s picture

We do exactly that, in a two-pass system. While the migration runs, it build up information about the translation sets, and saves them to a custom table.

Then, we implement postImport() which does a pass over the migrate map table and uses the translation set data to manually update the tnid value for each node.

I don't think it's possible to do this in 1 pass.

mikeryan’s picture

Status: Needs review » Fixed

I don't have a good use case to test with at the moment, but based on a review of the code and faith;) I've committed this to D6 and D7. We can always refine it later...

Thanks!

Status: Fixed » Closed (fixed)

Automatically closed -- issue fixed for 2 weeks with no activity.

Mark Theunissen’s picture

I've written a blog post on this, with tips on handling legacy content:

http://fourkitchens.com/blog/2012/05/04/migrating-old-html-files-drupal

There's a working gist, too, with the basics of parsing HTML using QueryPath : https://gist.github.com/2596787