DEV Community

Cover image for A Simple Next.js File Server
Isaac FEI
Isaac FEI

Posted on • Originally published at isaacfei.com

A Simple Next.js File Server

Introduction

The complete project repository is available here.

We will be building a simple file server that functions as a web-based file explorer for a specified directory FILE_STORAGE_ROOT_DIR on your server. The key features include:

  • File Browsing: View files and directories under a given path (of course, it should be under FILE_STORAGE_ROOT_DIR)
  • File Downloading: Pick and download the files from the server as needed
  • API Key Authentication: Simple but effective protection

Why I Built This

I am running several microservices on a remote server that continuously generate data and files.
At some point in my life, I need to download these outputs for data analysis
or machine learning projects - whether it's processing logs, training models, or just examining results.

While many file storage and management solutions like Nginx exist,
I always prefer crafting my own tools 💪🛠️.

App Screenshots

Auth page:

Auth Page

File storage page:

Storage Page 1

After clicking the logs directory, the page will list all the files and directories under the logs directory:

Storage Page 2

Clicking the abc.log file will download the file.

Setup

  • Next.js 15 (App Router)
  • shadcn/ui
  • pnpm

Project Structure

Other unimportant files are omitted.

file-server-nextjs/
├── src/                              # Primary source code
│   ├── app/                          # Next.js app router
│   │   ├── api/
│   │   │   ├── files/route.ts        # GET /api/files
│   │   │   └── download/[...pathSegments]/route.ts  # Dynamic download endpoint
│   │   ├── storage/[[...pathSegments]]/page.tsx  # File browser UI
│   │   ├── auth/page.tsx             # Authentication page
│   │   └── layout.tsx                # Root layout
│   │
│   ├── components/
│   │   ├── ui/                       # Shadcn UI components
│   │   ├── file-system-entry-view.tsx  # File system entry view
│   │   ├── api-key-input.tsx         # API key input
│   │   └── sign-in-form-data.ts      # Auth form schema
│   │
│   ├── models/                       # Data models
│   │   ├── file-system-entry.ts      # File/directory model
│   │   └── sign-in-form-data.ts      # Auth form schema
│   │
│   └── lib/                          # Business logic
│       ├── services/                  
│       │   ├── list-directory.ts     # Filesystem operations
│       │   └── index.ts              # Service exports
│       ├── auth.ts                   # Authentication logic
│       └── utils.ts                  # Shared utilities
│
├── middleware.ts                     # Edge middleware
├── Dockerfile                        # Production build
├── .dockerignore
├── docker-compose.example.yml        # Example config
├── docker-compose.yml                # Local dev config
├── .env.example                      # Env template
├── .env.local                        # Local development
├── .env.production                   # Production env
├── next.config.ts                    # Next.js config
└── package.json
Enter fullscreen mode Exit fullscreen mode

File Server API

List Files & Directories

First, we define the structure for file/directory entries using Zod:

// src/models/file-system-entry.ts

import { z } from "zod";

export const FileSystemEntrySchema = z.object({
  name: z.string(),
  path: z.string(),
  isDirectory: z.boolean(),
});

export type FileSystemEntry = z.infer<typeof FileSystemEntrySchema>;
Enter fullscreen mode Exit fullscreen mode

This schema ensures each entry contains:

  • name: File/directory name
  • path: Relative path from root
  • isDirectory: Boolean flag for type checking

The business logic is encapsulated in a reusable service.
We will use it both in the API route and the server component src/app/storage/[[...pathSegments]]/page.tsx.

// src/lib/services/list-directory.ts

import type { FileSystemEntry } from "@/models/file-system-entry";
import fs from "fs";
import path from "path";

export async function listDirectory(relativeDirectoryPath: string) {
  // Construct an absolute directory path
  const directoryPath = path.join(
    process.env.FILE_STORAGE_ROOT_DIR!,
    relativeDirectoryPath,
  );

  // Names of the files and directories
  const names = fs.readdirSync(directoryPath);

  const fileSystemEntries: FileSystemEntry[] = names.map((name) => {
    const stats = fs.statSync(path.join(directoryPath, name));
    return {
      name,
      path: path.join(relativeDirectoryPath, name),
      isDirectory: stats.isDirectory(),
    };
  });

  return fileSystemEntries;
}
Enter fullscreen mode Exit fullscreen mode

The following is the implmentation fo the API route.
It is a GET request that takes a path query parameter and returns a list of FileSystemEntry.

In Next.js, we can get the query parameter from the request object like this: request?.nextUrl?.searchParams.get("key").

// src/app/api/files/route.ts

import { NextRequest, NextResponse } from "next/server";
import { listDirectory } from "@/lib/services/list-directory";

export async function GET(request: NextRequest) {
  // Get the path from the query parameters
  const relativeDirectoryPath =
    request?.nextUrl?.searchParams.get("path") || "";

  // Get the file system entries
  const fileSystemEntries = await listDirectory(relativeDirectoryPath);

  return NextResponse.json(fileSystemEntries);
}
Enter fullscreen mode Exit fullscreen mode

Download a File

The simplest way to stream files in Next.js is using Node.js's native readableWebStream() method (official documentation).

// src/app/api/download/[...pathSegments]/route.ts

import { NextRequest, NextResponse } from "next/server";
import fs from "fs";
import path from "path";

export async function GET(
  request: NextRequest,
  {
    params,
  }: {
    params: Promise<{
      pathSegments: string[];
    }>;
  },
) {
  const { pathSegments } = await params;

  // Get file path
  const filePath = path.join(
    process.env.FILE_STORAGE_ROOT_DIR!,
    ...pathSegments,
  );

  // Open the file
  const fileHandle = await fs.promises.open(filePath);

  // Create a readable stream that can be passed to NextResponse
  const stream = fileHandle.readableWebStream({
    type: "bytes",
  }) as ReadableStream<Uint8Array>;

  return new NextResponse(stream);
}
Enter fullscreen mode Exit fullscreen mode

This API uses catch-all route ([...pathSegments]) to support nested directories.
You can get the path segments by defining the params in the function signature.

One more thing to note is that although we didn't use the parameter request here,
we still need to define it in the function signature!
Otherwise, the route handler will not be recognized by Next.js.

Althernative approaches of streaming files are available in the following awesome blog post: How to stream files from Next.js Route Handlers.

Build the UI for Browsing and Downloading Files

A component for displaying both files and directories with appropriate icons and interactions:

// src/app/components/file-system-entry-view.tsx

import type { FileSystemEntry } from "@/models/file-system-entry";
import { FileIcon, FolderIcon } from "lucide-react";

interface FileSystemEntryViewProps extends FileSystemEntry {}

export function FileSystemEntryView({
  name,
  path,
  isDirectory,
}: FileSystemEntryViewProps) {
  const href = isDirectory ? `/storage/${path}` : `/api/download/${path}`;
  const download = isDirectory ? undefined : name;
  const Icon = isDirectory ? FolderIcon : FileIcon;

  return (
    <a
      href={href}
      download={download}
      className="hover:bg-muted flex flex-row items-center justify-between gap-2 rounded-md p-2"
    >
      <div className="flex flex-row items-center gap-2">
        <Icon size={16} className="opacity-60" />
        <span className="line-clamp-1">{name}</span>
      </div>
    </a>
  );
}
Enter fullscreen mode Exit fullscreen mode
  • If it is a directory, it will link to the storage page (implemented below) with the path segments added.
  • If it is a file, it will link to the download API route.

Note that I used an anchor tag <a> here instrad of Next.js's Link component.
This is because the Link component does not trigger the download of the file somehow 🤔.
Instead, it will display the file content in a browser tab.

The main storage page uses Next.js's catch-all route to handle nested directory paths:

The main page is grouped under storage (for the ease of middleware matcher later) with dynamic catch-all route [[...pathSegments]].

We will intentionally exclude a dedicated home page (/) to simplify our middleware's route matching logic.
By redirecting the root path directly to /storage, we maintain a single common entry point
for all file storage routes, which should be authenticated.

To do this, in next.config.ts, we can set the redirects option:

// next.config.ts
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  /* config options here */
+  redirects: async () => {
+    return [
+      {
+        source: "/",
+        destination: "/storage",
+        permanent: false,
+      },
+    ];
+  },
};

export default nextConfig;
Enter fullscreen mode Exit fullscreen mode

File storage page:

// src/app/storage/[[...pathSegments]]/page.tsx

import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { FileSystemEntryView } from "@/components/file-system-entry-view";
import { listDirectory } from "@/lib/services";

export default async function Page({
  params,
}: {
  params: Promise<{ pathSegments?: string[] }>;
}) {
  const pathSegments = (await params).pathSegments ?? [];
  const directoryPath = pathSegments.join("/");
  const fileSystemEntries = await listDirectory(directoryPath);

  return (
    <div className="flex h-full flex-col items-center justify-center p-8">
      <Card className="flex h-full max-h-[40rem] w-full max-w-[40rem] flex-col">
        <CardHeader>
          <CardTitle>File Server</CardTitle>
        </CardHeader>

        <CardContent className="flex h-full flex-col overflow-y-auto">
          {fileSystemEntries.map((fileSystemEntry, index) => (
            <FileSystemEntryView key={index} {...fileSystemEntry} />
          ))}
        </CardContent>
      </Card>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

It is a server component that:

  • Uses [[...pathSegments]] to handle both root and nested paths
  • Empty path fallback for root directory
  • Renders the file system entries using the FileSystemEntryView component with the data obtained from the listDirectory service

Add API Key Authentication

I want to achieve the following:

  1. API Protection

    • All API endpoints (/api/files, /api/download) require:
    Authorization: Bearer <API_KEY>
    
  2. UI Protection

    • Middleware redirects unauthenticated users to /auth
    • Valid JWT in cookies grants access for a certain period of time, say 8 hours

JWT

We use JWTs in this project to maintain user sessions without requiring repeated API key entry. When users first authenticate with their API key, we generate a time-limited JWT and store it in a cookie. This approach provides the following benefits:

  • users stay logged in across page visits,
  • the server can validate sessions without storing sensitive API keys.

The package I chose to handle the JWT generation and decryption is jose instead of jsonwebtoken since the former also works in the Edge runtime.

// src/lib/auth/route.ts

import * as jose from "jose";

export async function createAuthToken() {
  // Encode the JWT secret
  const secret = new TextEncoder().encode(process.env.JWT_SECRET!);

  // Create a JWT token
  const authToken = await new jose.EncryptJWT({
    authorized: true,
  })
    .setProtectedHeader({ alg: "dir", enc: "A256GCM" })
    .setIssuedAt()
    .setExpirationTime(process.env.JWT_EXPIRATION_TIME!)
    .encrypt(secret);

  return authToken;
}

export async function decryptAuthToken(authToken: string) {
  // Encode the JWT secret
  const secret = new TextEncoder().encode(process.env.JWT_SECRET!);

  // Decrypt the auth token
  const { payload } = await jose.jwtDecrypt(authToken, secret, {
    contentEncryptionAlgorithms: ["A256GCM"],
    keyManagementAlgorithms: ["dir"],
  });

  return payload;
}
Enter fullscreen mode Exit fullscreen mode

Note that, in the above implementation, we are actually generating a encrypted JWT.

If the payload does not contain any sensitive information, we can use a plain JWT instead by using jose.SignJWT and jose.jwtVerify.
(In this case, the JWT can be decoded by everyone. It is ok for this project since the payload is just a simple object {authorized: true}.)

Auth API

The logic of the auth API is straightforward.

  • Verify the API key
  • Generate a JWT and store it in the cookie

In Next.js we can use the funciton cookies from next/headers to get and set cookies.

// src/app/api/auth/sign-in/route.ts

import { NextRequest, NextResponse } from "next/server";
import { SignInFormDataSchema } from "@/models/sign-in-form-data";
import { cookies } from "next/headers";
import { createAuthToken } from "@/lib/auth";

export async function POST(request: NextRequest) {
  // Parse the request data
  const signInFormData = SignInFormDataSchema.parse(await request.json());

  // Get the API key
  const apiKey = signInFormData.apiKey;

  if (apiKey !== process.env.API_KEY!) {
    return NextResponse.json({ error: "invalid API key" }, { status: 400 });
  }

  // Create a JWT token
  const authToken = await createAuthToken();

  // Put the token into the cookie
  const cookieStore = await cookies();
  cookieStore.set("auth-token", authToken);

  return NextResponse.json({
    authToken,
  });
}
Enter fullscreen mode Exit fullscreen mode

Build the UI: API Key Input, Sign-In Form and Auth Page

When bulding the sign-in form, we follow the suggested steps from Shadcn's React Hook Form:

  1. Define the form data schema (already done)
  2. Use useForm to create the form instance
  3. Use Form to wrap the <form> HTML element
  4. Use FormField to wrap the form field UI (e.g., input, textarea, radio group, etc.)

The schema of the form data is simple, which is just an API key:

// src/models/sign-in-form-data.ts

import { z } from "zod";

export const SignInFormDataSchema = z.object({
  apiKey: z.string().min(1),
});

export type SignInFormData = z.infer<typeof SignInFormDataSchema>;
Enter fullscreen mode Exit fullscreen mode

Next, we create an API key input component in the following.
It is modified from another awesome UI library Origin UI, which is built based on shadcn/ui.

// src/components/api-key-input.tsx

"use client";

import { useState } from "react";

import { Input } from "@/components/ui/input";
import { EyeIcon, EyeOffIcon, KeyRoundIcon } from "lucide-react";
import { cn } from "@/lib/utils";

export function ApiKeyInput(props: React.ComponentProps<"input">) {
  const [isVisible, setIsVisible] = useState<boolean>(false);

  const toggleVisibility = () => setIsVisible((prevState) => !prevState);

  const { className, ...restInputProps } = props;

  return (
    <div className="relative">
      <Input
        className={cn("ps-9 pe-9", className)}
        placeholder="API Key"
        type={isVisible ? "text" : "password"}
        {...restInputProps}
      />

      <div className="text-muted-foreground/80 pointer-events-none absolute inset-y-0 start-0 flex items-center justify-center ps-3 peer-disabled:opacity-50">
        <KeyRoundIcon size={16} />
      </div>

      <button
        className="text-muted-foreground/80 hover:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 absolute inset-y-0 end-0 flex h-full w-9 items-center justify-center rounded-e-md transition-[color,box-shadow] outline-none focus:z-10 focus-visible:ring-[3px] disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50"
        type="button"
        onClick={toggleVisibility}
        aria-label={isVisible ? "Hide API key" : "Show API key"}
        aria-pressed={isVisible}
        aria-controls="apiKey"
      >
        {isVisible ? (
          <EyeOffIcon size={16} aria-hidden="true" />
        ) : (
          <EyeIcon size={16} aria-hidden="true" />
        )}
      </button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Finally, the sign-in form:

// src/components/sign-in-form.tsx

"use client";

import {
  type SignInFormData,
  SignInFormDataSchema,
} from "@/models/sign-in-form-data";

import { LockIcon, LockOpenIcon } from "lucide-react";
import { Card, CardContent } from "@/components/ui/card";
import { Form, FormControl, FormField, FormItem } from "@/components/ui/form";
import { ApiKeyInput } from "@/components/api-key-input";
import { Button } from "@/components/ui/button";

import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import axios from "axios";
import { useRouter } from "next/navigation";

export function SignInForm() {
  const form = useForm<SignInFormData>({
    resolver: zodResolver(SignInFormDataSchema),
    defaultValues: {
      apiKey: "",
    },
  });

  const router = useRouter();

  const submitFormData = async (formData: SignInFormData) => {
    // Clear the form
    form.reset();

    // Send the request
    try {
      // Send the request to sign in
      await axios.post("/api/auth/sign-in", formData);

      // Redirect to the home page
      router.push("/");
    } catch (e) {
      alert(e);
    }
  };

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(submitFormData)}>
        <Card>
          <CardContent className="flex flex-col items-center justify-center gap-6">
            <LockIcon className="text-muted-foreground size-24" />

            <div className="flex flex-col gap-4">
              <FormField
                name="apiKey"
                render={({ field }) => (
                  <FormItem>
                    <FormControl>
                      <ApiKeyInput {...field} />
                    </FormControl>
                  </FormItem>
                )}
              />

              <Button type="submit">
                <LockOpenIcon
                  className="-ms-1 opacity-60"
                  size={16}
                  aria-hidden="true"
                />
                Unlock
              </Button>
            </div>
          </CardContent>
        </Card>
      </form>
    </Form>
  );
}
Enter fullscreen mode Exit fullscreen mode

The auth page is just a simple page that contains the SignInForm component.

// src/app/auth/page.tsx

import { SignInForm } from "@/components/sign-in-form";

export default function Page() {
  return (
    <div className="flex h-full flex-col items-center justify-center">
      <SignInForm />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Middleware

// src/middleware.ts

import { NextRequest, NextResponse } from "next/server";
import { cookies } from "next/headers";
import { decryptAuthToken } from "@/lib/auth";

export async function middleware(request: NextRequest) {
  // Get the auth header
  const authHeader = request.headers.get("authorization");

  if (authHeader !== null) {
    // Get the API key from the bearer token
    const apiKey = authHeader.replace("Bearer ", "");

    if (apiKey === process.env.API_KEY!) {
      return NextResponse.next();
    } else {
      return NextResponse.json({ error: "invalid API key" }, { status: 400 });
    }
  }

  // Get the cookies
  const cookieStore = await cookies();

  // Get the auth token
  const authToken = cookieStore.get("auth-token")?.value;

  if (authToken === undefined) {
    return NextResponse.redirect(new URL("/auth", request.url));
  }

  try {
    // Decrypt the auth token
    const { payload } = await decryptAuthToken(authToken);

    // We don't need to do anything with the payload though
  } catch (e) {
    console.error(`failed to verify the auth token: ${e}`);
    return NextResponse.redirect(new URL("/auth", request.url));
  }

  return NextResponse.next();
}

export const config = {
  matcher: ["/storage/:path*", "/api/((?!auth\\b).*)"],
};
Enter fullscreen mode Exit fullscreen mode

This middleware handles authentication for protected routes using two distinct methods:

The middleware activates for:

  • All paths beginning with /storage/ (using the :path* wildcard to match nested paths)
  • All API endpoints except /api/auth (using the regex (?!auth\\b) to exclude the auth route specifically)

The authentication process is as follows:

  1. API Key Verification

    • The middleware first checks for an Authorization header
    • If present, it validates the API key against your environment variable
    • Valid keys proceed to the route handler
    • Invalid keys receive a 400 error response
  2. Session Cookie Fallback

    • When no API key is provided, the middleware checks for an auth-token cookie
    • Missing tokens trigger an immediate redirect to the authentication page
    • Existing tokens are decrypted and validated
    • Failed validation also redirects to the auth page

Flow diagram:

Request → [Has API Key?] → Yes → [Valid?] → Yes → Proceed
                     ↓ No
                     [Has Auth Token?] → No → Redirect to /auth
                     ↓ Yes
                     [Valid Token?] → No → Redirect to /auth
                     ↓ Yes
                     Proceed
Enter fullscreen mode Exit fullscreen mode

Dockerize the app

To containerize this app, copy Next.js's official Dockerfile into the project root.
No modifications are needed.
But ensure next.config.ts enables output: "standalone" for optimized deployment, alongside your existing redirect rules.

// next.config.ts

import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  /* config options here */
+  output: "standalone",
  redirects: async () => {
    return [
      {
        source: "/",
        destination: "/storage",
        permanent: false,
      },
    ];
  },
};

export default nextConfig;
Enter fullscreen mode Exit fullscreen mode

Environment Variables

We have seen from previous code snippets,
this application requires several environment variables for authentication, security, and file storage configuration.
Below is an example .env.example file, which serves as a template for setting up your environment:

# .env.example

# API key for accessing the file server
API_KEY=

# 256-bit JWT encryption key
JWT_SECRET=

# JWT expiration time
JWT_EXPIRATION_TIME=8h

# File storage directory
FILE_STORAGE_ROOT_DIR=
Enter fullscreen mode Exit fullscreen mode
  1. For Development
  2. Copy .env.example to .env.local and fill in test values.
  3. Set the JWT_EXPIRATION_TIME to a shorter time, e.g., 1min for the ease of development.
  4. Remember to ignore .env.local in .gitignore.

  5. For Production

  6. Create .env.production with real credentials.

  7. Also remember to ignore .env.production in .gitignore.

  8. Fill in the missing environment variables.

  9. Ensure FILE_STORAGE_ROOT_DIR exists and has proper read/write permissions.

Write a Docker Compose File

The following is the example of the docker-compose.yml file:

// docker-compose.example.yml

version: "3.8"

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
      args:
        - NODE_ENV=production
    env_file: .env.production
    ports:
      - ${HOST_PORT}:3000
    volumes:
      # Mount host file storage directory as read-only volume
      - ${HOST_FILE_STORAGE_DIR}:/app/file-storage:ro
Enter fullscreen mode Exit fullscreen mode

The Docker image uses /app as its working directory (review the Dockerfile). Within this, /app/file-storage serves as the container's storage path - this directory is automatically created during container initialization and mounts to your designated host directory without requiring manual creation.

The ro flag ensures read-only access from the container for security.

Note from the Dockerfile that the project root inside the image is /app.
The virtual file storage directory will be designe as /app/file-storage. (file-storage needs not to be created beforehand. it will be mounted to the specified directory on the host machine.)

For production, copy the docker-compose.example.yml to docker-compose.yml and

  • Replace ${HOST_PORT} with your desired port (e.g., 8080).
  • Replace ${FILE_STORAGE_ROOT_DIR} with the path to the directory on the host machine.

Then, you may build the image and run the container on your local machine via command:

docker-compose up -d
Enter fullscreen mode Exit fullscreen mode

Bonus: Multi-Platform Build

As I'm developing this project on my M2 Mac (which uses ARM64 architecture),
when I built my container locally, pushed to Docker Hub,
and then tried pulling and running it on my production server (which uses AMD64 architecture), I got those frustrating exec format error messages.
Of course this happened - the architectures were completely incompatible.

The solution is to build the image for both ARM64 and AMD64 architectures.

If you have never run a multi-platform build before, you need to create a builder instance first by running:

docker buildx create \
  --name multi-platform \
  --use \
  --platform linux/amd64,linux/arm64 \
  --driver docker-container
Enter fullscreen mode Exit fullscreen mode
  • --name multi-platform
    • Gives your builder a descriptive label (e.g., multi-platform).
    • Helpful if you manage multiple builders (e.g., for different projects).
  • --use
    • Immediately sets this builder as the default for all docker buildx operations.
    • Without this, you’d need to manually specify the builder each time.
  • --platform linux/amd64,linux/arm64
    • Specifies the platforms you want to build for.
  • --driver docker-container
    • Uses an isolated container runtime (instead of your local Docker daemon).
    • Enables QEMU emulation, allowing your machine to build ARM images on Intel hardware (and vice versa).

After that, you can build the image for both architectures by running:

docker buildx build -t isaacfei/file-server-nextjs:0.1.0 --platform linux/amd64,linux/arm64 --push .
Enter fullscreen mode Exit fullscreen mode
  • -t isaacfei/file-server-nextjs:0.1.0
    • Tag flag (names your image)
    • Format: [username]/[repository]:[version]
    • Example breaks down to:
    • isaacfei = Your Docker Hub username
    • file-server-nextjs = Repository name
    • 0.1.0 = Semantic version tag
  • --platform linux/amd64,linux/arm64
    • Builds two separate images in parallel:
    • linux/amd64: For Intel/AMD processors (most cloud servers)
    • linux/arm64: For Apple Silicon (M1/M2) and ARM servers
    • Creates a manifest list that automatically serves the correct image
  • --push
    • Immediately pushes both images + manifest to Docker Hub
    • Without this, images only exist locally (useful for testing)
  • .
    • The build context (where your Dockerfile lives)
    • Docker includes all files in this directory by default

Installation

Build from Source

  1. Clone the repository:
git clone https://github.com/Isaac-Fate/file-server-nextjs.git
Enter fullscreen mode Exit fullscreen mode
  1. Check out the specific tag:
git checkout v0.1.0
Enter fullscreen mode Exit fullscreen mode
  1. Set the environment variables in .env.production:
cp .env.example .env.production
Enter fullscreen mode Exit fullscreen mode

Fill in the missing environment variables.

  1. Create docker-compose.yml file:
cp docker-compose.example.yml docker-compose.yml
Enter fullscreen mode Exit fullscreen mode

Fill in the host port and the host file storage directory you want to use.

  1. Build the image and run the container:
docker-compose up -d
Enter fullscreen mode Exit fullscreen mode

Use the Pre-Built Docker Image from Docker Hub

The pre-built Docker image isaacfei/file-server-nextjs:0.1.0 is available on Docker Hub. To use it:

Modify the docker-compose.yml:

  • Remove the build section
  • Add the image specification
# docker-compose.yml

version: "3.8"

services:
  app:
-    build:
-      context: .
-      dockerfile: Dockerfile
-      args:
-        - NODE_ENV=production
+    image: isaacfei/file-server-nextjs:0.1.0
Enter fullscreen mode Exit fullscreen mode

All other configurations remain unchanged.
Set the environment variables in .env.production and run docker-compose up -d as usual.

API Reference

Authentication

Include in headers:

Authorization: Bearer <API_KEY>
Enter fullscreen mode Exit fullscreen mode

Endpoints

Method Endpoint Description
GET /api/files?path=<DIR> List directory contents
GET /api/download/<PATH> Download a file

List files and directories under a given subdirectory path:

GET /api/files?path=<DIRECTORY_PATH> HTTP/1.1
Host: <HOST_NAME>:<HOST_PORT>
Authorization: Bearer <API_KEY>
Enter fullscreen mode Exit fullscreen mode

Download a file:

GET /api/download/<DIRECTORY_PATH>/<FILE_NAME> HTTP/1.1
Host: <HOST_NAME>:<HOST_PORT>
Authorization: Bearer <API_KEY>
Enter fullscreen mode Exit fullscreen mode

Examples

Suppose that I am running the container on my local machine and the host name is localhost and the port is 3000.
And suppose that the tree view of my file storage root directory data is like this:

data/
├── hello.txt
└── logs/
    └── abc.log
Enter fullscreen mode Exit fullscreen mode

To download the file hello.txt, I can send the following request:

GET /api/download/hello.txt HTTP/1.1
Host: localhost:3000
Authorization: Bearer <API_KEY>
Enter fullscreen mode Exit fullscreen mode

To download the file logs/abc.log:

GET /api/download/logs/abc.log HTTP/1.1
Host: localhost:3000
Authorization: Bearer <API_KEY>
Enter fullscreen mode Exit fullscreen mode

To list files and directories under data, I can send the following request:

GET /api/files HTTP/1.1
Host: localhost:3000
Authorization: Bearer <API_KEY>
Enter fullscreen mode Exit fullscreen mode

or

GET /api/files?path=/ HTTP/1.1
Host: localhost:3000
Authorization: Bearer <API_KEY>
Enter fullscreen mode Exit fullscreen mode

To list files and directories under the subdirectory logs:

GET /api/files?path=logs HTTP/1.1
Host: localhost:3000
Authorization: Bearer <API_KEY>
Enter fullscreen mode Exit fullscreen mode

Top comments (0)