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

Cover image for Let's create a File Manager From Scratch With React And Typescript Chapter III: Fetching Root Directory
Hasan Zohdy
Hasan Zohdy

Posted on

Let's create a File Manager From Scratch With React And Typescript Chapter III: Fetching Root Directory

Now our basic setup is ready, let's make a simple mockup api for our file manager.

File Manager Service

Let's create file-manager-service.ts under file-manager/services directory then let's establish it.

// file-manager-service.ts
export class FileManagerService implements FileManagerServiceInterface {

}
Enter fullscreen mode Exit fullscreen mode

We just created an empty class that implements FileManagerServiceInterface interface which is clearly not defined yet, so let's define it.

FileManagerServiceInterface Interface

Let's think a little bit here, what possibly requests will be used in our file manager, let's list them all, or at least the basic requests.

  • List Directory contents
  • Upload Files
  • Rename Directory/File
  • Delete Directory/File
  • Create Directory
  • Move Directory/File
  • Copy Directory/File

So we've like 6-7 requests to do, let's create their interface.

Head to file-manager/types and create FileManagerServiceInterface.ts file

// file-manager/types/FileManagerServiceInterface.ts
export default interface FileManagerServiceInterface {
  /**
   * List directory contents for the given path
   */
  list(directoryPath: string): Promise<any>;

  /**
   * Create new directory
   *
   * First parameter will be the directory name,
   * second parameter will be the directory path that will be created into.
   */
  createDirectory(directoryName: string, saveTo: string): Promise<any>;

  /**
   * Delete directories/files
   */
  delete(paths: string[]): Promise<any>;

  /**
   * Rename directory | file
   *
   * The first parameter will be the old path,
   * the second parameter will be the new path.
   */
  rename(path: string, newPath: string): Promise<any>;

  /**
   * Copy the given files/directories to the given destination
   */
  copy(paths: string[], destination: string): Promise<any>;

  /**
   * Move the given files/directories to the given destination
   */
  move(paths: string[], destination: string): Promise<any>;

  /**
   * Upload the given files into the given directory path
   */
  upload(files: File[], directoryPath: string): Promise<any>;
}
Enter fullscreen mode Exit fullscreen mode

Now let's import our interface in our FileManagerService file.

// file-manager-service.ts
import FileManagerServiceInterface from "../types/FileManagerServiceInterface";

export class FileManagerService implements FileManagerServiceInterface {}
Enter fullscreen mode Exit fullscreen mode

Of course now the Typescript complier will complain that we didn't implement these methods, so let's do it.

But before we do this, let's install first Faker so we can fake some data.

yarn add -D @faker-js/faker

Now let's return to our service class.

import FileManagerServiceInterface from "../types/FileManagerServiceInterface";

export class FileManagerService implements FileManagerServiceInterface {
  /**
   * {@inheritDoc}
   */
  public list(directoryPath: string): Promise<any> {
    return new Promise(resolve => {
      resolve({
        data: {
          nodes: listNodes(),
        },
      });
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

We made our promise to mock axios response shape so once we're done with the mockup api, we can easily replace it with real api at anytime.

If you've a real API to work with, i'll be pleased to use in the series :)

and the list method will return list of nodes as array.

Now let's create listNodes function from faker, it will be created in utils directory under data.ts file.

// file-manager/utils/data.ts
import { faker } from "@faker-js/faker";
import { DirectoryNode, Node } from "../types/FileManager.types";

export function newNode(): Node {
  const node: Node | DirectoryNode = {
    name: faker.system.fileName(),
    path: faker.system.filePath(),
    size: faker.datatype.number({ min: 1, max: 100000 }),
    isDirectory: faker.datatype.boolean(),
  };

  if (node.isDirectory) {
    (node as DirectoryNode).children = listNodes(3);
  }

  return node;
}

export function listNodes(maxNodes = 10): Node[] {
  return faker.datatype
    .array(faker.datatype.number({ min: 1, max: maxNodes }))
    .map(newNode);
}
Enter fullscreen mode Exit fullscreen mode

In the second function listNodes it will create list of nodes randomly and it accepts one parameter, the max generated nodes, this is important so we don't get in infinity loop when generating children nodes as we maximize it with only 3 children nodes (in newNode function).

newNode function is used to generated random node data, we set the node that it can be Node or DirectoryNode.

Actually, let's work only with Node and set the children property inside it, so if the node is directory it can have children.

Let's navigate to types/FileManager.types.ts to remove FileNode and DirectoryNode then update our Node.

// file-manager/types/FileManager.types.ts
/**
 * File Manager node is the primary data structure for the File Manager.
 * It can be a directory or a file.
 * It contains the following properties:
 */
export type Node = {
  /**
   * Node Name
   */
  name: string;
  /**
   * Node full path to root
   */
  path: string;
  /**
   * Node size in bits
   */
  size: number;
  /**
   * Is node directory
   */
  isDirectory: boolean;
  /**
   * Node children
   * This should be present (event with empty array) if the node is directory
   */
  children?: Node[];
};
Enter fullscreen mode Exit fullscreen mode

This will make our code less confused so do us lol, so the node can have two types of shapes, a directory shape that contains children and a file shape that does not have children.

Let's go to FileManager class and update the current directory node.

// file-manager/utils/FileManager.ts
import { Node } from "../types/FileManager.types";

export default class FileManager {
  /**
   * Root path
   */
  protected rootPath = "/";

  /**
   * Current directory path
   */
  protected currentDirectoryPath = "/";

  /**
   * Current directory node
   */
  protected currentDirectoryNode?: Node;
}
Enter fullscreen mode Exit fullscreen mode

Finally let's update again our utils/data.ts file

// file-manager/utils/data.ts
import { faker } from "@faker-js/faker";
import { Node } from "../types/FileManager.types";

export function newNode(): Node {
  const node: Node = {
    name: faker.system.fileName(),
    path: faker.system.filePath(),
    size: faker.datatype.number({ min: 1, max: 100000 }),
    isDirectory: faker.datatype.boolean(),
  };

  if (node.isDirectory) {
    node.children = listNodes(3);
  }

  return node;
}

export function listNodes(maxNodes = 10): Node[] {
  return faker.datatype
    .array(faker.datatype.number({ min: 1, max: maxNodes }))
    .map(newNode);
}
Enter fullscreen mode Exit fullscreen mode

Now our code is much cleaner.

Pro Tip: you don't get the perfect code from the first time, that's why we're going to enhance and develop our code gradually as we progress in our project.

Heading back to our File Manager Service.

import FileManagerServiceInterface from "../types/FileManagerServiceInterface";
import { listNodes } from "../utils/data";

export class FileManagerService implements FileManagerServiceInterface {
  /**
   * {@inheritDoc}
   */
  public list(directoryPath: string): Promise<any> {
    return new Promise(resolve => {
      resolve({
        data: {
          nodes: listNodes(),
        },
      });
    });
  }

  /**
   * {@inheritDoc}
   */
  public createDirectory(directoryName: string, saveTo: string): Promise<any> {
    throw new Error("Method not implemented.");
  }
  /**
   * {@inheritDoc}
   */
  public delete(paths: string[]): Promise<any> {
    throw new Error("Method not implemented.");
  }
  /**
   * {@inheritDoc}
   */
  public rename(path: string, newPath: string): Promise<any> {
    throw new Error("Method not implemented.");
  }
  /**
   * {@inheritDoc}
   */
  public copy(paths: string[], destination: string): Promise<any> {
    throw new Error("Method not implemented.");
  }
  /**
   * {@inheritDoc}
   */
  public move(paths: string[], destination: string): Promise<any> {
    throw new Error("Method not implemented.");
  }
  /**
   * {@inheritDoc}
   */
  public upload(files: File[], directoryPath: string): Promise<any> {
    throw new Error("Method not implemented.");
  }
}
Enter fullscreen mode Exit fullscreen mode

I made the rest of the code to be auto injected using vscode feature or so, so it doesn't pop any compiler error.

So far so good, i'll stop at the moment at this point.

You can see the results in the HomePage component.

Head to the component and paste the following code inside it then see the results in the console.

// home/components/HomePage/HomePage.tsx
import Helmet from "@mongez/react-helmet";
import { FileManagerService } from "app/file-manager/services/file-manager-service";

const fileManager = new FileManagerService();

export default function HomePage() {
  fileManager.list("/").then(response => {
    console.log(response.data);
  });
  return (
    <>
      <Helmet title="home" appendAppName={false} />
      <h1>Welcome To Home Page</h1>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Article Repository

You can see chapter files in Github Repository

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

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!