OSDN Git Service

packages cache by caching wp http api
authorFrancesco Laffi <francesco.laffi@gmail.com>
Tue, 30 Jul 2013 08:14:17 +0000 (10:14 +0200)
committerFrancesco Laffi <francesco.laffi@gmail.com>
Thu, 10 Oct 2013 19:43:45 +0000 (21:43 +0200)
composer.json
php/WP_CLI/CommandWithUpgrade.php
php/WP_CLI/FileCache.php [new file with mode: 0644]
php/WP_CLI/WpHttpCacheManager.php [new file with mode: 0644]
php/class-wp-cli.php
php/commands/plugin.php
php/commands/theme.php

index 96170b9..3531bcc 100644 (file)
@@ -12,7 +12,8 @@
                "wp-cli/php-cli-tools": "0.9.3",
                "mustache/mustache": "~2.4",
                "rhumsaa/array_column": "~1.1",
-               "rmccue/requests": "~1.6"
+               "rmccue/requests": "~1.6",
+        "symfony/finder": "~2.3"
        },
        "require-dev": {
                "symfony/finder": "~2.3"
index 9d23c5a..c1054e2 100644 (file)
@@ -208,7 +208,7 @@ abstract class CommandWithUpgrade extends \WP_CLI_Command {
                        \WP_CLI::line( "Available {$this->item_type} updates:" );
 
                        \WP_CLI\Utils\format_items( 'table', $items_to_update,
-                               array( 'name', 'status', 'version' ) );
+                               array( 'name', 'status', 'version', 'update_version' ) );
 
                        return;
                }
@@ -217,6 +217,10 @@ abstract class CommandWithUpgrade extends \WP_CLI_Command {
 
                // Only attempt to update if there is something to update
                if ( !empty( $items_to_update ) ) {
+                       $cache_manager = \WP_CLI::get_http_cache_manager();
+                       foreach ($items_to_update as $item) {
+                               $cache_manager->whitelist_package($item['update_package'], $this->item_type, $item['name'], $item['update_version']);
+                       }
                        $upgrader = $this->get_upgrader( $assoc_args );
                        $result = $upgrader->bulk_upgrade( wp_list_pluck( $items_to_update, 'update_id' ) );
                }
@@ -276,6 +280,18 @@ abstract class CommandWithUpgrade extends \WP_CLI_Command {
                return isset( $update_list->response[ $slug ] );
        }
 
+       /**
+        * Get the available update info
+        *
+        * @param string $slug The plugin/theme slug
+        *
+        * @return array|null
+        */
+       protected function get_update_info( $slug ) {
+               $update_list = get_site_transient( $this->upgrade_transient );
+               return isset( $update_list->response[ $slug ] ) ?  (array) $update_list->response[ $slug ] : null;
+       }
+
        private $map = array(
                'short' => array(
                        'inactive' => 'I',
diff --git a/php/WP_CLI/FileCache.php b/php/WP_CLI/FileCache.php
new file mode 100644 (file)
index 0000000..65ee748
--- /dev/null
@@ -0,0 +1,329 @@
+<?php
+
+/*
+ * This file is heavily inspired and use code from Composer(getcomposer.org),
+ * in particular Composer/Cache and Composer/Util/FileSystem from 1.0.0-alpha7
+ *
+ * The original code and this file are both released under MIT license.
+ *
+ * The copyright holders of the original code are:
+ * (c) Nils Adermann <naderman@naderman.de>
+ *     Jordi Boggiano <j.boggiano@seld.be>
+ */
+
+namespace WP_CLI;
+
+use Symfony\Component\Finder\Finder;
+
+/**
+ * Reads/writes to a filesystem cache
+ */
+class FileCache {
+
+       /**
+        * @var string cache path
+        */
+       protected $root;
+       /**
+        * @var bool
+        */
+       protected $enabled = true;
+       /**
+        * @var int files time to live
+        */
+       protected $ttl;
+       /**
+        * @var int max total size
+        */
+       protected $maxSize;
+       /**
+        * @var string key allowed chars (regex class)
+        */
+       protected $whitelist;
+
+       /**
+        * @param string $cacheDir   location of the cache
+        * @param int    $ttl        cache files default time to live (expiration)
+        * @param int    $maxSize    max total cache size
+        * @param string $whitelist  List of characters that are allowed in path names (used in a regex character class)
+        */
+       public function __construct( $cacheDir, $ttl, $maxSize, $whitelist = 'a-z0-9.' ) {
+               $this->root = rtrim( $cacheDir, '/\\' ) . '/';
+               $this->ttl = (int) $ttl;
+               $this->maxSize = (int) $maxSize;
+               $this->whitelist = $whitelist;
+
+               if ( !$this->ensure_dir_exists( $this->root ) ) {
+                       $this->enabled = false;
+               }
+
+       }
+
+       /**
+        * Cache is enabled
+        *
+        * @return bool
+        */
+       public function is_enabled() {
+               return $this->enabled;
+       }
+
+       /**
+        * Cache root
+        *
+        * @return string
+        */
+       public function get_root() {
+               return $this->root;
+       }
+
+
+       /**
+        * Check if a file is in cache and return its filename
+        *
+        * @param string $key cache key
+        * @param int    $ttl time to live
+        * @return bool|string filename or false
+        */
+       public function has( $key, $ttl = null ) {
+               if ( !$this->enabled ) {
+                       return false;
+               }
+
+               $filename = $this->filename( $key );
+
+               if ( !file_exists( $filename ) ) {
+                       return false;
+               }
+
+               // use ttl param or global ttl
+               if ( $ttl === null ) {
+                       $ttl = $this->ttl;
+               } elseif ( $this->ttl > 0 ) {
+                       $ttl = min( (int) $ttl, $this->ttl );
+               } else {
+                       $ttl = (int) $ttl;
+               }
+
+               //
+               if ( $ttl > 0 && filemtime( $filename ) + $ttl < time() ) {
+                       if ( $this->ttl > 0 && $ttl >= $this->ttl ) {
+                               unlink( $filename );
+                       }
+                       return false;
+               }
+
+               return $filename;
+       }
+
+       /**
+        * Write to cache file
+        *
+        * @param string $key      cache key
+        * @param string $contents file contents
+        * @return bool
+        */
+       public function write( $key, $contents ) {
+               $filename = $this->prepare_write( $key );
+
+               if ( $filename ) {
+                       return file_put_contents( $filename, $contents ) && touch( $filename );
+               } else {
+                       return false;
+               }
+       }
+
+       /**
+        * Read from cache file
+        *
+        * @param string $key cache key
+        * @param int    $ttl time to live
+        * @return bool|string file contents or false
+        */
+       public function read( $key, $ttl = null ) {
+               $filename = $this->has( $key, $ttl );
+
+               if ( $filename ) {
+                       return file_get_contents( $filename );
+               } else {
+                       return false;
+               }
+       }
+
+       /**
+        * Copy a file into the cache
+        *
+        * @param string $key    cache key
+        * @param string $source source filename
+        * @return bool
+        */
+       public function import( $key, $source ) {
+               $filename = $this->prepare_write( $key );
+
+               if ( $filename ) {
+                       return copy( $source, $filename ) && touch( $filename );
+               } else {
+                       return false;
+               }
+       }
+
+       /**
+        * Copy a file out of the cache
+        *
+        * @param string $key    cache key
+        * @param string $target target filename
+        * @param int    $ttl    time to live
+        * @return bool
+        */
+       public function export( $key, $target, $ttl = null ) {
+               $filename = $this->has( $key, $ttl );
+
+               if ( $filename ) {
+                       return copy( $filename, $target );
+               } else {
+                       return false;
+               }
+       }
+
+       /**
+        * Remove file from cache
+        *
+        * @param string $key cache key
+        * @return bool
+        */
+       public function remove( $key ) {
+               if ( !$this->enabled ) {
+                       return false;
+               }
+
+               $filename = $this->filename( $key );
+
+               if ( file_exists( $filename ) ) {
+                       return unlink( $filename );
+               } else {
+                       return false;
+               }
+       }
+
+       /**
+        * Clean cache based on time to live and max size
+        *
+        * @return bool
+        */
+       public function clean() {
+               if ( !$this->enabled ) {
+                       return false;
+               }
+
+               $ttl = $this->ttl;
+               $maxSize = $this->maxSize;
+
+               // unlink expired files
+               if ( $ttl > 0 ) {
+                       $expire = new \DateTime();
+                       $expire->modify( '-' . $ttl . ' seconds' );
+
+                       $finder = $this->get_finder()->date( 'until ' . $expire->format( 'Y-m-d H:i:s' ) );
+                       foreach ( $finder as $file ) {
+                               unlink( $file->getRealPath() );
+                       }
+               }
+
+               // unlink older files if max cache size is exceeded
+               if ( $maxSize > 0 ) {
+                       $files = array_reverse( iterator_to_array( $this->get_finder()->sortByAccessedTime()->getIterator() ) );
+                       $total = 0;
+
+                       foreach ( $files as $file ) {
+                               if ( $total + $file->getSize() <= $maxSize ) {
+                                       $total += $file->getSize();
+                               } else {
+                                       unlink( $file->getRealPath() );
+                               }
+                       }
+               }
+
+               return true;
+       }
+
+       /**
+        * Ensure directory exists
+        *
+        * @param string $dir directory
+        * @return bool
+        */
+       protected function ensure_dir_exists( $dir ) {
+               if ( !is_dir( $dir ) ) {
+                       if ( file_exists( $dir ) ) {
+                               // exists and not a dir
+                               return false;
+                       }
+                       if ( !@mkdir( $dir, 0777, true ) ) {
+                               return false;
+                       }
+               }
+
+               return true;
+       }
+
+       /**
+        * Prepare cache write
+        *
+        * @param string $key cache key
+        * @return bool|string filename or false
+        */
+       protected function prepare_write( $key ) {
+               if ( !$this->enabled ) {
+                       return false;
+               }
+
+               $filename = $this->filename( $key );
+
+               if ( !$this->ensure_dir_exists( dirname( $filename ) ) ) {
+                       return false;
+               }
+
+               return $filename;
+       }
+
+       /**
+        * Validate cache key
+        *
+        * @param string $key cache key
+        * @return string relative filename
+        */
+       protected function validate_key( $key ) {
+               $url_parts = parse_url( $key );
+               if ( $url_parts['scheme'] ) { // is url
+                       $parts = array('misc');
+                       $parts[] = $url_parts['host'] . ( $url_parts['port'] ? '-' . $url_parts['port'] : '' );
+                       $parts[] = $url_parts['path'] . ( $url_parts['query'] ? '-' . $url_parts['query'] : '' );
+               } else {
+                       $key = str_replace( '\\', '/', $key );
+                       $parts = explode( '/', ltrim( $key ) );
+               }
+
+               $parts = preg_replace( "#[^{$this->whitelist}]#i", '-', $parts );
+
+               return implode( '/', $parts );
+       }
+
+       /**
+        * Filename from key
+        *
+        * @param string $key
+        * @return string filename
+        */
+       protected function filename( $key ){
+               return $this->root . $this->validate_key( $key );
+       }
+
+       /**
+        * Get a Finder that iterates in cache root only the files
+        *
+        * @return Finder
+        */
+       protected function get_finder() {
+               return Finder::create()->in( $this->root )->files();
+       }
+}
diff --git a/php/WP_CLI/WpHttpCacheManager.php b/php/WP_CLI/WpHttpCacheManager.php
new file mode 100644 (file)
index 0000000..1fe5a89
--- /dev/null
@@ -0,0 +1,124 @@
+<?php
+
+
+namespace WP_CLI;
+use WP_CLI;
+
+
+/**
+ * Manage caching with whitelisting
+ *
+ * @package WP_CLI
+ */
+class WpHttpCacheManager {
+
+       /**
+        * @var array map whitelisted urls to keys and ttls
+        */
+       protected $whitelist = array();
+       /**
+        * @var FileCache
+        */
+       protected $cache;
+
+       /**
+        * @param FileCache $cache
+        */
+       public function __construct( FileCache $cache ) {
+               $this->cache = $cache;
+
+               // hook into wp http api
+               add_filter( 'pre_http_request', array($this, 'filter_pre_http_request'), 10, 3 );
+               add_filter( 'http_response', array($this, 'filter_http_response'), 10, 3 );
+       }
+
+
+       /**
+        * short circuit wp http api with cached file
+        */
+       public function filter_pre_http_request( $response, $args, $url ) {
+               // check if whitelisted
+               if ( !isset( $this->whitelist[$url] ) ) {
+                       return $response;
+               }
+               // check if downloading
+               if ( 'GET' !== $args['method'] || empty( $args['filename'] ) ) {
+                       return $response;
+               }
+               // check cache and export to designated location
+               $filename = $this->cache->has( $this->whitelist[$url]['key'], $this->whitelist[$url]['ttl'] );
+               if ( $filename ) {
+                       WP_CLI::log( sprintf( 'Using cached file \'%s\'...', $filename, $url ) );
+                       if ( copy( $filename, $args['filename'] ) ) {
+                               // simulate successful download response
+                               return array(
+                                       'response' => array('code' => ( 200 ), 'message' => 'OK'),
+                                       'filename' => $args['filename']
+                               );
+                       } else {
+                               WP_CLI::error( sprintf( 'Error copying cached file %s to %s', $filename, $url ) );
+                       }
+               }
+               return $response;
+       }
+
+
+       /**
+        * cache wp http api downloads
+        */
+       public function filter_http_response( $response, $args, $url ) {
+               // check if whitelisted
+               if ( !isset( $this->whitelist[$url] ) ) {
+                       return $response;
+               }
+               // check if downloading
+               if ( 'GET' !== $args['method'] || empty( $args['filename'] ) ) {
+                       return $response;
+               }
+               // check if download was successful
+               if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
+                       return $response;
+               }
+               // cache downloaded file
+               $this->cache->import( $this->whitelist[$url]['key'], $response['filename']);
+               return $response;
+       }
+
+       /**
+        * whitelist a package url
+        *
+        * @param string $url
+        * @param string $group   package group (themes, plugins, ...)
+        * @param string $slug    package slug
+        * @param string $version package version
+        * @param int    $ttl
+        */
+       public function whitelist_package( $url, $group, $slug, $version, $ttl = null ) {
+               $ext = pathinfo( parse_url( $url, PHP_URL_PATH ), PATHINFO_EXTENSION );
+               $key = "$group/$slug-$version.$ext";
+               $this->whitelist_url( $url, $key, $ttl );
+               wp_update_plugins();
+       }
+
+       /**
+        * whitelist a url
+        *
+        * @param string $url
+        * @param string $key
+        * @param int    $ttl
+        */
+       public function whitelist_url( $url, $key = null, $ttl = null ) {
+               $key = $key ? : $url;
+               $this->whitelist[$url] = compact( 'key', 'ttl' );
+       }
+
+       /**
+        * check if url is whitelisted
+        *
+        * @param string $url
+        * @return bool
+        */
+       public function is_whitelisted( $url ) {
+               return isset( $this->whitelist[$url] );
+       }
+}
\ No newline at end of file
index 12b759e..7be4ac9 100644 (file)
@@ -2,6 +2,8 @@
 
 use \WP_CLI\Utils;
 use \WP_CLI\Dispatcher;
+use \WP_CLI\FileCache;
+use \WP_CLI\WpHttpCacheManager;
 
 /**
  * Various utilities for WP-CLI commands.
@@ -54,6 +56,41 @@ class WP_CLI {
                return $runner;
        }
 
+       /**
+        * @return FileCache
+        */
+       static function get_cache() {
+               static $cache;
+
+               if ( !$cache ) {
+                       $home = getenv( 'HOME' ) ? : getenv( 'HOMEDRIVE' ) . '/' . getenv( 'HOMEPATH' );
+                       $dir = getenv( 'WP_CLI_CACHE_DIR' ) ? : "$home/.wp-cli/cache";
+                       // 6 months, 300mb
+                       $cache = new FileCache( $dir, 15552000, 314572800);
+
+                       if (0 === mt_rand( 0, 50 ) ) {
+                               register_shutdown_function( function () use ( $cache ) {
+                                       $cache->clean();
+                               } );
+                       }
+               }
+
+               return $cache;
+       }
+
+       /**
+        * @return WpHttpCacheManager
+        */
+       static function get_http_cache_manager() {
+               static $http_cacher;
+
+               if ( !$http_cacher ) {
+                       $http_cacher = new WpHttpCacheManager( self::get_cache() );
+               }
+
+               return $http_cacher;
+       }
+
        static function colorize( $string ) {
                return \cli\Colors::colorize( $string, self::get_runner()->in_color() );
        }
index 535a3f4..da85395 100644 (file)
@@ -260,6 +260,9 @@ class Plugin_Command extends \WP_CLI\CommandWithUpgrade {
                }
 
                WP_CLI::log( sprintf( 'Installing %s (%s)', $api->name, $api->version ) );
+               if ( 'dev' !== $assoc_args['version'] ) {
+                       WP_CLI::get_http_cache_manager()->whitelist_package( $api->download_link, $this->item_type, $api->slug, $api->version );
+               }
                $result = $this->get_upgrader( $assoc_args )->install( $api->download_link );
 
                return $result;
@@ -304,10 +307,14 @@ class Plugin_Command extends \WP_CLI\CommandWithUpgrade {
                $items = array();
 
                foreach ( get_plugins() as $file => $details ) {
+                       $update_info = $this->get_update_info( $file );
+
                        $items[ $file ] = array(
                                'name' => $this->get_name( $file ),
                                'status' => $this->get_status( $file ),
-                               'update' => $this->has_update( $file ),
+                               'update' => (bool) $update_info,
+                               'update_version' => $update_info['new_version'],
+                               'update_package' => $update_info['package'],
                                'version' => $details['Version'],
                                'update_id' => $file,
                        );
index 7e093a8..f5cf3ce 100644 (file)
@@ -176,6 +176,9 @@ class Theme_Command extends \WP_CLI\CommandWithUpgrade {
                }
 
                WP_CLI::log( sprintf( 'Installing %s (%s)', $api->name, $api->version ) );
+               if ( 'dev' !== $assoc_args['version'] ) {
+                       WP_CLI::get_http_cache_manager()->whitelist_package( $api->download_link, $this->item_type, $api->slug, $api->version );
+               }
                $result = $this->get_upgrader( $assoc_args )->install( $api->download_link );
 
                return $result;
@@ -186,11 +189,14 @@ class Theme_Command extends \WP_CLI\CommandWithUpgrade {
 
                foreach ( wp_get_themes() as $key => $theme ) {
                        $file = $theme->get_stylesheet_directory();
+                       $update_info = $this->get_update_info( $theme->get_stylesheet() );
 
                        $items[ $file ] = array(
                                'name' => $key,
                                'status' => $this->get_status( $theme ),
-                               'update' => $this->has_update( $theme->get_stylesheet() ),
+                               'update' => (bool) $update_info,
+                               'update_version' => $update_info['new_version'],
+                               'update_package' => $update_info['package'],
                                'version' => $theme->get('Version'),
                                'update_id' => $theme->get_stylesheet(),
                        );