DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

Cover image for Let's create a React File Manager Chapter XVI: Create Server Requests
Hasan Zohdy
Hasan Zohdy

Posted on

Let's create a React File Manager Chapter XVI: Create Server Requests

This chapter is about creating server requests, let's start with the listing directory.

No Security

This project is not about security, so we'll not be adding any security layer to our server, we'll just make it work.

So don't use it in production, it's just for learning purposes.

Defining Routes

Let's start by listing the directory, we'll create a new route in our server, and we'll call it /file-manager and it will accept a path as query string.

Let's create src/routes.ts file and add the following code:

// routes.ts
import { Express } from "express";

export default function listRoutes(app: Express) {
  //
}
Enter fullscreen mode Exit fullscreen mode

Now let's import it from our src/index.ts file and call it.

// index.ts
// imported express server
import express, { Express } from "express";
πŸ‘‰πŸ» import listRoutes from "./routes";

// port to run the server
const port = 8001;

// πŸ‘‡πŸ» create express app
const app: Express = express();

listRoutes(app);

// start the Express server
app.listen(port, () => {
  console.log(`⚑️[server]: Server is running at http://localhost:${port}`);
});
Enter fullscreen mode Exit fullscreen mode

Now let's define our routes list

import { Express } from "express";

import fileManager from "./controllers/file-manager";

export default function listRoutes(app: Express) {
  // Let's define our routes
  // list directory nodes
  app.get("/file-manager", fileManager.list);
  // create a directory node
  app.post("/file-manage/directory", fileManager.createDirectory);
}
Enter fullscreen mode Exit fullscreen mode

We created two requests, one for listing the directory and the other for creating a directory.

Now let's create a new controllers directory which will manage our requests, create src/controllers/file-manager directory and inside it let's create an index file

// src/controllers/file-manager/index.ts
const fileManager = {
  list: listDirectory,
  createDirectory: createDirectory,
};

export default fileManager;
Enter fullscreen mode Exit fullscreen mode

We just created a simple object that contains our route mapping, ,list to list our directory and create to create a directory.

Now let's create a file for each of them, let's start with listDirectory file.

// src/controllers/file-manager/listDirectory.ts
import { Request, Response } from "express";

/**
 *List directories
 */
export default async function listDirectory(request: Request, response: Response) {
  //
}
Enter fullscreen mode Exit fullscreen mode

Now let's define the second function createDirectory

// src/controllers/file-manager/createDirectory.ts
import { Request, Response } from "express";

/**
 * Create a directory
 */
export default async function createDirectory(request: Request, response: Response) {
  //
}
Enter fullscreen mode Exit fullscreen mode

Let's import it now in our src/controllers/file-manager/index.ts file

// src/controllers/file-manager/index.ts
import createDirectory from "./createDirectory";
import listDirectory from "./listDirectory";

const fileManager = {
  list: listDirectory,
  createDirectory: createDirectory,
};

export default fileManager;
Enter fullscreen mode Exit fullscreen mode

Express request and response

As you can see above, when we define a function for a route we always receive two parameters, request and response, these are the request and response objects from Express as it injects to our function directly when the request route is match with its corresponding function.

Listing Directory

Now let's list our directory, we'll use @mongez/fs module to list our directory.

The path of the directory will be relative to /data directory in our root directory, so if we want to list the /data directory we'll just use / as the default path.

// src/controllers/file-manager/listDirectory.ts
import { Request, Response } from "express";

/**
 *List directories
 */
export default async function listDirectory(
  request: Request,
  response: Response
) {
  // get the directory path from the request query string
  // let's set `/` as the default path
  const { path = '/' } = request.query;
}
Enter fullscreen mode Exit fullscreen mode

Config file

Before we continue, we need to get the dataDirectory, we defined it in our generator file, let's create a config.ts file and add it there then we can import it in our files.

// src/config.ts
export const root = process.cwd();
export const dataDirectory = root + "/data";
Enter fullscreen mode Exit fullscreen mode

Now we import it in our generator file.

// src/generator.ts
import { faker } from "@faker-js/faker";
import fs from "@mongez/fs";
// import it here
import { dataDirectory } from "./config";

function start() {
  make(3, 3, dataDirectory);
}

start();

function makeFiles(maxFilesPerDirectory: number, path: string) {
  const filesList: string[] = [];

  while (filesList.length < maxFilesPerDirectory) {
    const file = faker.system.fileName();
    const filePath = path + "/" + file;
    if (!filesList.includes(filePath) && !fs.isFile(filePath)) {
      filesList.push(filePath);
      fs.put(filePath, faker.lorem.paragraphs());
    }
  }
}

function make(
  maxDirectories: number,
  maxFilesPerDirectory: number,
  path: string
) {
  const directoriesList: string[] = [];

  while (directoriesList.length < maxDirectories) {
    const directory = faker.system.directoryPath().split("/")[1];
    const directoryPath = path + "/" + directory;
    if (
      !directoriesList.includes(directoryPath) &&
      !fs.isDirectory(directoryPath)
    ) {
      directoriesList.push(directoryPath);
      fs.makeDirectory(directoryPath, 777);
      makeFiles(maxFilesPerDirectory, directoryPath);
      // to make recursive directories and files, you'll need to stop the script after some time
      //   and run it again
      //   make(maxDirectories, maxFilesPerDirectory, directoryPath);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Now let's get the full path of the directory and check if it exists.

// src/controllers/file-manager/listDirectory.ts
import fs from "@mongez/fs";
import { Request, Response } from "express";
import { dataDirectory } from "../../config";

/**
 *List directories
 */
export default async function listDirectory(
  request: Request,
  response: Response
) {
  // get the directory path from the request query string
  // let's set `/` as the default path
  const { path = "/" } = request.query;
  const directoryPath = dataDirectory + path; // the full path to the directory

  if (!fs.isDirectory(directoryPath)) {
    return response.status(404).json({
      message: "Directory not found",
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Here we got the full path of the given directory, then we checked if the directory does not exist we'll return a 404 response with a message to indicate directory is not found.

Paths utility

As we can see we added full directory path as follows:

const directoryPath = dataDirectory + path;
Enter fullscreen mode Exit fullscreen mode

Let's make a function to handle it properly.

Now let's create a utility file to handle our paths, let's create src/utils/paths.ts file

// src/utils/paths.ts
import path from "path";
import { dataDirectory } from "../config";

export function dataPath(relativePath: string): string {
  return path.resolve(dataDirectory, relativePath);
}
Enter fullscreen mode Exit fullscreen mode

Here we created a method to get the full path of the given relative path, we used path.resolve to resolve the full path to generate it properly.

Let's use it now in our listDirectory function.

// src/controllers/file-manager/listDirectory.ts

import fs from "@mongez/fs";
import { Request, Response } from "express";
import { dataPath } from "../../utils/paths";

/**
 *List directories
 */
export default async function listDirectory(
  request: Request,
  response: Response
) {
  // get the directory path from the request query string
  // let's set `/` as the default path
  const { path = "/" } = request.query;
  const directoryPath = dataPath(path); // the full path to the directory

  if (!fs.isDirectory(directoryPath)) {
    return response.status(404).json({
      message: "Directory not found",
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Relative Paths

As we can see our paths have ../../ which as you know really hate, so let's install link-module-alias

yarn add link-module-alias -D

Open now package.json and update it to look like this

{
  "name": "backend",
  "version": "1.0.0",
  "main": "src/index.ts",
  "license": "MIT",
  "author": {
    "name": "Hasan Zohdy",
    "email": "hassanzohdy@gmail.com"
  },
  "scripts": {
    // πŸ‘‡πŸ»  add this
    "postinstall": "npx link-module-alias",
    "generate": "npx ts-node ./src/generator.ts",
    "start": "npx ts-node ./src/index.ts"
  },
   // πŸ‘‡πŸ» and this
  "_moduleAliases": {
    "app": "./src"
  },
  "dependencies": {
    "@faker-js/faker": "^7.5.0",
    "@mongez/fs": "^1.0.3",
    "express": "^4.18.1"
  },
  "devDependencies": {
    "@types/express": "^4.17.14",
    "@types/node": "^18.7.18",
    "link-module-alias": "^1.2.0",
    "ts-node": "^10.9.1",
    "tslib": "^2.4.0",
    "typescript": "^4.8.3"
  }
}
Enter fullscreen mode Exit fullscreen mode

Final step is to run command postinstall to link the aliases.

yarn postinstall

If you see this, then everything is ok

postinstall

Tip Why we named the command postinstall? because this is a special command that will be executed after installing the dependencies, so we don't need to run it manually.

Now let's update or listDirectory function to use the new relative path.

// src/controllers/file-manager/listDirectory.ts
import fs from "@mongez/fs";
πŸ‘‰πŸ» import { dataPath } from "app/utils/paths";
import { Request, Response } from "express";

/**
 *List directories
 */
export default async function listDirectory(
  request: Request,
  response: Response
) {
  // get the directory path from the request query string
  // let's set `/` as the default path
  const { path = "/" } = request.query;
  const directoryPath = dataPath(path); // the full path to the directory

  if (!fs.isDirectory(directoryPath)) {
    return response.status(404).json({
      message: "Directory not found",
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

If you get an error from typescript that the function can not accept the path, we can cast it to string

Path Error

import fs from "@mongez/fs";
import { dataPath } from "app/utils/paths";
import { Request, Response } from "express";

/**
 *List directories
 */
export default async function listDirectory(
  request: Request,
  response: Response
) {
  // get the directory path from the request query string
  // πŸ‘‡πŸ» let's set `/` as the default path
  const path = (request.query.path as string) || "/";
  const directoryPath = dataPath(path); // the full path to the directory

  if (!fs.isDirectory(directoryPath)) {
    return response.status(404).json({
      message: "Directory not found",
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Now let's list our directories inside the root path of data directory.

// src/controllers/file-manager/listDirectory.ts

...
  // get the directory content
  const children = fs.list(directoryPath);

  return response.json({
    node: {
      path,
      name: path.split("/").pop(),
      children,
    },
  });
Enter fullscreen mode Exit fullscreen mode

Now let's start the server again and test it.

Navigate to our browser to http://localhost:8001/file-manager?path=/ and you'll see the following response:

The output will be like this

Result

Why? because we didn't resolve the paths right, go to dataPath function and try to log the output of it.

It will be /!

Let's fix it by making sure the relative path start with ./

So we need to remove any / and add ./ if it doesn't start with it.

Install @mongez/reinforcements so we can use some good utilities from it.

yan add @mongez/reinforcements

Now let's modify the dataPath function to look like this

// src/utils/paths.ts
import { ltrim } from "@mongez/reinforcements";
import path from "path";
import { dataDirectory } from "../config";

export function dataPath(relativePath: string): string {
  relativePath = ltrim(relativePath, "/");
  return path.resolve(dataDirectory, relativePath);
}
Enter fullscreen mode Exit fullscreen mode

ltrim removes the given string from the beginning of the string, so we will remove / to make sure the path is properly set.

Now when we restart the server, we can see results are coming properly.

Response

In our next chapter, we'll create our node properly and distinguish between children files and directories and create createDirectory request as well.

Article Repository

You can see chapter files in Github Repository

Don't forget the main branch has the latest updated code.

Tell me where you are now

If you're following up with me this series, tell me where are you now and what you're struggling with, i'll try to help you as much as i can.

Salam.

Top comments (0)

Hey 😍

Want to help the DEV Community feel more like a community?

Head over to the Welcome Thread and greet some new community members!

It only takes a minute of your time, and goes a long way!