Building a file manager in React with Snorlax
Tags: react, webdev
Published: 2023-05-25 15:52:50

**Written by [Andrew Evans](https://blog.logrocket.com/author/andrewevans/)✏️**

Snorlax is an open source file management system that you can deploy and connect to a web application. Built with Go and Rust, [Snorlax](https://github.com/hyfic/snorlax) offers a simple API that you can leverage to handle the basic Create, Read, Update, and Delete (CRUD) functions, which are typically required. We can create a frontend that looks similar to the Finder on MacOS or Window’s File Explorer using Snorlax’s API. 

In this article, we’ll discuss what Snorlax is and walk through building a React application that connects to a Snorlax server. You can follow along by accessing the [source code for Snorlax on GitHub](https://github.com/hyfic/snorlax), which includes the server code as well as a desktop application built with React and Tauri. Let's get started! 

_Jump ahead:_

*   [Getting started with Snorlax](#getting-started)
*   [Snorlax server internals](#server-internals)
*   [Connecting the React frontend](#react-frontend)
*   [Going beyond localhost with Docker](#going-beyond-localhost)

## Getting started with Snorlax <a name="getting-started">

You can find the Snorlax server in the `server` folder in the GitHub repo. To get it up and running, you’ll need to install Go. Follow the [instructions for your OS of choice from the docs](https://go.dev/doc/install). 

Open your terminal and navigate to the `/server` folder. Run the `go get` command followed by the `go run main.go` command. Once that’s done, you should see a message asking for a default storage location, which creates a folder inside the server to store your files:

```javascript
SNORLAX SERVER v1.0.0 🚀
=======================
Enter storage location [default ./storage/]:
```

Once you’ve determined a storage location, select the port to run it on; the default is `PORT 8000`. You’ll see the following:

```javascript
SNORLAX SERVER v1.0.0 🚀
=======================
Enter storage location [default ./storage/]: 
[i] CONNECTED TO STORAGE FOLDER storage/
=======================
Enter port where server should run [default 8000]: 
=======================
[+] SERVER STARTED AT PORT 8000
[i] http://127.0.0.1:8000
```

Now that the server is started, go into the `desktop` folder and start the client application. The client application is built with React and Tauri, so you’ll need to install the [necessary Tauri prerequisites](https://tauri.app/v1/guides/getting-started/prerequisites/). If you’re not familiar with it, [Tauri is an application development toolkit that leverages Rust](https://blog.logrocket.com/rust-solid-js-tauri-desktop-app/) to build performant applications. There is a lot you can do with Tauri, and I highly recommend [checking out the guides](https://tauri.app/v1/guides/). 

When I initially got set up, I also had to [upgrade my version of Rust](https://doc.rust-lang.org/book/ch01-01-installation.html). Once you have all of the prerequisites installed, you should be able to open a terminal and go to the `desktop` directory of the Snorlax repo. Here, you'll install packages with `npm install` and start the application with `npm run tauri dev`. You could also use [pnpm](https://pnpm.io/) to run the desktop application. Once the application starts up, you should see the following: ![Run Snorlax Desktop Application Pnpm](http://blog.logrocket.com/wp-content/uploads/2023/05/run-snorlax-desktop-application-pnpm.jpeg) 

The application is built with a fairly self-explanatory interface. To perform the basic file operations, you’ll need to select a server and simply enter the values that were output when you started the server earlier: ![Perform Basic File Operations React Snorlax](http://blog.logrocket.com/wp-content/uploads/2023/05/perform-basic-file-operations-react-snorlax.jpeg) Now that you have the application running, let’s dive into some of the details of how it works.

## Snorlax server internals <a name="server-internals">

The server exposes API endpoints that you can use to interact with files, like your typical endpoints that either `GET` or `POST` a file, as well as methods to get information about the files stored. 

The project's [`README`](https://github.com/hyfic/snorlax#readme) has the endpoints listed, but I'll include screenshots for easy reference. Below are the `Read` endpoints: ![Snorlax Server Internals Readme Endpoints](http://blog.logrocket.com/wp-content/uploads/2023/05/snorlax-server-internals-readme-endpoints.jpeg) ![Snorlax Read Endpoints Readme File](http://blog.logrocket.com/wp-content/uploads/2023/05/snorlax-read-endpoints-readme-file.jpeg) 

Below are the `Create` endpoints: ![Snorlax Create Endpoints Create Folder](http://blog.logrocket.com/wp-content/uploads/2023/05/snorlax-create-endpoints-create-folder.jpeg) ![Snorlax Create Endpoints Readme File](http://blog.logrocket.com/wp-content/uploads/2023/05/snorlax-create-endpoints-readme-file.jpeg) 

The screenshot below shows the `Update` endpoint: ![Snorlax Update Endpoint](http://blog.logrocket.com/wp-content/uploads/2023/05/snorlax-update-endpoint.jpeg) Finally, we have the `Delete` endpoint: ![Snorlax Delete Endpoint](http://blog.logrocket.com/wp-content/uploads/2023/05/snorlax-delete-endpoint.jpeg)

## Connecting the React frontend <a name="react-frontend">

As I stated earlier, the example client application we’re reviewing is built with React and Tauri. The application follows most of the standard things you’d see in any React application, and it also has custom hooks that create a central store that can interact with the server. 

Below is the hook for creating the connection to the server at the `server.store.ts` file:

```typescript
import { create } from 'zustand';
import { ServerType } from '@/types/server.type';
import { invoke } from '@tauri-apps/api';
interface ServerStore {
  loading: boolean;
  servers: ServerType[];
  selectedServer: ServerType | null;
  setSelectedServer: (selectedServer: ServerType | null) => void;
  loadServers: (selectServerWithId?: number) => void;
  deleteServer: (serverId: number) => void;
  editServer: (serverId: number, connection: string, name: string) => void;
}
export const useServerStore = create<ServerStore>((set) => ({
  loading: false,
  servers: [],
  selectedServer: null,
  setSelectedServer: (selectedServer) => {
    // set selected database id to localstorage
    if (selectedServer) {
      localStorage.setItem(
        'selectedServerId',
        JSON.stringify(selectedServer.id)
      );
    }
    set({ selectedServer });
  },
  loadServers(selectServerWithId) {
    set({ servers: [], loading: true });
    invoke('read_servers')
      .then((servers: any) => {
        set({ servers, loading: false });
        // get serverId from localstorage is `selectedServerId` is not given
        if (!selectServerWithId) {
          selectServerWithId = JSON.parse(
            localStorage.getItem('selectedServerId') || ''
          );
        }
        // select server if id is given
        if (selectServerWithId) {
          // check if database exist in loaded data
          let filteredServer = servers.filter(
            (server: ServerType) => server.id === selectServerWithId
          );
          if (filteredServer.length == 1) {
            set({ selectedServer: filteredServer[0] });
          }
        }
      })
      .catch(console.log);
  },
  editServer(serverId, connection, name) {
    set((state) => ({
      servers: state.servers.map((server) => {
        if (serverId === server.id) {
          server.name = name;
          server.connection = connection;
        }
        return server;
      }),
    }));
  },
  deleteServer(serverId) {
    set((state) => ({
      servers: state.servers.filter((server) => server.id !== serverId),
      selectedServer:
        state.selectedServer?.id == serverId
          ? state.servers.length > 1
            ? state.servers[0]
            : null
          : state.selectedServer,
    }));
  },
}));
```

If you notice the imports, the store actually leverages [Zustand](https://www.npmjs.com/package/zustand) to manage the application’s state. Then, the various functions like `setSelectedServers` or `loadServers` take an inventory of the sever values and set the parameters, like the connection string. This is then used in the API calls to actually interact with the file system. 

Similar to the `servers` hook, there is also a hook for interacting with the files, as seen in the `file.store.ts` file:

```typescript
import { getDirectory } from '@/api/file.api';
import { create } from 'zustand';
import { FileType } from '@/types/file.type';
import { showToast } from '@/utils/showToast';
interface FilesStore {
  files: FileType[];
  selectedFile: FileType | null;
  loading: boolean;
  setSelectedFile: (selectedFile: FileType | null) => void;
  addFile: (file: FileType) => void;
  deleteFile: (deletedFile: FileType) => void;
  renameFile: (oldName: string, newName: string) => void;
  loadFiles: (connection: string, path: string) => void;
}
export const useFilesStore = create<FilesStore>((set) => ({
  files: [],
  selectedFile: null,
  loading: false,
  setSelectedFile: (selectedFile) => set({ selectedFile }),
  addFile(file) {
    set((state) => ({
      files: [...state.files, file],
    }));
    set({ selectedFile: file.isDir ? null : file });
  },
  deleteFile(deletedFile) {
    set((state) => ({
      files: state.files.filter((file) => file.name !== deletedFile.name),
      selectedFile: null,
    }));
  },
  renameFile(oldName, newName) {
    set((state) => ({
      selectedFile: state?.selectedFile
        ? {
            name: newName,
            isDir: state.selectedFile?.isDir,
          }
        : null,
      files: state.files.map((file) => {
        if (file.name == oldName) {
          file.name = newName;
        }
        return file;
      }),
    }));
  },
  loadFiles(connection, path) {
    set({ loading: true, files: [] });
    getDirectory(connection, path)
      .then(({ data }) => set({ files: data || [] }))
      .catch((err) => {
        set({ files: [] });
        console.log(err);
        showToast({
          title: 'Failed to load files',
          description: err?.response?.data?.message || err?.message,
          status: 'error',
          duration: 5000,
        });
      })
      .finally(() => set({ loading: false }));
  },
}));
```

The functions for interacting with the files leverage an API definition that is laid out in the `src/api/file.api.ts` file:

```typescript
import axios, { AxiosProgressEvent } from 'axios';
export const getDirectory = (connection: string, path: string) => {
  return axios.get(`${connection}/file/view-folder?path=${path}`);
};
export const getFileInfo = (
  connection: string,
  path: string,
  fileName: string
) => {
  return axios.get(`${connection}/file/get-file-info?path=${path}/${fileName}`);
};
export const deleteFile = (
  connection: string,
  path: string,
  fileName: string
) => {
  return axios.delete(
    `${connection}/file/delete-file?path=${path}/${fileName}`
  );
};
export const renameFile = (connection: string, data: any) => {
  return axios({
    url: `${connection}/file/rename-file`,
    method: 'PUT',
    data,
  });
};
export const createFolder = (
  connection: string,
  path: string,
  folderName: string
) => {
  return axios({
    url: `${connection}/file/create-folder`,
    method: 'POST',
    data: {
      path: `${path}/${folderName}`,
    },
  });
};
export const uploadFile = (
  connection: string,
  path: string,
  file: File,
  fileName: string,
  onUploadProgress: (progressEvent: AxiosProgressEvent) => void
) => {
  const formData = new FormData();
  formData.append('file', file);
  formData.append('fileName', fileName);
  formData.append('filePath', path);
  return axios.post(`${connection}/file/upload`, formData, {
    headers: {
      'Content-Type': 'multipart/form-data',
    },
    onUploadProgress,
  });
};
export const downloadFile = (
  connection: string,
  path: string,
  fileName: string,
  onDownloadProgress: (progressEvent: AxiosProgressEvent) => void
) => {
  return axios.get(
    `${connection}/file/download?path=${
      path + '/' + fileName
    }&name=${fileName}`,
    {
      responseType: 'blob',
      onDownloadProgress,
    }
  );
};
```

The combination of these hooks and the API definition create the interactions for the frontend to connect to the server. The React components can then call these hooks and do something like the following:

```typescript
  const { path, searchQuery } = useFilePageStore();
  const { files, loadFiles, loading } = useFilesStore();
  const { selectedServer } = useServerStore();
  useEffect(() => {
    if (!selectedServer) return;
    loadFiles(selectedServer.connection, path);
  }, [path, selectedServer]);
```

## Going beyond localhost with Docker <a name="going-beyond-localhost">

The Snorlax server exposes an API that an application can consume. The sample React application connects to the server and acts as a file management solution. 

This is a great start, but we can take it even further by [hosting the server in a Docker container](https://blog.logrocket.com/packaging-a-rust-web-service-using-docker/). You could create a Dockerfile that copies the application and then exposes it on the port of your choice:

```typescript
# syntax=docker/dockerfile:1
FROM golang:1.19-alpine
WORKDIR /server
COPY server ./
RUN go build -o /docker-snorlax
EXPOSE 8000
CMD [ "/docker-snorlax" ]
```

If you copy the code above and put it in a file named `Dockerfile` inside of the cloned Snorlax repo, you can build it with `docker build --tag docker-snorlax`. You can run the built image with `docker run docker-snorlax`, and you'll see the following:

```typescript
➜  snorlax git:(main) ✗ docker run docker-snorlax          
SNORLAX SERVER v1.0.0 🚀
=======================
Enter storage location [default ./storage/]: [i] CONNECTED TO STORAGE FOLDER storage/
=======================
Enter port where server should run [default 8000]: =======================
[+] SERVER STARTED AT PORT 8000
[i] http://127.0.0.1:8000
[i] http://172.17.0.2:8000
=======================
```

Now, you’ve containerized the server! You could do the same for the React application and then push this to a cloud provider for hosting. Obviously, you should also consider things like security and proxying requests, but these are the basics to get you started.

## Conclusion

In this article, we discussed how to get started using Snorlax as a file server. Following the sample application provided in the Snorlax repo, we were able to see the server running and learn how to leverage React to connect to it. We also discussed how to use Docker to containerize the server and potentially host it for a cloud application. 

Snorlax is a really nice software solution that is well built and thoroughly documented. I highly recommend checking out the sample repo and trying out some of the things discussed in this post. Thanks for reading! Follow my writing on [rhythmandbinary.com](https://rhythmandbinary.com/) and Twitter at [@AndrewEvans0102](https://twitter.com/AndrewEvans0102).

---

#Cut through the noise of traditional React error reporting with LogRocket

[LogRocket](https://lp.logrocket.com/blg/react-signup-issue-free) is a React analytics solution that shields you from the hundreds of false-positive errors alerts to just a few truly important items. LogRocket tells you the most impactful bugs and UX issues actually impacting users in your React applications.

IMAGE

[LogRocket](https://lp.logrocket.com/blg/react-signup-issue-free) automatically aggregates client side errors, React error boundaries, Redux state, slow component load times, JS exceptions, frontend performance metrics, and user interactions. Then LogRocket uses machine learning to notify you of the most impactful problems affecting the most users and provides the context you need to fix it.

Focus on the React bugs that matter — [try LogRocket today](https://lp.logrocket.com/blg/react-signup-issue-free).