/** * Copyright 2009 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS-IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * @fileOverview serving static files, including js and css, and cacheing * and minifying. * * Terminology Note: * "path" is confusing because paths can be part of URLs and part * of filesystem paths, and static files have both types of paths * associated with them. Therefore, in this module: * * LOCALDIR or LOCALFILE refers to directories or files on the filesystem. * * HREF is used to describe things that go in a URL. */ import("fileutils.{readFile,readFileBytes}"); import("yuicompressor"); import("stringutils"); import("varz"); import("ejs.EJS"); jimport("java.lang.System.out.println"); //---------------------------------------------------------------- // Content Type Guessing //---------------------------------------------------------------- var _contentTypes = { 'gif': 'image/gif', 'png': 'image/png', 'jpg': 'image/jpeg', 'jpeg': 'image/jpeg', 'css': 'text/css', 'js': 'application/x-javascript', 'txt': 'text/plain', 'html': 'text/html; charset=utf-8', 'ico': 'image/x-icon', 'swf': 'application/x-shockwave-flash', 'zip': 'application/zip', 'xml': 'application/xml' }; var _gzipableTypes = { 'text/css': true, 'application/x-javascript': true, 'text/html; charset=utf-8': true }; function _guessContentType(path) { var ext = path.split('.').pop().toLowerCase(); return _contentTypes[ext] || 'text/plain'; } //---------------------------------------------------------------- function _getCache(name) { var m = 'faststatic'; if (!appjet.cache[m]) { appjet.cache[m] = {}; } var c = appjet.cache[m]; if (!c[name]) { c[name] = {}; } return c[name]; } var _mtimeCheckInterval = 5000; // 5 seconds function _getMTime(f) { var mcache = _getCache('mtimes'); var now = +(new Date); if (appjet.config.devMode || !(mcache[f] && (now - mcache[f].lastCheck < _mtimeCheckInterval))) { var jfile = new net.appjet.oui.JarVirtualFile(f); if (jfile.exists() && !jfile.isDirectory()) { mcache[f] = { lastCheck: now, mtime: jfile.lastModified() }; } else { mcache[f] = null; } } if (mcache[f]) { return +mcache[f].mtime; } else { return null; } } function _wrapFile(localFile) { return { getPath: function() { return localFile; }, getMTime: function() { return _getMTime(localFile); }, getContents: function() { return _readFileAndProcess(localFile, 'string'); } }; } function _readFileAndProcess(fileName, type) { if (fileName.slice(-8) == "_ejs.css") { // run CSS through EJS var template = readFile(fileName); var ejs = new EJS({text:template, name:fileName}); var resultString = ejs.render({}); if (type == 'bytes') { return new java.lang.String(resultString).getBytes("UTF-8"); } else { return resultString; } } else if (type == 'string') { return readFile(fileName); } else if (type == 'bytes') { return readFileBytes(fileName); } } function _cachedFileBytes(f) { var mtime = _getMTime(f); if (!mtime) { return null; } var fcache = _getCache('file-bytes-cache'); if (!(fcache[f] && (fcache[f].mtime == mtime))) { varz.incrementInt("faststatic-file-bytes-cache-miss"); var bytes = _readFileAndProcess(f, 'bytes'); if (bytes) { fcache[f] = {mtime: mtime, bytes: bytes}; }; } if (fcache[f] && fcache[f].bytes) { return fcache[f].bytes; } else { return null; } } function _shouldGzip(contentType) { var userAgent = request.headers["User-Agent"]; if (! userAgent) return false; if (! (/Firefox/.test(userAgent) || /webkit/i.test(userAgent))) return false; if (! _gzipableTypes[contentType]) return false; return request.acceptsGzip; } function _getCachedGzip(original, key) { var c = _getCache("gzipped"); if (! c[key] || ! java.util.Arrays.equals(c[key].original, original)) { c[key] = {original: original, gzip: stringutils.gzip(original)}; } return c[key].gzip; } function _setGzipHeader() { response.setHeader("Content-Encoding", "gzip"); } //---------------------------------------------------------------- /** * Function for serving a single static file. */ function singleFileServer(localPath, opts) { var contentType = _guessContentType(localPath); return function() { (opts.cache ? response.alwaysCache() : response.neverCache()); response.setContentType(contentType); var bytes = _cachedFileBytes(localPath); if (bytes) { if (_shouldGzip(contentType)) { bytes = _getCachedGzip(bytes, "file:"+localPath); _setGzipHeader(); } response.writeBytes(bytes); return true; } else { return false; } }; } /** * valid opts: * alwaysCache: default false */ function directoryServer(localDir, opts) { if (stringutils.endsWith(localDir, "/")) { localDir = localDir.substr(0, localDir.length-1); } return function(relpath) { if (stringutils.startsWith(relpath, "/")) { relpath = relpath.substr(1); } if (relpath.indexOf('..') != -1) { response.forbid(); } (opts.cache ? response.alwaysCache() : response.neverCache()); var contentType = _guessContentType(relpath); response.setContentType(contentType); var fullPath = localDir + "/" + relpath; var bytes = _cachedFileBytes(fullPath); if (bytes) { if (_shouldGzip(contentType)) { bytes = _getCachedGzip(bytes, "file:"+fullPath); _setGzipHeader(); } response.writeBytes(bytes); return true; } else { return false; } }; } /** * Serves cat files, which are concatenated versions of many files. */ function compressedFileServer(opts) { var cfcache = _getCache('compressed-files'); return function() { var key = request.path.split('/').slice(-1)[0]; var contentType = _guessContentType(request.path); response.setContentType(contentType); response.alwaysCache(); var data = cfcache[key]; if (data) { if (_shouldGzip(contentType)) { data = _getCachedGzip((new java.lang.String(data)).getBytes(response.getCharacterEncoding()), "comp:"+key); _setGzipHeader(); response.writeBytes(data); } else { response.write(data); } return true; } else { return false; } }; } function getCompressedFilesKey(type, baseLocalDir, localFileList) { if (stringutils.endsWith(baseLocalDir, '/')) { baseLocalDir = baseLocalDir.substr(0, baseLocalDir.length-1); } var fileList = []; // convert passed-in file list into list of our file objects localFileList.forEach(function(f) { if (typeof(f) == 'string') { fileList.push(_wrapFile(baseLocalDir+'/'+f)); } else { fileList.push(f); } }); // have we seen this exact fileset before? var fsId = fileList.map(function(f) { return f.getPath(); }).join('|'); var fsMTime = Math.max.apply(this, fileList.map(function(f) { return f.getMTime(); })); var kdcache = _getCache('fileset-keydata-cache'); if (!(kdcache[fsId] && (kdcache[fsId].mtime == fsMTime))) { //println("cache miss for fileset: "+fsId); //println("compressing fileset..."); kdcache[fsId] = { mtime: fsMTime, keyString: _compressFilesAndMakeKey(type, fileList) }; } return kdcache[fsId].keyString; } function _compressFilesAndMakeKey(type, fileList) { function _compress(s) { if (type == 'css') { varz.incrementInt("faststatic-yuicompressor-compressCSS"); return yuicompressor.compressCSS(s); } else if (type == 'js') { varz.incrementInt("faststatic-yuicompressor-compressJS"); return yuicompressor.compressJS(s); } else { throw Error('Dont know how to compress this filetype: '+type); } } var fullstr = ""; fileList.forEach(function(f) { fullstr += _compress(f.getContents()); }); fullstr = _compress(fullstr); var key = stringutils.md5(fullstr) + '.' + type; var cfcache = _getCache('compressed-files'); cfcache[key] = fullstr; return key; }