Sunday, December 8, 2024

Web file manager in less than 100 lines of code

Uploading and download files in web browser is a common task in virtually any web application or service. This article shows how to do this with very little coding - in less than 100 lines of code. The database used is PostgreSQL, and the web server is Nginx.

You will use Gliimly as an application server and the programming language. It will run behind the web server for performance and security, as well as to enable richer web functionality. This way end-user cannot talk to your application server directly because all such requests go through the web server, while your back-end application can talk directly to your application server for better performance.

Assuming your currently logged-on Linux user will own the application, create a source code directory and also create Gliimly application named "file-manager":
mkdir filemgr
cd filemgr
gg -k file-manager

Next, create PostgreSQL database named "db_file_manager", owned by currently logged-on user (i.e. passwordless setup):
echo "create user $(whoami);
create database db_file_manager with owner=$(whoami);
grant all on database db_file_manager to $(whoami);
\q"  | sudo -u postgres psql

Create database configuration file used by Gliimly that describes the database (it's a file "db"):
echo "user=$(whoami) dbname=db_file_manager" > db

Create SQL table that will hold files currently stored on the server:
echo "create table if not exists files (fileName varchar(100), localPath varchar(300), extension varchar(10), description varchar(200), fileSize int, fileID bigserial primary key);" | psql -d db_file_manager

Finally, create source Gliimly files. First create "start.gliim" file and copy and paste:
 begin-handler /start public
    @<h2>File Manager</h2>
    @To manage the uploaded files, <a href="<<p-path "/list">>">click here.</a><br/>
    @<br/>
    @<form action="<<p-path "/upload">>" method="POST" enctype="multipart/form-data">
    @    <label for="file_description">File description:</label><br>
    @    <textarea name="filedesc" rows="3" columns="50"></textarea><br/>
    @    <br/>
    @    <label for="filename">File:</label>
    @    <input type="file" name="file" value=""><br><br>
    @    <input type="submit" value="Submit">
    @</form>
 end-handler

Create "list.gliim" file and copy and paste:
 begin-handler /list public
     @<h2>List of files</h2>
     @To add a file, <a href="<<p-path "/start">>">click here</a><br/><br/>
     @<table border="1">
     @<tr>
     @    <td>File</td><td>Description</td><td>Size</td><td>Show</td><td>Delete</td>
     @</tr>
     run-query @db= \
         "select fileName, description, fileSize, fileID from files order by fileSize desc" \
         output file_name, description noencode, file_size, file_ID
         @<tr>
         @    <td><<p-web file_name>></td><td><<p-web description>><td><<p-web file_size>></td>
         @    <td><a href="<<p-path "/download">>/file_id=<<p-url file_ID>>">Show</a></td>
         @    <td><a href="<<p-path "/delete">>/action=confirm/file_id=<<p-url file_ID>>">Delete</a></td>
         @</tr>
     end-query
     @</table>
 end-handler

Create "upload.gliim" file and copy and paste:
 begin-handler /upload public
    get-param filedesc      // file description from the upload form
    get-param file_filename // file name
    get-param file_location // the path to uploaded file
    get-param file_size     // size in bytes
    get-param file_ext      // the file extension
    @<h2>Uploading file</h2>
    run-query @db= \
         "insert into files (fileName, localPath, extension, description, fileSize) \
             values ('%s', '%s', '%s', '%s', '%s')" \
         input file_filename, file_location, file_ext, filedesc, file_size
    end-query
    @File <<p-web file_filename>> of size <<p-web file_size>> \
         is stored on server at <<p-web file_location>>. \
         File description is <<p-web filedesc>>.<hr/>
 end-handler

Create "download.gliim" file and copy and paste:
 begin-handler /download public
     get-param file_id
     run-query @db= \
         "select localPath,extension from files where fileID='%s'" \
         input file_id \
         output local_path, ext \
         row-count num_files
             if-true ext equal ".jpg"
                 send-file local_path headers content-type "image/jpg"
             else-if ext equal ".pdf"
                 send-file local_path headers content-type "application/pdf"
             else-if
                 send-file local_path headers content-type "application/octet-stream" download
             end-if
     end-query
     if-true num_files not-equal 1
         @Cannot find this file!<hr/>
         exit-handler
     end-if
 end-handler

Create "delete.gliim" file and copy and paste:
 begin-handler /delete public
    @<h2>Delete a file</h2>
    get-param action
    get-param file_id
    run-query @db="select fileName, localPath, description  from files where fileID='%s'" output file_name, local_path, desc input file_id
         if-true action equal "confirm" // get file information to confirm what will be deleted
            @Are you sure you want to delete file <<p-web file_name>> (<<p-web desc>>)? Click <a href="<<p-path "/delete">>?action=delete&amp;file_id=<<p-url file_id>>">Delete</a> or click the browser's Back button to go back.<br/>
         else-if action equal "delete"  // actual delete file, once confirmed
            begin-transaction @db
            run-query @db= "delete from files where fileID='%s'" input file_id error err no-loop
            if-true err not-equal "0"
                @Could not delete the file (error <<p-web err>>)
                rollback-transaction @db
            else-if
                delete-file local_path status st
                if-true st equal GG_OKAY
                    commit-transaction @db
                    @File deleted. Go back to <a href="<<p-path "/start">>">start page</a>
                else-if
                    rollback-transaction @db
                    @File could not be deleted, error <<p-num st>>
                end-if
            end-if
         else-if
            @Unrecognized action <<p-web action>>
         end-if
    end-query
 end-handler

Make the application:
gg -q --db=postgres:db

Run your application server:
mgrg file-manager

A web server sits in front of Gliimly application server, so it needs to be setup. This example is for Ubuntu, so edit Nginx config file there:
sudo vi /etc/nginx/sites-enabled/default

Add this in "server {}" section ("client_max_body_size" allows for images of typical sizes to be uploaded):
location /file-manager/ { include /etc/nginx/fastcgi_params; fastcgi_pass  unix:///var/lib/gg/file-manager/sock/sock; }
client_max_body_size 100M;

Restart Nginx:
sudo systemctl restart nginx

Go to your web browser, and enter:
http://127.0.0.1/file-manager/start

This is what the end result looks like. Obviously, we used just bare-bone HTML, but that's not the point here at all. You can use any kind of front-end technology, the point is to demonstrate Gliimly as a back-end server for web applications/services.

Here's the home screen, with the form to upload a file and a link to list of files:


Listing files:


Asking to delete a file:


Confirmation of deletion: