import/export

Migrating users and profiles with the Migrate module

At Torchbox we’ve been working a lot with the Migrate module recently. It’s a framework for representing relationships in the data you want to import; both relationships between bits of data, and also relationships between the data and relevant Drupal entities. It used to be a GUI-driven system, but now it seems quite code-heavy.

Which suits us fine, certainly for one-off imports which can be built most straightforwardly by developers. We had to write an import for around 4600 users, plus data which we wanted to store in three Profile2 profiles (address, subscriptions, personal information.)

Migrate did a lot of the heavy lifting for us, along with some code examples. The most useful one was this example import module, started by wusel and edited by (among others) Profile2’s maintainer joachim. As a thankyou I’ve contributed to that post a bit, tidying things up and incorporating some of the lessons we learned.

The results? A flawless migration, including mapping many CSV columns to three multi-valued entity fields for the subscriptions (basically an ORM mapping.) And it was blisteringly fast: user importing on a reasonably high-spec server was at the rate of 12592/min, and we had a peak profile import of 9954/min.

Exporting your tables via features in a dozen lines

Pickling your friends is easy via the CTools exportables API

Stella's written an excellent blogpost on Using CTools in Drupal to make exportables, which feeds in so nicely to integrating your generic database table with Drupal Features that the module maintainers recommend it as an intro to hooking your module up through CTools and making database data exportable using it (there's also documentation in CTools itself but it's best used as a reference.)

Because Stella's post covers admin interfaces and helper functions, and has been made generic so people can adapt it, then it covers far more than just integration. So when I worked out just how tiny the absolute bare minimum you needed to integrate a database table with Features really was, I thought it was worth writing a blogpost which boils Features/CTools exportables down to their impressive distillates.

Here's my example. Imagine you're building a contacts module which just creates a database table through hook_schema. This table will be used to store your friends' contact details: at first, a unique nickname and their full name. The module does nothing more, so here's its contacts.info file:

name = Contacts
description = All my contacts
core = 6.x

And here's the contacts.module file:

// Empty file!

Tiny, isn't it? That's because all our clutter is in the database schema for now. So we use hook_schema to define the database schema, and hook_install/hook_uninstall to create and drop the table for our friends' information:

/**
 * Implementation of hook_install
 */
function contacts_install() {
  drupal_install_schema('contacts');
}
 
/**
 * Implementation of hook_install
 */
function contacts_uninstall() {
  drupal_install_schema('contacts');
}
 
/**
 * Implementation of hook_schema
 */
function contacts_schema() {
  $schema['contacts_friend'] = array(
    'description' => t('Friends'),
    'fields' => array(
      'nickname' => array(
        'description' => 'Unique nickname for friend',
        'type' => 'varchar',
        'length' => 32,
        'not null' => TRUE,
      ),
      'fullname' => array(
        'description' => 'Friend fullname',
        'type' => 'varchar',
        'length' => 128,
        'not null' => TRUE,
      ),
 
    'primary key' => array('nickname'),
  );
  return $schema;
}

This is all standard schema API stuff: if you're not sure what any of this means, check out the Schema API documentation for more information. Incidentally, good database maintainers might blanch at the idea of a text primary key. I feel your pain, but for this demo---and for a friendly Features admin interface---it's necessary, as we'll see below.

So now you've got your module. You haven't worried about features yet, but you suddenly realized you might want to export friends through Features, so you can import them into some other site. How do you do that? Well, you add nine lines of array code to your database schema: a new 'export' array key in the table schema with the following contents:

/**
 * Implementation of hook_schema
 */
function contacts_schema() {
  $schema['contacts_friend'] = array(
    'description' => t('Friends'),
 
    // This is all the CTools Exportables block
    'export' => array(
      // Unique key to identify an object
      'key' => 'nickname',
      // Object type identifier, e.g. "enemy" for your enemy exportables!
      'identifier' => 'friend',
      // Function hook name in Features dump
      'default hook' => 'default_contacts_myobj',
      'api' => array(
        // Which module "owns" these objects?
        'owner' => 'contacts',
        // Base name for Features include file
        'api' => 'default_contacts_friends',
        // Exportables API version numbers: black magic; woowoo
        'minimum_version' => 1,
        'current_version' => 1,
      ),
    ),
 
    'fields' => array(
      'nickname' => array(
        'description' => 'Unique nickname for friend',
        'type' => 'varchar',
        'length' => 32,
        'not null' => TRUE,
      ),
      'fullname' => array(
        'description' => 'Friend fullname',
        'type' => 'varchar',
        'length' => 128,
        'not null' => TRUE,
      ),
 
    'primary key' => array('nickname'),
  );
  return $schema;
}

And that's it, in the first instance. When you add some friends to that table---you'll have to do it with SQL or phpMySQL for the moment, unless you build an admin interface---they should automatically appear as exportables in the Features creation interface. The human-readable label will be their 'export' 'key', which is why we needed that key to be human-readable. You could have a unique serial ID as well, and impose a mere unique_key constraint on the nickname field, but Features don't work terribly well with serial IDs for now; you'd need to make sure CTools does not export that column.

Which leads me neatly onto the next example. Let's say you add a column that tells you whether you secretly hate each "friend" or not, and you don't want that getting exported out in case people find out what you think about them. This column is just an integer/boolean, so you add your field description array as usual... with one extra array key called, with similar boolean confusion as "not null", "no export":

/**
 * Implementation of hook_schema
 */
function contacts_schema() {
  $schema['contacts_friend'] = array(
    'description' => t('Friends'),
 
    'export' => array(
      // Unique key to identify an object
      'key' => 'nickname',
      // Object type identifier, e.g. "enemy" for your enemy exportables!
      'identifier' => 'friend',
      // Function hook name in Features dump
      'default hook' => 'default_contacts_myobj',
      'api' => array(
        // Which module "owns" these objects?
        'owner' => 'contacts',
        // Base name for Features include file
        'api' => 'default_contacts_friends',
        // Exportables API version numbers: black magic; woowoo
        'minimum_version' => 1,
        'current_version' => 1,
      ),
    ),
 
    'fields' => array(
      'nickname' => array(
        'description' => 'Unique nickname for friend',
        'type' => 'varchar',
        'length' => 32,
        'not null' => TRUE,
      ),
      'fullname' => array(
        'description' => 'Friend fullname',
        'type' => 'varchar',
        'length' => 128,
        'not null' => TRUE,
      ),
      'do_i_hate' => array(
        'description' => 'Do I secretly hate this person?',
        'type' => 'int',
        'not null' => FALSE,
        'default' => 0,
        'no export' => TRUE,
      ),
 
    'primary key' => array('nickname'),
  );
  return $schema;
}

Result? You can still pickle friends into a Feature---if you like that sort of thing---but the contents of any "no export" columns will not be present in the final Feature bundle.

And that's it. You can build interfaces and applications on top of this, but that's all you need to get your database contents up and running through features. All these code snippets should be runnable; that is, you can save them and run them as live code. If you can't, please let me know and I'll fix.

Blog category:

Importing from Wordpress to Drupal

The initial import from Wordpress was much easier than then having to run two blogs alongside each other for two weeks.

The first stage of building this site was to import the content from the previous site. Well, the first stage was actually to set the site up: to install Drupal and enable the relevant modules. At Torchbox we've got a custom install profile that does a lot of this for us, installing and configuring relevant modules and creating users and roles. The actual company profile does a lot more work than I needed, in fact, and I've had to pare it back a little so that I've got less to maintain and worry about.

Importing content from Wordpress was largely handled by... the Wordpress Import module. There's a Drupal heuristic that, if it's a problem that a few people have encountered in the past, there's probably a module for it. Wordpress instances provide an XML export file called a WXR file, which you put on the filesystem and the module can convert content, freetext tags, the category hierarchy and users/authors.

The one tweak I had to make to the module was required to import the article summaries or excerpts, the little "humourous" quotes that are intended for blog listings. These were present in the WXR file as the <excerpt:encoded> element, and the Wordpress Import module contains a nice utility function that meant I only had to add this code at around line 621 to bring in the excerpt as a CCK field:

$node["field_summary"][0] = array(
  "value" => wordpress_import_get_tag($post, 'excerpt:encoded'),
  "format" => $params['format'],
);

Overall importing from Wordpress was pretty smooth---thanks to both Wordpress and Drupal, to give both technologies their due---but having content on a beta site is a double-edged sword. It's great to be able to see broadly how the site is going to look when it goes live, with everything in place; on the other hand, it's disappointing to be able to see broadly how the site is going to break when it goes live. Content showed up all sorts of little bugs: missing, slightly quirky CSS formatting you'd forgotten about; oversensitive output filters; slightly wonky imported URL aliases that needed a visit to the database to fix.

As I iterated and tweaked the configuration on the Drupal site---with content already in place---I had effectively frozen the site development, and could no longer roll back and re-import as it would lose my configuration changes. The import itself meant that for a week or so I was running two sites in parallel, writing blogposts on both, and getting a bit flustered about it all.

I think it was the right, pragmatic decision to do that, even though it initially felt like a lot of overhead: writing some sort of module to do the configuration changes was possible, but didn't really suit the way I wanted to fiddle with the site rather than run a set of fixes; importing at the very last minute would have meant I'd not have found most of the little irritations until they were publicly visible; not putting content on the blog for a week wasn't really possible, what with Oxford Geek Night 14 rumbling on, and our most excellent sponsors all clamouring to give us stuff. I just wish, as always, that I'd had twice as much time to do it all in.

Tonight we're gonna parse like it's 1997

Opinions are like closing angle brackets: everyone's got one, but some stick out more than others, depending on your kerning

Via Sean McGrath comes a reasonably lucid and comprehensible redux of the argument about of whether or not the XML standard should (or should have) stipulated draconian error handling. I hope I'm not misrepresenting Avery when I boil a lot of it down to his three broad "real-world" examples to this:

  1. Not well-formed XML, produced by a legacy application that takes ages to fix, is rejected by draconian parser
  2. Not well-formed XML is accepted by a permissive parser
  3. Well-formed XML is accepted by draconian parser

and I hope he's also happy for me to then state that his argument consists broadly of the suggestion that 1 and 2 are together more likely than 3, hence permissive parsers obtain for you the lion's share of the "real world" parsing instances; or, if you prefer, via a slightly more complicated profit-and-loss argment, that making your parser permissive, and sanctioning permissive parsers, contributes a lower overall cost through lumbering us with poor legacy applications, divided up among all the parsing events, than having to fix those legacy applications.

However, an application of Postel's Law to the process of implementation should not be confused with being able to apply it to the original specification. And besides, do those examples really portray the real world, nearly twelve years after the argument first took place? How much XML is out there, and how much of it is bad XML, and how much of it remains bad XML for long enough for it to cause a problem? I don't think it's clear that draconian error handling in the wild has held back RSS syndication, Google Maps, web services, or RDF so much as that, beyond a certain tipping-point (say, 2002?) they've ensured the rapid takeup thereof (with the possible exception of RDF until recently, for its own reasons).

XML is unbelievably popular today, so popular and routine in its use that you almost don't know it's there in most applications. and I think---purely from my own experience---it's plausible to suggest that that's at least partly because consumption of XML is easy; in turn, this is because basic production quality is enforced.

HTML (SGML) is a format (specification) that, because of its messiness (its complex rules), its parsing permissiveness (its potential for misunderstandings), and a whole host of cultural reasons (ditto), was terribly hard to write reliable consumption software for. Even now, there's around half a dozen good browsers, and in part that's surely because of the entry barrier to writing browsers: permissive parsing of real-world mistakes remains a complex task.

I also have dim and partial knowledge of SGML in the old-skool publishing industry, where a licence for fully-featured SGML software could set you back tens of thousands of pounds six or seven years ago, and that price didn't seem to be heading down under market pressure. In comparison, XML parsing is cheap, easy, and ubiquitous. There are free and open-source CMS and blogging packages that can do it; I have access to dozens of command-line tools that can do it; publication, syndication and webservice consumption are things that happen, almost as though nature intended it that way. A lot of that must surely be down to XML's combination of rule simplicity and parser rigour. As Dave Winer says on the subject of Postel's Law and XML:

I yearn for just one market with low barriers to entry, so that products are differentiated by features, performance and price; not compatibility. Compatibility should be expressed in terms of formats, not products.... Anyway, the other half of Postel's Law is just as interesting, but so far no one is commenting. Think about it, if everyone followed the second half, the first half would be a no-op. You could be fully liberal in an afternoon or less.

Mark Pilgrim's history of draconianism versus tolerance seems to consist of a lot of tolerantists pontificating about what they've decided the "draconian" argument is: I can't believe that Tim Bray, even if he really were a lone voice, would have been such a reluctant paper tiger. But like the 1997 tolerantists, I've thus far waded in with my own interpretation of events. And despite dealing with XML on a daily basis, I find that during so many of the tasks I have to accomplish the XML layer is able to fade almost completely into the background.

Of all the problems I encounter at work well-formedness of XML happens very rarely, compared to those concerning the quality and stability of my own algorithms, application control flow, scaling and coping with heavy load, and logging and bailing out. Whether XML's ease of use is in 2009 is a result of the small rule set in XML making well-formedness easy, or the initial decision in favour of draconian parsing, all decided back in 1997, we'll probably never be able to tell. All that's certain is that there'll always be opinions about it, and somewhere in the rambling above is mine.

A WTF at the heart of your Drupal feed aggregation

Do try this at home, kids: but please have the decency to feel a little dirty about it.

Embedding JSON in XML. Hah, that's ridiculous, right? Almost as ridiculous as running a successful blog in .NET/ASP. Well, RSS can combine with JSON to quickly get a Drupal site to consume complex data structures over a webservice.

Drupal's core Aggregator module understands RSS2.0 with no tweaking, putting the text in the <description/> element into the content of quasi-node objects, so you can aggregate all sorts of syndicated content. You could build your own Google Reader if you liked that sort of thing, with articles from the BBC sitting alongside those from the Guardian.

So far so boring. And, on one level, it doesn't get much more interesting than that: Aggregator understands neither Atom XML (rich content) nor RSS that contains Dublin Core fields. There's therefore a limit to how much you can extend the actual XML format.

But what if you get a remote application to produce an RSS feed like this:

<?xml version="1.0" encoding="utf-8" ?>
<rss version="2.0">
  <channel>
    <title>Hello, world</title>
    <link>http://example.com</link>
    <description>Recent updates</description>
    <language>en</language>
    <item>
      <title>Sample JSON encoded content</title>
      <link>Foo</link>
      <description>
        {"text": "This is some lovely JSON text"}
      </description>
      <pubDate>Mon, 24 Nov 2008 22:07:03 +0000</pubDate>
      <guid isPermaLink="false">none</guid>
    </item>
  </channel>
</rss>

"What if?" Well, you get a quasi-node of content whose body contains the literal JSON text. Not terribly exciting. But Drupal's powerful themeing system means you can override the way that such content is .

Drop a file into your theme's directory called aggregator-item.tpl.php and containing the following:

<?php
$data = json_decode($content);
print $data->text;
?>

Voilà! You've unpacked the JSON data packet and accessed the content. And the packet, being JSON, can contain however much hierarchical data that you want. You could essentially encode whatever you liked at the webservice side and unpack it at the webconsumer side. You can't pickle objects very easily, unfortunately, but my recommendation is to avoid doing that sort of thing.

(You might need to empty your cache, if you've got any sort of zealous cacheing switched on. And this specific example will only work on PHP 5.2, unfortunately: json_decode() is a recent addition to the already-polluted default PHP namespace. You could use the PHP serialize() format if you've got an older version of PHP, or some other serialized data format that PHP can understand.)

If you were building all this from scratch, then of course you'd use either XML or JSON throughout, and not this weird hybrid solution. If you were building it from scratch. And if you are building it from scratch: let me know when you're done.

Blogging about the password anti-pattern, finally

If you think I'm behind the times with this post then just give me your GMail username and password so I can tell all your contacts how tardy I am!

Here's a basic rule of account security: you should never give your login details on website X, to a form on website Y. And here's a basic rule of etiquette: if you're running website Y, you should never ask people for their login details on website X. Well, you can do so, but only if you're happy about those same users giving their login details to your website Y to miscellaneous sites A, B, C and D.

I realise I'm late to the party with this---to the blog-about-it party if not to the read-about-it-and-nod-furiously party---but here's a few choice quotes on the matter:

Jeremy Keith on "the password anti-pattern":

... Asking users to input their email address and password from a third-party site like GMail or Yahoo Mail is completely unacceptable. Here’s why: It teaches people how to be phished....

More from Jeremy Keith, about a specific instance of anti-pattern abuse:

... The second step of the process involved handing over your Twitter username and password. This request was dutifully obeyed by the eager geeks.

Muppets.

This is a classic example of the password anti-pattern. And this time it bit the willing victims on the ass. My Name Is E used the credentials to log in to Twitter as that person and post a spammy message from their account....

Jeff Atwood:

... I'm sure Yelp means well. They just want to help me find my friends, doggone it! But the very nature of the request is incredibly offensive; they have effectively asked for the keys to my house in order to riffle through my address book....

... What happened to the fundamental tenet of security common sense that says giving out your password, under any circumstances, is a bad idea?

Simon Willison, in a comment on Jeff Atwood's post:

This is known as the password anti-pattern. As of a few days ago, it is completely inexcusable - Google, Microsoft and Yahoo! all provide address book APIs which allow sites to request your permission to scrape your address book without needing to ask for your password.

Brian Oberkirch, on how if you implement the anti-pattern you're slowly killing the open-social web:

... Please stop just thinking about yourself. When you ask people for their login and passwords for other services, you are fucking things up for the rest of us.

Spliticket running again with BeautifulSoup

Or, how I learned to stop parsing and love the soup

Ages ago Matthew Somerville emailed me to say that spliticket had fallen over. It's my hacky interface to his wiki page documenting split tickets, and ultimately it found the vagaries of even wiki-generated HTML a bit too hard to cope with.

At the time I built the HTML parser using core SAX-based HTML parsing, and it was horrible. SAX works in a basic sense, but you have to build your own internal state engine, track which elements have gone past while working out what to do with the current context, and even write rules for what to do when the underlying dumb parser encounters HTML entities: no mean feat when the document is peppered with &#8211; en dashes.

Not only was writing the rules initially a pain in the rear, but adding new rules and bugfixing the existing ones was even worse. But I lived with SAX, because I was deploying on shared hosting: I presumed that this was the best option available if I couldn't install any new shared libraries.

Not true! I've just rebuilt the entire parsing layer with Beautiful Soup, a Python HTML/XML parser library which (a) is available as a single file and (b) works out a decent HTML DOM tree from pretty much anything you throw at it.

Try it yourself, if you have to do any HTML parsing.It's astonishing; beautiful, in fact. I will never write another SAX parser ever again, which I'm sure I've said before.

Firefox/Sage bookmarks to Google Reader import

When OPML is OPML but it isn't OPML

Want to migrate your RSS bookmarks from Firefox (or its RSS-reading addon, Sage) to Google Reader? I did, just now.

Christopher Hinze has written a great Firefox addon that exports bookmarks to OPML 1.0. Unfortunately, OPML is a bit of an anything-goes specification. So although Hinze's plugin produces valid OPML, it isn't the same sort of valid OPML that Google Reader expects. Google Reader, in fact, gags and chokes on Hinze's OPML, and refuses to import it.

The main problem is that the <outline/> element, the basic hierarchical building block for OPML, will take any attributes. What does that mean in practice? Well, here's what Hinze's export produces:

<outline text="Coding">
  <outline type="link" text="Joel on Software" url="http://www.joelonsoftware.com/rss.xml" />
</outline>

and here's the result of Google Reader exporting its own store of RSS bookmarks:

<outline title="Coding" text="Coding">
  <outline text="drupal.org - Community plumbing"
    title="drupal.org - Community plumbing" type="rss"
    xmlUrl="http://drupal.org/node/feed" htmlUrl="http://drupal.org"/>
</outline>

To a computer, these are fundamentally two different data formats: the URLs are stored in different attributes, and there are attributes on each that either have different values or are not present on the other. Someone did a DTD for OPML: looking at those two apparently analogous fragments above you have to ask yourself why they bothered.

Help is at hand, though. This sort of problem is bread and butter to XSLT, and here's an XSL transform for converting Firefox OPML to Google Reader OPML. If you have xsltproc installed on your system, you would type:

xsltproc http://www.jpstacey.info/applications/google/ff2gr_opml.xsl bookmarks.opml > fixed_bookmarks.opml

Or download the XSLT---it's released under GPL2---and run it locally, changing that URL there to a local file location.

One thing to note: the XSLT will remove an outline wrapped around your bookmarks with title "Sage Feeds" (case-sensitive). So you can export that branch of your bookmarks, and the XSLT will strip the wrapper off and you won't import a load of bookmarks tagged "Sage Feeds". If you don't like this behaviour then either rename your Sage bookmark container, or learn XSLT: it won't kill you.

Library of Congress, Flickr'd to the max

Flickr is working with the Library of Congress on new project The Commons. Currently there are around three thousand photographs up there from two collections, and according to the Commons homepage they’re all copyright-free. More information in the relevant post on the Flickr blog.

This is wonderful news, especially because the collection is being released through a slightly adapted version of Flickr’s existing website. This means, apart from it being an established interface that millions of people already know vaguely how to use, that you can do all the Flickry things with the photos—dedicated Flickr-heads will hopefully give a more qualified response in due course—and that third-party tools should already be set up to work with the content. The meta information storage won’t particularly excite any Dublin-Core enthusiasts—a block of unstructured HTML in the standard Flickr notes field, plus of course Flickr tagging—but the whole project is still a fascinating experiment, and interesting for even the casual observer of American history. How exciting does it get? More exciting than the World of Mirth Shows?

Thinking offline for a moment, this hopefully presages more leaps forward in MLA culture. One of the first would be to remove the “NO PHOTOGRAPHS” signs from all museums. At the very least such signs could be more honest, and instead read “NO PHOTOGRAPHS; unless our security guards don’t catch you at it, in which case we’ll be blissful in our ignorance. Anyway, in five years time it’ll all be online so we don’t know why we’re bothering, to be honest….” On reflection, I suppose they would need bigger signs.

Blosxom to WordPress: tying up loose ends

A busy few weeks, but they’ve included an import from a Blosxom blog to a WordPress blog which is worth describing. There are a couple of established methods for importing the data, and I opted for the one that seemed the most modular. This was Eric Davis’ Import-Blosxom method, consisting of a PHP script on the WordPress side and a set of Blosxom flavour files which produce a feed compatible with RSS 2.0. This separation of Blosxom and WordPress behaviours meant that I could thoroughly test the former before proceeding with the latter.

It worked very well with practically no configuration or edits, but there were a few issues with the out-of-the-box behaviour of the import script:

  1. Unicode character entities were being escaped in titles, leading to the exposure of the alphanumeric code e.g. “Z&amp;#252;rich” instead of “Zürich”.
  2. Whitespace in post bodies is converted to hard newlines by WordPress, and so must be excised to avoid tags being broken e.g. ‘<a [newline] href=”…”>’ becoming ‘<a <br/> href=”…”>’.
  3. Multiple hierarchical categories are not supported (a known problem).
  4. Although categories are created and posts are linked to them, the number of posts that a category is used in is not incremented and hence the list of categories on the front-end has zero posts for each category(possibly owing to a change between WordPress versions of how this has been handled).

I’ve come up with a number of fixes that I’ve mentioned both to Davis and on the WordPress support forums. As they’ve been greeted with an eerie silence that I’ve found typical of such forums, I’ll put them up here instead.

To fix the first three problems I created rss_to_wp, a Blosxom plugin that, along with the standard interpolate_fancy package, you can use to wrap your title and category processing bits in the RSS2.0 flavour templates. Respectively, this plugin tackles the above problems by:

  1. Providing an interpolate_fancy method to unescape entities
  2. Normalizing any whitespace in the body of your Blosxom posts to single spaces
  3. Providing an interpolate_fancy method to convert a Blosxom-style category path into a set of category tags

You’ll need to change the Davis-recommended story.rss20 template to implement the two interpolation methods. I’ve made a sample available.

The final issue was a more knotty problem, as it was a bug in the script (possibly caused by WordPress’ handling of categories changing over time). It’s easily fixed by adding a few lines to the category-handling part of import-blosxom.php as follows:

294    if (!$exists)
295    {
296        $wpdb->query("INSERT INTO $wpdb->post2cat (post_id, category_id)
297                      VALUES ($post_id, $cat_id)");
298    }
299
300    // JPS' addition - increment count if cat ID exists
301    if ($cat_id) {
302        $wpdb->query("UPDATE $wpdb->categories SET category_count = category_count + 1 WHERE cat_ID = $cat_id");
303    }
304    // End JPS' addition

Exit gracefully: exporting and then importing—transporting?—works well if the two tasks are separable. That way the integrity of the exported data can be checked in its transitory state and any bugs worked out, before it’s imported into the new system. It’s certainly worthwhile backing up the target database for the import, as this lets you preserve any quirks of your target database if you have to dump all the imported data and start again. The standard WordPress install includes a plugin for doing this, but the command-line tool mysqldump is arguably more powerful.

Pages

Subscribe to RSS - import/export