How to create Server File Explorer using jQuery and PHP

I’ve created plugin called jQuery File Browser that can be used to created browser for your server files. Here we will use php as server side script but it can be replaced with any other server side language like Ruby On Rails, Python or Node.js.

TL;DR you can find whole code in this gist

We will use JSON-RPC as our protocol to communicate with the server and my library that implement the protocol in php and JavaScript.

After we clone json-rpc and jQuery File Browser, we need to create our service.php file:

<?php

require('json-rpc/json-rpc.php');


class Service {
    function username() {
        return get_current_user();
    }
    function ls($path) {
        $files = array();
        $dirs = array();
        foreach (new DirectoryIterator($path) as $file) {
            $name = $file->getBasename();
            if ($name != "." && $name != "..") {
                if ($file->isFile() || $file->isLink()) {
                    $files[] = $name;
                } else {
                    $dirs[] = $name;
                }
            }
        }
        return array(
            "files" => $files,
            "dirs" => $dirs
        );
    }
}

echo handle_json_rpc(new Service());

?>

Then we need to create base html file with imported libraries including jQuery and jQuery UI (we will use file browser using jQuery UI dialog):

<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
  <meta charset="utf-8" />
  <title>File Explorer using jQuery</title>
  <!-- remove underscore from sc_ript (wordpress replace skrypt tag with a tag)-->
  <sc_ript src="https://code.jquery.com/jquery-3.1.1.min.js"></sc_ript>
  <sc_ript src="http://code.jquery.com/ui/1.12.0/jquery-ui.js"></sc_ript>
  <sc_ript src="jquery.filebrowser/js/jquery.filebrowser.min.js"></sc_ript>
  <sc_ript src="json-rpc/json-rpc.js"></sc_ript>

  <link href="http://code.jquery.com/ui/1.12.1/themes/dark-hive/jquery-ui.css" rel="stylesheet"/>
  <link href="jquery.filebrowser/css/jquery.filebrowser.min.css" rel="stylesheet"/>

Then we need to create instance of file browser inside jQuery UI dialog (we will call your service to get the username):

   $(function() {
       rpc({
           url: 'service.php',
           // this will make json-rpc library return promises instead of function that need to be called
           promisify: true
       }).then(function(service) {
           service.username().then(function(username) {
               var browser = $('<div/>').appendTo('body').dialog({
                   width: 800,
                   height: 600
               }).browse({
                   root: '/home/' + username + '/',
                   dir: function(path) {
                       return service.ls(path);
                   }
               });
           });
       });
   });

And now you should have file explorer window open with content of your server user home directory. If you don’t see the output you may want to change root to ‘/home/username’ and don’t use server to get the username, which may be the user that run http server.

The icons that are inside jQuery File Browser are taken from my Clarity icons for GNU/Linux with GTK+.

Next you can add editor that will open on double click on the icon, we will use CodeMirror for this.

First we need to include codemirror:

  <link href="codemirror-5.29.0/lib/codemirror.css" rel="stylesheet"/>
  <link href="codemirror-5.29.0/theme/base16-dark.css" rel="stylesheet"/>
  <sc_ript src="codemirror-5.29.0/lib/codemirror.js"></sc_ript>
  <sc_ript src="codemirror-5.29.0/keymap/emacs.js"></sc_ript>

We use base16-dark theme (to match dark theme of the jQuery UI) and emacs keymap, but you can pick different one.

Then we need to open the editor, we will create open function that look like this:

function open(path) {
    var fname = path.replace(/.*\//, '');
    var readonly = true;
    var mtime;
    service.is_writable(path).then(function(is_writable) {
        readonly = !is_writable;
    });
    service.filemtime(path).then(function(time) {
        mtime = time;
    });
    var ext = fname.replace(/^.*\./, '');
    var mode;
    // you can add more modes here:
    switch(ext) {
        case 'js':
            mode = 'javascript';
            break;
        case 'rb':
            mode = 'ruby';
            break;
        case 'py':
            mode = 'python';
            break;
        case 'sql':
            mode = 'mysql';
            break;
        case 'svg':
            mode = 'xml';
            break;
        case 'xml':
        case 'css':
        case 'php':
            mode = ext;
            break;
    }
    var scripts = $('script').map(function() {
        return ($(this).attr('src') || '').replace(/.*\//, '');
    }).get();
    if (scripts.indexOf(mode + '.js') == -1) {
        var name = 'codemirror-5.29.0/mode/' + mode + '/' + mode + '.js';
        service.file_exists(name).then(function(exists) {
            if (exists) {
                $('<sc' + 'ript/>').attr('src', name).appendTo('head');
            }
        });
    }
    service.file(path).then(function(content) {
        var unsaved = false;
        var div = $('<div><textarea/><div>');
        var textarea = div.find('textarea').hide();
        textarea.val(content);
        div.dialog({
            title: fname,
            width: 650,
            beforeClose: function(e, ui) {
                if (unsaved) {
                    if (confirm('Unsaved file, are you sure?')) {
                        $(this).dialog('destroy');
                    } else {
                        return false;
                    }
                } else {
                    $(this).dialog('destroy');
                }
            },
            resize: function(e, ui) {
                editor.editor.setSize(textarea.width(), textarea.height());
            }
        });
        var editor = {
            dialog: div,
            editor: CodeMirror.fromTextArea(textarea[0], {
                lineNumbers: true,
                matchBrackets: true,
                mode: mode,
                readOnly: readonly,
                theme: 'base16-dark',
                indentUnit: 4,
                keyMap: 'emacs',
                extraKeys: {
                    "Ctrl-S": function(cm) {
                        if (unsaved) {
                            function saveFile() {
                                editor.editor.save();
                                var content = textarea.val();
                                service.save(path, content).then(function() {
                                    unsaved = false;
                                    div.dialog('option', 'title', fname);
                                });
                            }
                            service.filemtime(path).then(function(time) {
                                if (mtime != time) {
                                    if (confirm('File `' + fname + '\' changed ' +
                                                'on Disk are you sure?')) {
                                        saveFile();
                                    }
                                } else {
                                    saveFile();
                                }
                            });
                        }
                    },
                    "Tab": "indentAuto"
                }
            })
        };
        editor.editor.on('change', function(editor) {
            console.log('change');
            unsaved = true;
            div.dialog('option', 'title', '*' + fname);
        });
    });
}

it require few more server side functions:

class Service {
    function file_exists($path) {
        return file_exists($path);
    }
    function username() {
        return get_current_user();
    }
    function is_writable($path) {
        return is_writable($path);
    }
    function file($path) {
        return file_get_contents($path);
    }
    function save($path, $content) {
        $file = fopen($path, 'w');
        $ret = fwrite($file, $content);
        fclose($file);
        return $ret;
    }
    function filemtime($path) {
        return filemtime($path);
    }
    function ls($path) {
        $files = array();
        $dirs = array();
        foreach (new DirectoryIterator($path) as $file) {
            $name = $file->getBasename();
            if ($name != "." && $name != "..") {
                if ($file->isFile() || $file->isLink()) {
                    $files[] = $name;
                } else {
                    $dirs[] = $name;
                }
            }
        }
        return array(
            "files" => $files,
            "dirs" => $dirs
        );
    }
}

and then we need to add this function to our file explorer, we can do that by adding open option, (we can also embed whole function instead of adding it as a value):

   $(function() {
       rpc({
           url: 'service.php',
           promisify: true
       }).then(function(service) {
           service.username().then(function(username) {
               var browser = $('<div/>').appendTo('body').dialog({
                   width: 800,
                   height: 600
               }).browse({
                   root: '/home/' + username + '/',
                   dir: function(path) {
                       return service.ls(path);
                   },
                   open: open
               });
           });
       });
   });

we also need some css tweaks to make icons take more space and to make editor resize properly.

body {
    font-size: 10px;
}
.ui-dialog .ui-dialog-content {
    padding: 0;
    overflow: hidden;
}
.browser {
    width: 800px;
    height: 600px;
}
.browser-widget .content .selection {
    border-color: #fff;
}
.browser-widget li.file,
.browser-widget li.directory {
    width: 60px;
}
.ui-dialog textarea {
    width: 100%;
    height: 100%;
    padding: 0;
    position: absolute;
    left: 0;
    right: 0;
    top: 0;
    bottom: 0;
    resize: none;
}
Advertisements

, ,

  1. Leave a comment

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: