var path = require('path'); var fs = require('fs-ext'); var fsExtra = require('fs-extra'); var utils = require('./utils'); var filetype = require('../content/src/filetype'); var filemeta = require('./filemeta'); var DirCache = require('./dircache').DirCache; var globalRootDirCache = {}; // Do not serve cached content older than 5 minutes. var maxDirCacheAge = 5 * 60 * 1000; // Rebuild cache in background after serving data older than 1 minute. var autoRebuildCacheAge = 1 * 60 * 1000; // Serve at most 600 usernames at a time from root directory. var maxRootDirEntries = 600; function getRootDirCache(dir) { var dircache = globalRootDirCache[dir] if (!dircache) { dircache = new DirCache(dir); globalRootDirCache[dir] = dircache; } return dircache; } exports.handleLoad = function(req, res, app, format) { var filename = utils.filenameFromUri(req); var callback = utils.param(req, 'callback'); var user = res.locals.owner; var origfilename = filename; if (filename == null) { filename = ''; } try { // The root listing, which lists all users, // is handled differently from other requests. var isrootlisting = !user && filename == '' && format == 'json'; if (isrootlisting) { var prefix = utils.param(req, 'prefix') || ''; var count = Math.max(maxRootDirEntries, utils.param(req, 'count') || maxRootDirEntries); // Grab the dir cache object for this root directory path. var dircache = getRootDirCache(app.locals.config.dirs.datadir); if (dircache.age() > maxDirCacheAge) { // A very-old or never-built cache must be rebuilt before serving. dircache.rebuild(sendCachedResult); } else if (prefix) { // When a specific prefix is requested, probe for an exact match. dircache.update(prefix, sendCachedResult); } else { // Fresh cache without prefix: just send the cached result. sendCachedResult(true); } function sendCachedResult(ok) { var data; if (!ok) { data = {error: "Could not read file /"}; } else { data = { directory: "/", list: dircache.readPrefix(prefix, count), auth: false }; // If the cache was sort-of-old, kick off an early rebuild. if (dircache.age() > autoRebuildCacheAge) { // No callback needed. dircache.rebuild(null); } } res.set('Cache-Control', 'must-revalidate'); res.set('Content-Type', 'text/javascript'); res.jsonp(data); } return; } // Validate username if (user) { utils.validateUserName(user); if (filename.length > 0) { filename = path.join(user, filename); } else { filename = user + '/'; } } // Validate filename if (filename.length > 0) { if (!utils.isFileNameValid(filename, false)) { utils.errorExit('Bad filename: ' + filename); } } var absfile = utils.makeAbsolute(filename, app); if (format == 'json') { var haskey = userhaskey(user, app); // Handle the case of a file that's present if (utils.isPresent(absfile, 'file')) { var m = filemeta.parseMetaString( fs.readFileSync(absfile)), data = m.data, meta = m.meta; var statObj = fs.statSync(absfile); var mimetype = filetype.mimeForFilename(filename); res.set('Cache-Control', 'must-revalidate'); resp = { file: '/' + filename, data: data, auth: haskey, mtime: statObj.mtime.getTime(), mime: mimetype }; if (meta != null) { resp.meta = meta; } res.jsonp(resp); return; } // Handle the case of a directory that's present if (utils.isPresent(absfile, 'dir')) { if (filename.length > 0 && filename[filename.length - 1] != '/') { filename += '/'; } var list = buildDirList(absfile, fs.readdirSync(absfile).sort()); var jsonRet = {'directory': '/' + filename, 'list': list, 'auth': haskey}; res.set('Cache-Control', 'must-revalidate'); res.jsonp(jsonRet); return; } // Handle the case of a new file create if (filename.length > 0 && filename[filename.length - 1] != '/' && isValidNewFile(absfile, app)) { res.set('Cache-Control', 'must-revalidate'); res.jsonp({'error': 'could not read file ' + filename, 'newfile': true, 'auth': haskey, 'info': absfile}); return; } utils.errorExit('Could not read file ' + filename); } else if (format == 'code') { // For loading the code only var mt = filetype.mimeForFilename(filename), m = filemeta.parseMetaString( fs.readFileSync(absfile)), data = m.data, meta = m.meta; res.set('Cache-Control', 'must-revalidate'); res.set('Content-Type', (meta && meta.type) || mt.replace(/\bx-pencilcode\b/, 'cofeescript')); res.send(data); return; } else if (format == 'print') { // For printing the code var mt = filetype.mimeForFilename(filename), m = filemeta.parseMetaString( fs.readFileSync(absfile)), data = m.data, meta = m.meta, needline = false, out = []; if (mt.indexOf('text') !== 0) { res.set('Content-Type', mt); res.send(data); return; } res.set('Cache-Control', 'must-revalidate'); res.set('Content-Type', 'text/html;charset=utf-8'); out.push('', '', ''); // Add a title in the form username: file. out.push('' + filename.replace('/', ': ') + ''); out.push(''); out.push('', '', '
');

      if (/\S/.test(data)) {
        out.push(addHTMLLineNumbers(data));
        needline = true;
      }
      if (meta && meta.css && /\S/.test(meta.css)) {
        if (needline)
           out.push('

'); out.push(addHTMLLineNumbers(meta.css)); needline = true; } if (meta && meta.html && /\S/.test(meta.html)) { if (needline) out.push('

'); out.push(addHTMLLineNumbers(meta.html)); needline = true; } out.push('
'); out.push('', ''); res.send(out.join('\n')); return; } else if (format == 'run') { // File loading outside the editor if (utils.isPresent(absfile, 'file')) { var mt = filetype.mimeForFilename(filename), m = filemeta.parseMetaString( fs.readFileSync(absfile)); // For turtle bits, assume it's coffeescript if (mt.indexOf('text/x-pencilcode') == 0) { data = filetype.wrapTurtle(m, res.locals.site); mt = mt.replace('x-pencilcode', 'html'); } else { data = m.data; } res.set('Cache-Control', 'must-revalidate'); res.set('Content-Type', mt); res.send(data); return; } else if (utils.isPresent(absfile, 'dir') || filename.indexOf('/') == filename.length) { if (filename.length > 0 && filename[filename.length - 1] != '/') { res.redirect(301, '/home' + req.path + '/'); return; } res.set('Cache-Control', 'must-revalidate'); res.set('Content-Type', 'text/html;charset=utf-8'); var text = '' + req.path + ''; text += '\n'; text += '

' + req.host + req.path + '

\n'; text += '
\n';

        var lastIndex = req.path.lastIndexOf('/');
        if (lastIndex > 0) {
          var prevIndex = req.path.substring(0, lastIndex).lastIndexOf('/');
          if (prevIndex >= 0) {
            var parentDir = req.path.substring(0, prevIndex + 1);
            text += 'Up to ' + parentDir;
            text += '\n';
          }
        }

        var contents = (utils.isPresent(absfile, 'dir')) ?
          fs.readdirSync(absfile).sort() : new Array();
        for (var i = 0; i < contents.length; i++) {
          if (contents[i][0] == '.') {
            // Skip past any dirs starting with a '.'
            continue;
          }

          var name = contents[i];
          var af = path.join(absfile, name);
          if (utils.isPresent(af, 'dir') &&
              name.charAt(name.length - 1) != '/') {
            name += '/';
          }
          var link = '' + name + '';
          if (utils.isPresent(af, 'file')) {
            link += ' edit';
          }
          text += link + '\n';
        }
        text += '
\n'; if (contents.length == 0) { text += '(directory is empty)
\n'; } res.send(text); return; } else if (filename.charAt(filename.length - 1) == '/') { res.redirect(301, path.dirname(filename.substring(0, filename.length-1)) + '/'); return; } else { res.set('Cache-Control', 'must-revalidate'); res.set('Content-Type', 'text/html;charset=utf-8'); var text = '
\n';
        text += 'No file ' + origfilename + ' found.\n\n';
        var strip = (req.path.charAt(req.path.length - 1) == '/') ?
          req.path.substring(0, req.path.length - 1) : req.path;
        var parentDir = path.dirname(strip) + '/';
        text += 'Up to ' + parentDir + '\n';

        var extIdx = filename.lastIndexOf('.');
        if (extIdx > 0) {
          var ext = filename.substring(extIdx + 1);
          if (ext == 'htm' || ext == 'html' || ext == 'js' || ext == 'css') {
            text += 'Create /home/' + origfilename + '\n';
          }
        }
        text += '
\n'; res.send(text); return; } } } catch (e) { if (e instanceof utils.ImmediateReturnError) { if (format == 'json') { res.jsonp((e.jsonObj) ? e.jsonObj : {'error': e.toString()}); } else { res.set('Content-Type', 'text/html'); if (/ENOENT/.test(e.message)) { res.status(404); res.send('' + '404: ' + filename.replace('/', ': ') + ' not found.'); } else { res.send('<html><body><plaintext>' + e.message); } } } else { throw e; } } }; function addIndentGuides(line) { var leading = /^(?: )*/.exec(line)[0].length; return line.substring(0, leading).replace(/ /g, '<s> </s>') + line.substring(leading); } function addHTMLLineNumbers(data) { var out = [], lines = data.replace(/\s*$/, '').split('\n'), spaces = Math.max(3, ('' + lines.length).length), j; for (j = 0; j < lines.length; ++j) { var line = '' + (j + 1); while (line.length < spaces) { line = ' ' + line; } line = line + '<s> </s>' + addIndentGuides(escapeHTML(lines[j])); out.push(line); } return out.join('\n'); } function escapeHTML(s) { return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); } function isValidNewFile(newAbsFileName, app) { var dir = path.dirname(newAbsFileName); while (true) { if (dir.indexOf(app.locals.config.dirs.datadir) != 0) { return false; } try { if (fs.statSync(dir).isDirectory()) { return true; } } catch (e) { } dir = path.dirname(dir); } } function buildDirList(absdir, contents) { var list = new Array(); for (var i = 0; i < contents.length; i++) { // Skip over any entries starting with . if (contents[i][0] == '.') { continue; } var item = path.join(absdir, contents[i]); var statObj = fs.statSync(item); var modestr = ''; if (statObj.isDirectory()) { modestr += 'd'; } if (statObj.mode & 00400) { modestr += 'r'; } if (statObj.mode & 00200) { modestr += 'w'; } if (statObj.mode & 00100) { modestr += 'x'; } var mtime = statObj.mtime.getTime(); // If its an empty file, then reset mtime if (statObj.isFile() && statObj.size == 0) { mtime = 0; } var absthumb = utils.makeThumbPath(item); list.push({ name: contents[i], thumbnail: fs.existsSync(absthumb), // whether there is a thumbnail mode: modestr, size: statObj.size, mtime: mtime }); } return list; } function userhaskey(user, app) { if (!user) { return false; } var keydir = utils.getKeyDir(user, app); if (!utils.isPresent(keydir, 'dir')) { return false; } var keys = fs.readdirSync(keydir); if (!keys || keys.length == 0) { return false; } return true; }