Build an Electron File Explorer with Quasar

@quasarframework · 2018-11-21 16:24 · busy

tl;dr; An in-depth tutorial that shows the steps taken to create (and secure) a file-explorer using Quasar and Electron.

Repo: https://github.com/hawkeye64/electron-quasar-file-explorer

Electron File Explorer

Introduction

What is Electron?

Electron is a framework that allows you to build cross platform desktop apps with javascript, HTML and CSS.

If you can build a website, you can build a desktop app. Electron is a framework for creating native applications with web technologies like JavaScript, HTML and CSS. It takes care of the hard parts so you can focus on the core of your application.

With Electron, you get two processes: the main process and the renderer process. You can think of the main process as your back-end code and the renderer process as your front-end code.

Electron Homepage

What is Quasar?

Quasar Framework is an up and coming MIT licensed open-source framework powered by Vue.

With Quasar, you can build: 1. SPAs (Single Page App) 2. SSR (Server-side Rendered App) (+ optional PWA client takeover) 3. PWAs (Progressive Web App) 4. Mobile Apps (Android, iOS, …) through Apache Cordova 5. Multi-platform Desktop Apps (using Electron)

It is basically an all-in-one solution for web developers with a rich supply of components derived from innovative ideas and concepts that take away the pain (also known as The Quasar Method to Quasarians) of managing a project with eslint, webpack, babel, etc., so you can get on with developing your app (web, desktop or mobile).

Quasar Framework Homepage

What is a File Explorer

A file explorer is a graphical user interface (GUI) that allows the User to visualize and traverse the file system of an operating system.

Why a tutorial?

Why not?

Sarcasm aside, let me say just a few things. I work with Vue on a day-to-day basis and use Quasar Framework. We made this decision collaboratively back in September 2017. It was a gamble to use Vue and even more so for Quasar. Since then, as of this writing, Vue (120k) has more stars on Github than React (116k) or Angular (42k) and Quasar (7.7k) is growing rapidly in popularity with it's all-in-one solution. So far, we feel our gamble has paid off in spades as we gear up to release 5.0 of our product (IntelliView Technologies Homepage) after converting from Angular.

OK, back to why a tutorial? Our product provides an intelligent camera that creates alert videos that Users can export to their computers, as well as archive videos. With that video, we have a custom SVG that runs as an overlay on top of the video so the User can see the analytics. Because the SVG is custom, we needed to write an offline viewer for these exported videos that can also play the associated SVG as an overlay. Thus, we used Electron and Quasar to develop a custom desktop application that fits our needs. So, yes, I am not that experienced with Electron, but I did learn things along the way, that, in this tutorial I will share with you.

About the Tutorial

This is not a full-on tutorial with step-by-step instructions. The source code for the project is freely available and you are welcomed to download or fork the project and extend it out further.

What I will be discussing is design decisions, code decisions, electron security and highlight various coding issues and tasks that I had to overcome in order to create this project. You might already be an experienced coder familiar with everything that I have to say, or you just might learn something you didn't know about. Either way, it's basically a history of events that allowed this project to be done.

Design Decisions

End Goal

The end goal is to have a file explorer that can traverse a User's file system. I want it to work with both Linux and Windows since we are making a desktop app. However, since I am mainly a Linux developer (C++ and Web), I need to get a copy of Windows to work with. Microsoft has a VM (Virtual Machine) with Windows 10 pre-installed that you can download and has a 90-day limit. That will work perfect! I can use Virtual Box to import the VM and install all my tools and work concurrently in Linux and Windows. If Apple would do a VM for Mac, I'd certainly be making sure it works for that as well.

One thing I needed to keep in mind is that with Linux, the file system is under one hierarchy. All files and folders are under root. Windows is different in that each hard drive is basically a root of the files and folders that the hard drive contains. This will present a challenge as I'll need to get the hard drives being used by Windows and then treat each like a root.

Existing File Explorers

Let's take a look at Windows Explorer:

Windows Explorer - List Mode

Windows Explorer - Grid Mode

Let's take a look at Ubuntu Files:

Ubuntu Files - List Mode

Ubuntu Files - Grid Mode

Both file explorers offer a grid and list mode. They also have an (optional) address bar at the top. There is navigation of various sorts on the left side. Then there is the content area in the middle that shows both files and folders.

Design

I want to keep a similar look-and-feel so that the User will already be familiar with Using my file explorer from the first time they start it.

Here are the things I'll do:

  1. Have a read-only address bar that shows the current path. I'll call this the breadcrumb bar. For each part of the path, the User will be able to click on it in order to make back-traversal easy.
  2. Show shortcuts on the left side. This will show links to a number of pre-defined paths. Both Windows and Linux share this concept. Like, the home, documents, and downloads folder. I don't want to hard-code this, so I'll have to research if there is a way of getting them via Electron.
  3. Under the shortcuts I want to show a traversal-tree that expands each folder to show additional folders. I'll be able to use the Quasar QTree component for this. It provides for lazy loading of each tree node, so this will be perfect as I want to load data on-demand to keep things running quickly.
  4. In the main content area I want to show the contents of a folder. The contents may contain other folders as well as files. I want to display the file name and I want to display an icon that represents the file mime type for the most commonly used files. In the content area, I also want the User to be able to switch between grid and list views. And, finally, if the User double-clicks on a folder in the content area, the UI will navigate to the selected folder.
  5. In the content area, if additional files are added, removed, modified, while viewing, then they'll automatically be updated. There is a great package called Chokidar that I can use to watch files and give me notifications.
  6. Finally, if a file is an image, instead of a generic icon, I want to show a thumbnail of the actual image.

Here are things I won't do:

  1. Double-clicking a file won't open it. Although this would be nice, this is not one of my goals at this time.
  2. The User will be unable to create, delete or modify files. This will be a read-only app only to provide proof-of-concept abilities only.
  3. The User will be unable to get information (properties) about a selected file.

In general, the User won't be able to do much except traverse the file system.

These items are likely something I'd do when if I expand out this project with additional functionality.

Security Concerns

As I found out, Electron comes with security concerns, depending on how much power you give it. I want to make sure I limit Electron while still having the power I require. For this aspect, I'll make sure when I set up Electron, that I use the following:

    webPreferences: {
      webSecurity: true
    }

This is actually the default setting for this option. But, I did want to highlight it here.

When false, it will disable the same-origin policy (usually using testing websites by people), and set allowRunningInsecureContent to true if this options has not been set by user. Default is true.

You can read up on more options here.

This presents an additional challenge as you can no longer use the file:// protocol from within the renderer process. In this case, I want to be able to show thumbnails of files that are image-based. So, I'll have to feed them from the main process to the renderer process via a web server instance, like express.

Quasar

Quasar makes it really easy to get started. But, first you have to have it installed globally. You can do that by typing npm install -g quasar-cli. Once installed globally, you can access it as it's now on your path.

Quasar Help Quasar Help

To begin a project, from the command-line:

quasar init my-project-name

This will create a new folder, based on your current folder, and start the scaffolding process. But, first, you will be asked several questions about your project:

Quasar Project Initialization Quasar Project Initialization

To start the web application, you can now type quasar dev or your can build a production-ready web site using quasar build.

At this point, I have not told Quasar to add Electron. So, I type: quasar dev -m electron -t mat

Quasar will detect that Electron needs to be installed, does that, then continues on with the development build.

The folder system that Quasar creates is logical and easy to understand: Quasar Folder Scaffolding

A very nice feature of Quasar is layouts. This is basically a controller for a page layout and you create pages and components that will be loaded into your layout. For most of my work, I use just one layout, but you can have more than one that does different takes based on your needs.

I will want to have Quasar create the page for use using quasar new command:

$ quasar new
  Description
    Quickly scaffold a page/layout/component/store module.

  Usage
    $ quasar new [p|page] 
    $ quasar new [l|layout] 
    $ quasar new [c|component] 
    $ quasar new plugin 
    $ quasar new [s|store] 

    # Examples:

    # Create src/pages/MyNewPage.vue:
    $ quasar new p MyNewPage

    # Create src/pages/MyNewPage.vue and src/pages/OtherPage.vue:
    $ quasar new p MyNewPage OtherPage

    # Create src/layouts/shop/Checkout.vue
    $ quasar new layout shop/Checkout.vue

  Options
    --help, -h     Displays this message

This is what I will be creating:

  1. A main Layout: created automatically for you by Quasar when you init the project.
  2. A contents page using this Quasar command: quasar new p contents
  3. And, then, our components: breadcrumbs, shortcuts, folderTree, gridItem and gridItemImage. These can be created all at once with this Quasar command: quasar new p breadcrumbs, shortcuts, folderTree, gridItem, gridItemImage

Just in case there is any confusion, my gridItem component will contain an image and text and will be displayed on the contents area for both grid and list modes of the file explorer. The gridItemImage component will be a child of the gridItem component and be responsible for image display. This could be a generic icon of sorts, based on mime-type, or if an image file, then the actual image. It will also be responsible for centering images that may be smaller than the display area.

Task #1: Modifications to Scaffolding

The first thing I do is update a few things in my scripts section in package.json. I add the following to make builds more convenient:

  "lint-fix": "eslint --ext .js,.vue src --fix",
  "dev": "quasar dev -m electron -t mat",
  "build-linux": "quasar build -m electron linux -t mat -b builder",
  "build-win32": "quasar build -m electron win32 -t mat -b builder",

The first one with the lint-fix command is because I am lazy. When I forget to do something that that eslint complains about, I issue yarn lint-fix (or npm run lint-fix) and if the issue can be fixed, it goes away.

The dev command is for concurrent development while running. Changes to your code cause an HMR (Hot Module Reload).

Finally, the last two commands build-linux and build-win32 are to build the production ready applications. build-linux should be called on a Linux system and build-win32 should be called on a Windows system. (Note: For windows I will actually be building a 64-bit application. Electron uses "win32" to denote a Windows system, not it's "arch" type.)

Finally, I add the following in the generated eslintrc.js file:

'brace-style': [2, 'stroustrup', { 'allowSingleLine': false }],

Only because this is the style I like to use.

Task #2: Files and Folders

One of the first things I want to do is get a list of files and folders from the local file system. I wrote a utility function to do this because I wasn't sure if I wanted to do this in Electron's main process or renderer process. In the end, I went with the renderer process out of convenience.

This utility function walkFolders is a JavaScript Generator function.

Generator functions provide a powerful alternative [to custom iterators]: they allow you to define an iterative algorithm by writing a single function whose execution is not continuous. Generator functions are written using the function* syntax. When called initially, generator functions do not execute any of their code, instead returning a type of iterator called a Generator. When a value is consumed by calling the generator's next method, the Generator function executes until it encounters the yield keyword.

const path = require('path')
const fs = require('fs')

/**
 * Generator function that lists all files in a folder recursively
 * in a synchronous fashion
 *
 * @param {String} folder - folder to start with
 * @param {Number} recurseLevel - number of times to recurse folders
 * @returns {IterableIterator}
 */
function *walkFolders (folder, recurseLevel = 0) {
  try {
    const files = fs.readdirSync(folder)

    for (const file of files) {
      try {
        const pathToFile = path.join(folder, file)
        const stat = fs.statSync(pathToFile)
        const isDirectory = stat.isDirectory()
        if (isDirectory && recurseLevel > 0) {
          yield * walkFolders(pathToFile, recurseLevel - 1)
        }
        else {
          yield {
            rootDir: folder,
            fileName: file,
            isDir: isDirectory,
            stat: stat
          }
        }
      }
      catch (err) {
        yield {
          rootDir: folder,
          fileName: file,
          error: err
        }
      }
    }
  }
  catch (err) {
    yield {
      rootDir: folder,
      error: err
    }
  }
}

export default walkFolders

Now that I have a function that will give us what I want, I can call it like this:

    getFolders: function (absolutePath) {
      let folders = []
      // check incoming arg
      if (!absolutePath || typeof absolutePath !== 'string') {
        return folders
      }
      for (const fileInfo of walkFolders(absolutePath, false)) {
        // all files and folders
        if ('error' in fileInfo) {
          console.error(`Error: ${fileInfo.rootDir} - ${fileInfo.error}`)
          continue
        }
        // we only want folders
        if (!fileInfo.isDir) {
          continue
        }
        const node = this.createNode(fileInfo)
        folders.push(node)
      }
      return folders
    },

and

    getFolderContents: function (folder) {
      let contents = []
      // check incoming arg
      if (!folder || typeof folder !== 'string') {
        return contents
      }
      for (const fileInfo of walkFolders(folder, false)) {
        // all files and folders
        if ('error' in fileInfo) {
          console.error(`Error: ${fileInfo.rootDir} - ${fileInfo.error}`)
          continue
        }
        const node = this.createNode(fileInfo)
        contents.push(node)
      }
      return contents
    },

The first function is responsible for getting the data and filtering it for folders only. The second function gets all content (files and folders). These could have been combined into one function taking a parameter to decide if you wanted all returned or just folders, but depending on the skill-level of the reader, I didn't want to add too much confusion.

Initially when I wrote this code, I would just console.log() the output. I didn't have a createNode function yet.

Task #3: QTree

In my folderTree component, I want to use the Quasar QTree component. The tree takes an array of objects. Each object contains a label (folder or file name), a nodeKey (absolute path to the file or folder), expandable (I'll set to true for a folder), lazy (if children of this node will be lazy loaded - on demand), children (an empty array where lazy-loaded sub-folders will get loaded, each with their own node), and data (my own addition carrying additional meta data for that node).

    createNode: function (fileInfo) {
      let nodeKey = fileInfo.rootDir
      if (nodeKey.charAt(nodeKey.length - 1) !== path.sep) {
        nodeKey += path.sep
      }
      if (fileInfo.fileName === path.sep) {
        fileInfo.fileName = nodeKey
      }
      else {
        nodeKey += fileInfo.fileName
      }
      // get file mime type
      const mimeType = mime.lookup(nodeKey)
      // create object
      return {
        label: fileInfo.fileName,
        nodeKey: nodeKey,
        expandable: fileInfo.isDir,
        tickable: true,
        lazy: true,
        children: [],
        data: {
          rootDir: fileInfo.rootDir,
          isDir: fileInfo.isDir,
          mimeType: mimeType,
          stat: fileInfo.stat
        }
      }
    },

You may have noticed that I am using path.sep which contains the path separator based on the current operating system. This is very important when making cross-platform software. To get access to this, you will need to const path = require('path') in the script area of your code.

Task #4: Get Windows Drives

When developing this project, I realized that with Linux, all the files and folders are under the root system. Even mounted drives are under root. This is very convenient. However, under Windows you get drives. I needed a way of determining which drives were in use by the operating system.

I did try a couple of packages, that worked, but then when I went to compile my project for Linux, it was broken. The packages themselves were restricted to Windows builds and I needed a way of saying it was optional. I tried the npm route of optionalPackages in the package.json but it did not work for me at all. Really, I could not be bothered trying to figure that out, so I decided to roll my own. I would only ever call it if the app

#quasarframework #utopian-io #fundition-1bwt63vgu #tutorial #fundition
Payout: 0.000 HBD
Votes: 270
More interactions (upvote, reblog, reply) coming soon.