Skip to content

Commit

Permalink
Minify now allows static file serving
Browse files Browse the repository at this point in the history
With slightly altered URLs, Minify can cache files so they're served directly
from the filesystem instead of through PHP. A simple library helps create URLs
and clearing the cache.

See `static/README.md` for details.
  • Loading branch information
mrclay committed Jun 29, 2016
1 parent 22fb644 commit 59d4c97
Show file tree
Hide file tree
Showing 10 changed files with 340 additions and 1 deletion.
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -8,3 +8,4 @@
/composer.lock
/vendor
/.php_cs.cache
/static/[0-9]*
1 change: 1 addition & 0 deletions HISTORY.md
@@ -1,5 +1,6 @@
## Version 3.0.0 (unreleased)
* The project root is now what is deployed as `min`
* Adds feature to serve static files directly
* Installation requires use of Composer to install dependencies
* Removes JSMin+ (unmaintained, high memory usage)
* Removes DooDigestAuth
Expand Down
4 changes: 4 additions & 0 deletions README.md
Expand Up @@ -11,6 +11,10 @@ The stats above are from a [brief walkthrough](http://mrclay.org/index.php/2008/

Relative URLs in CSS files are rewritten to compensate for being served from a different directory.

## Static file serving

Version 3 allows [serving files directly from the filesystem](static/README.md) for much better performance. We encourage you to try this feature.

## Support

Post to the [Google Group](http://groups.google.com/group/minify).
Expand Down
6 changes: 6 additions & 0 deletions config.php
Expand Up @@ -7,6 +7,12 @@
*/


/**
* Enable the static serving feature
*/
$min_enableStatic = false;


/**
* Allow use of the Minify URI Builder app. Only set this to true while you need it.
*/
Expand Down
4 changes: 3 additions & 1 deletion docs/FAQ.wiki.md
Expand Up @@ -4,7 +4,9 @@ The simple JSMin algorithm is the most reliable in PHP, but check the [CookBook]

## How fast is it?

Certainly not as fast as an HTTPd serving flat files. On a high-traffic site:
If you [serve static files](https://github.com/mrclay/minify/blob/master/static/README.md), it's as fast as your web server, and you should do this for high-traffic sites.

The PHP-based server is not as fast, but still performs well thanks to an internal cache. Tips:

* **Use a reverse proxy** to cache the Minify URLs. This is by far the most important tip.
* Revision your Minify URIs (so far-off Expires headers will be sent). One way to do this is using [groups](UserGuide.wiki.md#using-groups-for-nicer-urls) and the [Minify_groupUri()](UserGuide.wiki.md#far-future-expires-headers) utility function. Without this, clients will re-request Minify URLs every 30 minutes to check for updates.
Expand Down
5 changes: 5 additions & 0 deletions lib/Minify/Config.php
Expand Up @@ -11,6 +11,11 @@ class Config
*/
public $enableBuilder = false;

/**
* @var bool
*/
public $enableStatic = false;

/**
* @var bool
*/
Expand Down
40 changes: 40 additions & 0 deletions static/.htaccess
@@ -0,0 +1,40 @@
<IfModule mod_expires.c>
ExpiresActive On
ExpiresDefault "access plus 1 year"
</IfModule>

<FilesMatch "\.(js|css|less)$">
FileETag MTime Size
</FilesMatch>

<IfModule mod_gzip.c>
mod_gzip_on yes
mod_gzip_dechunk yes
mod_gzip_keep_workfiles No
mod_gzip_minimum_file_size 1000
mod_gzip_maximum_file_size 1000000
mod_gzip_maximum_inmem_size 1000000
mod_gzip_item_include mime ^text/.*
mod_gzip_item_include mime ^application/javascript$
mod_gzip_item_include mime ^application/x-javascript$
# Exclude old browsers and images since IE has trouble with this
mod_gzip_item_exclude reqheader "User-Agent: .*Mozilla/4\..*\["
mod_gzip_item_exclude mime ^image/.*
</IfModule>

<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/css text/javascript application/javascript application/x-javascript
BrowserMatch ^Mozilla/4 gzip-only-text/html
BrowserMatch ^Mozilla/4\.[0678] no-gzip
BrowserMatch \bMSIE !no-gzip
</IfModule>

<IfModule mod_rewrite.c>
RewriteEngine on

# You may need RewriteBase on some servers
#RewriteBase /min/static

RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ gen.php [QSA,L]
</IfModule>
85 changes: 85 additions & 0 deletions static/README.md
@@ -0,0 +1,85 @@

# Static file serving

**Note:** This feature is new and not extensively tested.

Within this folder, Minify creates minified files on demand, serving them without the overhead of PHP at all.

For example, when a visitor requests a URL like `/min/static/1467089473/f=js/my-script.js`, Minify creates the directories `1467089473/f=js`, and saves the minified file `my-script.js` in it. On following requests, the file is served directly.

## Getting started

1. Make sure the `static` directory is writable by your server.

2. In `minify/config.php`, set `$min_enableStatic = true;`

3. Request the test script http://example.org/min/static/0/f=min/quick-test.js

This will create a new cache directory within `static` and redirect the browser to the new location, e.g. http://example.org/min/static/1467089473/f=min/quick-test.js.

You should see the minified script and on the server the `static` directory should contain a new subdirectory tree with the static file. Following requests will serve the file directly.

4. Delete the new subdirectory (e.g. `1467089473`) and refresh the browser.

You should be redirected to the new location where the file and cache directory has been recreated.

## Site integration

You don't want to hardcode any URLs. Instead we'll use functions in `lib.php`:

```php
require_once __DIR__ . '/path/to/static/lib.php';

$static_uri = "/min/static";
$query = "b=scripts&f=1.js,2.js";
$type = "js";

$uri = Minify\StaticService\build_uri($static_uri, $query, $type);
```

If you release a new build (or change any source file), you *must* clear the cache by deleting the entire directory:

```php
require_once __DIR__ . '/path/to/static/lib.php';

Minify\StaticService\flush_cache();
```

## URL rules

As URLs result in files being created, they are more strictly formatted.

* Arbitrary parameters (e.g. to bust a cache) are not permitted.
* URLs must end with `.js` or `.css`.

If your URL does not end with `.js` or `.css`, you'll need to append `&z=.js` or `&z=.css` to the URL. E.g.:

* http://example.org/min/static/1467089473/g=home-scripts&z=.js
* http://example.org/min/static/1467089473/f=styles.less&z=.css

Note that `Minify\StaticService\build_uri` handles this automatically for you.

URLs aren't canonical, so these URLs are all valid and will produce separate files:

* http://example.org/min/static/1467089473/f=one/two/three.js
* http://example.org/min/static/1467089473/b=one/two&f=three.js
* http://example.org/min/static/1467089473/f=three.js&b=one/two&z=.js

## Disable caching

You can easily switch to use the regular `min/` endpoint during development:

```php
<?php

$query = "b=styles&f=minimal.less";
$type = "css";

if ($use_static) {
require_once __DIR__ . '/path/to/static/lib.php';
$static_uri = "/min/static";
$uri = Minify\StaticService\build_uri($static_uri, $query, $type);
} else {
$uri = "/min/?$query";
}
```
127 changes: 127 additions & 0 deletions static/gen.php
@@ -0,0 +1,127 @@
<?php

// allows putting /static anywhere as long as you put a boostrap.php in it
if (is_file(__DIR__ . '/bootstrap.php')) {
$bootstrap_file = __DIR__ . '/bootstrap.php';
} else {
$bootstrap_file = __DIR__ . '/../bootstrap.php';
}

$send_400 = function($content = 'Bad URL') {
http_response_code(400);
die($content);
};

$send_301 = function($url) {
http_response_code(301);
header("Cache-Control: max-age=31536000");
header("Location: $url");
exit;
};

$app = (require $bootstrap_file);
/* @var \Minify\App $app */

if (!$app->config->enableStatic) {
die('Minify static serving is not enabled. Set $min_enableStatic = true; in config.php');
}

require __DIR__ . '/lib.php';

if (!is_writable(__DIR__)) {
http_response_code(500);
die('Directory is not writable.');
}

// parse request
// SCRIPT_NAME = /path/to/minify/static/gen.php
// REQUEST_URI = /path/to/minify/static/1467084520/b=path/to/minify&f=quick-test.js

// "/path/to/minify/static"
$root_uri = dirname($_SERVER['SCRIPT_NAME']);

// "/1467084520/b=path/to/minify&f=quick-test.js"
$uri = substr($_SERVER['REQUEST_URI'], strlen($root_uri));

if (!preg_match('~^/(\d+)/(.*)$~', $uri, $m)) {
http_response_code(404);
die('File not found');
}

// "1467084520"
$requested_cache_dir = $m[1];

// "b=path/to/minify&f=quick-test.js"
$query = $m[2];

// we basically want canonical querystrings because we make a file for each one.
// manual parsing is the only way to do this. The MinApp controller will validate
// these parameters anyway.
$get_params = array();
foreach (explode('&', $query) as $piece) {
if (false === strpos($piece, '=')) {
$send_400();
}

list($key, $value) = explode('=', $piece, 2);
if (!in_array($key, array('f', 'g', 'b', 'z'))) {
$send_400();
}

if (isset($get_params[$key])) {
// already used
$send_400();
}

if ($key === 'z' && !preg_match('~^\.(css|js)$~', $value, $m)) {
$send_400();
}

$get_params[$key] = urldecode($value);
}

$cache_time = Minify\StaticService\get_cache_time();
if (!$cache_time) {
http_response_code(500);
die('Directory is not writable.');
}

$app->env = new Minify_Env(array(
'get' => $get_params,
));
$ctrl = $app->controller;
$options = $app->serveOptions;
$sources = $ctrl->createConfiguration($options)->getSources();
if (!$sources) {
http_response_code(404);
die('File not found');
}
if ($sources[0]->getId() === 'id::missingFile') {
$send_400("Bad URL: missing file");
}

// we need URL to end in appropriate extension
$type = $sources[0]->getContentType();
$ext = ($type === Minify::TYPE_JS) ? '.js' : '.css';
if (substr($query, - strlen($ext)) !== $ext) {
$send_301("$root_uri/$cache_time/{$query}&z=$ext");
}

// fix the cache dir in the URL
if ($cache_time !== $requested_cache_dir) {
$send_301("$root_uri/$cache_time/$query");
}

$content = $app->minify->combine($sources);

// save and send file
$file = __DIR__ . "/$cache_time/$query";
if (!is_dir(dirname($file))) {
mkdir(dirname($file), 0777, true);
}

file_put_contents($file, $content);

header("Content-Type: $type;charset=utf-8");
header("Cache-Control: max-age=31536000");
echo $content;
68 changes: 68 additions & 0 deletions static/lib.php
@@ -0,0 +1,68 @@
<?php

namespace Minify\StaticService;

/**
* Build a URI for the static cache
*
* @param string $static_uri E.g. "/min/static"
* @param string $query E.g. "b=scripts&f=1.js,2.js"
* @param string $type "css" or "js"
* @return string
*/
function build_uri($static_uri, $query, $type) {
$static_uri = rtrim($static_uri, '/');
$query = ltrim($query, '?');

$ext = ".$type";
if (substr($query, - strlen($ext)) !== $ext) {
$query .= "&z=$ext";
}

$cache_time = get_cache_time();

return "$static_uri/$cache_time/$query";
}

/**
* Get the name of the current cache directory within static/. E.g. "1467089473"
*
* @param bool $auto_create Automatically create the directory if missing?
* @return null|string null if missing or can't create
*/
function get_cache_time($auto_create = true) {
foreach (scandir(__DIR__) as $entry) {
if (ctype_digit($entry)) {
return $entry;
break;
}
}

if (!$auto_create) {
return null;
}

$time = (string)time();
if (!mkdir(__DIR__ . "/$time")) {
return null;
}

return $time;
}

function flush_cache() {
$time = get_cache_time(false);
if ($time) {
remove_tree(__DIR__ . "/$time");
}
}

function remove_tree($dir) {
$files = array_diff(scandir($dir), array('.', '..'));

foreach ($files as $file) {
is_dir("$dir/$file") ? remove_tree("$dir/$file") : unlink("$dir/$file");
}

return rmdir($dir);
}

0 comments on commit 59d4c97

Please sign in to comment.