[TypeScript][Express] Search and load local files
Tags: typescript, express
Published: 2021-10-30 20:53:37

## Intro
This time, I will try searching local files and returning them for getting requests.

### Environemtns

* Node.js ver.17.0.1
* TypeScript ver.4.4.4
* Express ver.4.17.1
* ts-node ver.10.4.0

### Directories & files

I want to get files from these directories.

```
sample_files
    L type1
        L subFolder
            L こんにちは.txt
        L webrtc_image.pptx
    L type2
        L sub folder
            L こんにちは.txt
    L Hello world!.txt
```

## Get directory and file informations

I can get directory and file informations by "fs.promises.readdir".

#### fileLoader.ts
```ts
import fs from "fs";

async function getFilePath(): Promise<void> {
    const directory = "C:/Users/example/OneDrive/Documents/workspace/express-sample/sample_files";    

    // get directory or file informations as fs.Dirent[]
    const paths = await fs.promises.readdir(directory, { withFileTypes: true});
    
    // get only file names as string[]
    // const paths = await fs.promises.readdir(directory);
    
    for(const p of paths) {
        console.log(`Path ${p.name} Dir:${p.isDirectory()}`);  
    }
}
```

#### Result
```
Path Hello world!.txt Dir:false
Path type1 Dir:true
Path type2 Dir:true
```

### Search sub directories

"fs.promises.readdir" can't get from sub directories.
So I have to do by my self.

#### fileLoader.ts
```ts
import fs from "fs";
import path from "path/posix";

const rootDirectory = "C:/Users/example/OneDrive/Documents/workspace/express-sample/sample_files";    
export async function loadFile(): Promise<void> {
    const gotPath = await getFilePath(rootDirectory);    
    console.log(gotPath);
}
async function getFilePath(directory: string): Promise<string|null> {
    // get directory or file informations as fs.Dirent[]
    const paths = await fs.promises.readdir(directory, { withFileTypes: true});
    for(const p of paths) {
        if(p.isDirectory()) {
            console.log(`Folder ${p.name} Dir:${directory}`);
            await getFilePath(path.join(directory, p.name));
        } else {
            console.log(`File ${p.name} Dir:${directory}`);
        }
    }
}
```

#### Results
```ts
File Hello world!.txt Dir:C:/Users/example/OneDrive/Documents/workspace/express-sample/sample_files
Folder type1 Dir:C:/Users/example/OneDrive/Documents/workspace/express-sample/sample_files
Folder subFolder Dir:C:/Users/example/OneDrive/Documents/workspace/express-sample/sample_files/type1
File こんにちは.txt Dir:C:/Users/example/OneDrive/Documents/workspace/express-sample/sample_files/type1/subFolder
File webrtc_image.pptx Dir:C:/Users/example/OneDrive/Documents/workspace/express-sample/sample_files/type1
Folder type2 Dir:C:/Users/example/OneDrive/Documents/workspace/express-sample/sample_files
Folder sub folder Dir:C:/Users/example/OneDrive/Documents/workspace/express-sample/sample_files/type2
File こんにちは.txt Dir:C:/Users/example/OneDrive/Documents/workspace/express-sample/sample_files/type2/sub folder
```

And search specified files by name.

#### fileLoader.ts
```ts
...
export async function loadFile(): Promise<void> {
    const gotPath = await getFilePath(rootDirectory, "webrtc_image.pptx");
    console.log(gotPath);
}
async function getFilePath(directory: string, fileName: string): Promise<string|null> {
    // get directory or file informations as fs.Dirent[]
    const paths = await fs.promises.readdir(directory, { withFileTypes: true});
    for(const p of paths) {
        if(p.isDirectory()) {
            const filePath = await getFilePath(path.join(directory, p.name), fileName);
            if(filePath != null) {
                return filePath;
            }
        } else {
            if(p.name === fileName) {
                return path.join(directory, p.name);
            }
        }
    }
    return null;
}
```

#### Results
```
C:/Users/example/OneDrive/Documents/workspace/express-sample/sample_files/type1/webrtc_image.pptx
```

* [File system - Node.js v17.0.1 documentation](https://nodejs.org/api/fs.html)
* [Node.js fs.readdir recursive directory search - Stack Overflow](https://stackoverflow.com/questions/5827612/node-js-fs-readdir-recursive-directory-search)

## Load files or create error messages

Next, I will get files from the gotten file paths and return the data.
But if I won't be able to find the files, I want to return error messages as JSON files.

#### file.type.ts
```ts
export type DownloadFile = {
    name: string,
    mimeType: "application/json"|"application/vnd.openxmlformats-officedocument.presentationml.presentation"|"text/plain",
    fileData: Buffer, 
};
```
#### fileLoader.ts
```ts
import fs from "fs";
import path from "path/posix";
import { DownloadFile } from "./file.type";

...  
export async function loadFile(fileName: string): Promise<DownloadFile> {
    const gotPath = await getFilePath(rootDirectory, fileName);    
    if(gotPath == null) {
        return getFailedMessage(`File: ${fileName} was not found`);
    }
    const extension = getMimeType(path.extname(gotPath));
    if(extension == null) {
        return getFailedMessage(`Unknown type File: ${fileName}`);
    }
    const fileData = await fs.promises.readFile(gotPath);
    return {
        name: fileName,
        mimeType: extension,
        fileData,
    };
}
...
function getFailedMessage(errorMessage: string): DownloadFile {
    const failedResult = {
        succeeded: false,
        errorMessage
    };
    const fileData = Buffer.from(JSON.stringify(failedResult));
    return {
        name: "failedResult.json",
        mimeType: "application/json",
        fileData,
    };
}
function getMimeType(value: string): "application/json"|
        "application/vnd.openxmlformats-officedocument.presentationml.presentation"|
        "text/plain"|
        null {
    switch(value) {
        case ".json":
            return "application/json";
        case ".pptx":
            return "application/vnd.openxmlformats-officedocument.presentationml.presentation";
        case ".txt":
            return "text/plain";
    }
    return null;
}
```

* [Common MIME types - HTTP | MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types)
* [JavaScript: The Definitive Guide, 7th Edition](https://www.oreilly.com/library/view/javascript-the-definitive/9781491952016/)
* [[Express][TypeScript] Downloading files](https://dev.to/masanori_msl/express-typescript-downloading-files-1n61)

## Return file data

I will set loaded or generated file data as response value.

#### index.ts
```ts
import express from "express";
import { loadFile } from "./files/fileLoader";

const port = 3098;
const app = express();

app.get('/files', async (req, res) => {
    // TODO: get file names from URL paramters
    const result = await loadFile("webrtc_image.pptx");
    res.setHeader("File-Name", result.name);
    res.setHeader("Content-Type", result.mimeType);
    
    res.write(result.fileData);
    res.end();
});
app.listen(port, () => {
    console.log(`Example app listening at http://localhost:${port}`)
});
```

### Set multi byte text into Response Headers

One problem is I can't set multi byte text into Response Headers.
Or I will get an exception.

```
TypeError [ERR_INVALID_CHAR]: Invalid character in header content ["File-Name"]
    at ServerResponse.setHeader (node:_http_outgoing:579:3)
    at ServerResponse.header (C:\Users\example\OneDrive\Documents\workspace\express-sample\node_modules\express\lib\response.js:771:10)
    at C:\Users\example\OneDrive\Documents\workspace\express-sample\index.ts:15:9
    at step (C:\Users\example\OneDrive\Documents\workspace\express-sample\index.ts:33:23)
    at Object.next (C:\Users\example\OneDrive\Documents\workspace\express-sample\index.ts:14:53)
    at fulfilled (C:\Users\example\OneDrive\Documents\workspace\express-sample\index.ts:5:58) {
  code: 'ERR_INVALID_CHAR'
}
```

So I encode the file name.

#### index.ts
```ts
...
app.get('/files', async (req, res) => {
    // TODO: get file names from URL paramters
    const result = await loadFile("こんにちは.txt");
    res.setHeader("File-Name", encodeURI(result.name));
    res.setHeader("Content-Type", result.mimeType);
    
    res.write(result.fileData);
    res.end();
});
...
```

* [Express 4.x API - API Reference](https://expressjs.com/en/api.html)
* [[TypeScript][Express][JsBarcode] Generate 1D barcode images](https://dev.to/masanori_msl/typescriptexpressjsbarcode-generate-1d-barcode-images-49mj)
* [encodeURI() - JavaScript | MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURI)