+6

Thư viện S3Local: Làm thế nào để giả lập AWS S3 bằng NodeJS không cần dựa vào thư viện bên thứ 3?

Mayfest2023

1. Giới thiệu

Hé lô các bạn! Hôm nay, mình sẽ giới thiệu cho các bạn về thư viện S3Local mà mình đã tự tay xây dựng. S3Local giả lập lại AWS S3 trên NodeJS mà không cần dựa vào thư viện bên thứ 3. Có 2 lý do mà mình đã quyết định tự làm nó: đầu tiên, mình không muốn phụ thuộc vào thư viện bên thứ 3 vì không biết chúng có thể mang lại những lỗ hổng bảo mật không kiểm soát được. Thứ hai, đơn giản là vì mình thích làm thế. Điều này cũng giúp mình nắm bắt được rõ hơn cách hoạt động của S3 thông qua một ví dụ siêu đơn giản (Tất nhiên trong thực tế nó sẽ fancy hơn vậy gấp 100 lần - tuy nhiên learn mà cứ giễ mà phang trước đã). Việc ứng dụng vào thực tế sử dụng thì còn phải sửa chữa nhiểu, tuy nhiên bài này mình cũng muốn lồng ghép để giới thiệu Command Design Pattern trá hình luôn 🤣.

image.png

1.1 Cài đặt ban đầu

Không lạ gì với ai đang làm việc với NodeJS, chúng ta sẽ bắt đầu bằng cách khởi tạo một project mới. Sử dụng command sau trong terminal của bạn:

npm init -y

Cuối cùng, thêm dòng "type": "module" vào file package.json của bạn. Và nó sẽ trông như thế này:

{
  "name": "s3local",
  "version": "1.0.0",
  "description": "",
  "main": "app.js",
  "type": "module",
  "scripts": {
    "dev": "node app.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

image.png

2. S3Local thư viện của chúng ta

2.1 Tổng quan

Thư viện S3Local gồm 2 file chính: s3_local.jsapp.js.

2.2 File s3_local.js

Đầu tiên, hãy cùng tìm hiểu về file s3_local.js.

Đây là nơi mà mình đã định nghĩa ra S3Local cùng với các command để thao tác với các bucket. Mình đã áp dụng Command Design Pattern vào đây để tạo ra một cấu trúc rõ ràng và giúp dễ dàng mở rộng hơn trong tương lai.

import fs from "fs";
import path from "path";

class Command {
  constructor(bucket) {
    this.bucket = bucket;
  }

  async execute() {
    throw new Error("Execute method must be overridden in the child class");
  }
}

class GetObjectCommand extends Command {
  async execute(s3Local) {
    return await s3Local.get(this.bucket);
  }
}

class PutObjectCommand extends Command {
  async execute(s3Local, file) {
    return await s3Local.put(this.bucket, file);
  }
}

class DeleteObjectCommand extends Command {
  async execute(s3Local) {
    return await s3Local.delete(this.bucket);
  }
}

class S3Local {
  constructor(config) {
    this.path = config.path;
    this.buckets = [];
  }

  getBucketPath(bucket) {
    return bucket.Bucket || "" + bucket.Key || "";
  }

  async get(bucket) {
    const filePath = path.join(this.path, this.getBucketPath(bucket));
    return await fs.promises.readFile(filePath, "utf-8");
  }

  async put(bucket, file) {
    const filePath = path.join(this.path, this.getBucketPath(bucket));
    await fs.promises.writeFile(filePath, file);
    this.buckets.push(bucket);
  }

  async delete(bucket) {
    const filePath = path.join(this.path, this.getBucketPath(bucket));
    await fs.promises.unlink(filePath);
    this.buckets = this.buckets.filter((b) => this.getBucketPath(b) !== this.getBucketPath(bucket));
  }

  async send(command, file) {
    return await command.execute(this, file);
  }
}

export { GetObjectCommand, PutObjectCommand, DeleteObjectCommand, S3Local };

Tiếp theo mình sẽ giải thích kỹ từng thành phần của thư viện nhé

2.2.1 Class Command

Class Command là class cha, nó chứa phương thức execute() mà các class con sẽ phải ghi đè để thực hiện các thao tác khác nhau.

2.2.2 Class GetObjectCommand, PutObjectCommand, DeleteObjectCommand

Đây là các class con, mỗi class sẽ ghi đè phương thức execute() để thực hiện việc lấy dữ liệu từ bucket (GetObjectCommand), đặt dữ liệu vào bucket (PutObjectCommand), hoặc xóa bucket (DeleteObjectCommand).

2.2.3 Class S3Local

Class S3Local chính là trung tâm của thư viện mình viết. Nó chứa các phương thức cơ bản như get(), put(), và delete(). Bên cạnh đó, mình còn thêm phương thức send() để thực hiện các command mà chúng ta đã định nghĩa ở trên.

image.png

3. Sử dụng thư viện S3Local

Mình đã giới thiệu sơ qua về S3Local, bây giờ hãy cùng xem cách sử dụng nó như thế nào qua file app.js.

import { DeleteObjectCommand, GetObjectCommand, PutObjectCommand, S3Local } from "./s3_local.js";

// Tạo một instance của S3Local
const config = {
  path: ".", // Đường dẫn thư mục lưu trữ dữ liệu
};
const s3Local = new S3Local(config);

// Tạo một bucket mới
const bucket = {
  Key: "file của tôi.txt",
};

// Thực hiện các command
(async () => {
  try {
    // Ghi dữ liệu vào bucket
    const fileContent = "Nội dung file test.";
    await s3Local.send(new PutObjectCommand(bucket), fileContent);

    // Đọc dữ liệu từ bucket
    const result = await s3Local.send(new GetObjectCommand(bucket));
    console.log("Ghi và đọc dữ liệu thành công >> Nội dung file là:", result);

    // Xóa bucket
    await s3Local.send(new DeleteObjectCommand(bucket));

    // Kiểm tra xem bucket đã bị xóa thành công hay chưa
    try {
      await s3Local.send(new GetObjectCommand(bucket));
    } catch (error) {
      console.log("Xóa Bucket thành công.");
    }

    // Ghi dữ liệu Backup
    await s3Local.send(new PutObjectCommand({...bucket, Key: 'file backup.txt'}), fileContent + '.... Backup');
  } catch (error) {
    console.error("Lỗi zồi:", error);
  }
})();

3.1 Khởi tạo S3Local

Đầu tiên, chúng ta sẽ khởi tạo một instance của S3Local với config chỉ ra đường dẫn đến thư mục mà chúng ta sẽ lưu trữ dữ liệu.

3.2 Thực hiện các command

Sau khi đã có S3Local, giờ đến lúc chúng ta sẽ tận dụng nó để thực hiện các thao tác với dữ liệu. Qua hàm send(), chúng ta có thể gửi các command khác nhau như PutObjectCommand, GetObjectCommand, DeleteObjectCommand để thực hiện việc đặt, lấy, hoặc xóa dữ liệu từ bucket.

image.png

4. Kết luận

Đó là tất cả những gì về S3Local. Mình hy vọng rằng thông qua việc tự tay xây dựng một thư viện như S3Local, các bạn đã hiểu rõ hơn về cách thức hoạt động của AWS S3 cũng như việc áp dụng Command Design Pattern trong việc thiết kế thư viện. Chắc chắn còn nhiều điều mà mình chưa kịp chia sẻ trong bài viết này. Không nói đến thư viện aws s3 real, thư viện ở trên khi áp dụng vào dự án thực tế hiện tại của mình thì cũng sẽ được xào nấu lại khá nhiều cho phù hợp với logic dự án. Tuy nhiên về mặt ý tưởng nó vẫn như vậy. Chúc các bạn thành công.


日本語バジョン

1. 自己紹介

こんにちは、みなさん!今日は、私が一から作ったS3Localというライブラリーについて紹介します。S3Localは、AWS S3をサードパーティーのライブラリーに頼ることなくNodeJSでエミュレートします。これを自分で作ろうと思ったのは2つの理由があります。まず一つ目は、サードパーティーのライブラリーに依存したくなかったからです。なぜなら、それらが未検出のセキュリティホールを生むかどうかわからなかったからです。二つ目の理由は、単純にそれが好きだからです。これによって、超シンプルな例を通じてS3の動作をより深く理解することができました(もちろん、実際にはこれよりも100倍複雑ですが、とりあえず学びながら進めていきましょう)。現実の応用にはまだ多くの修正が必要ですが、この記事では隠れているCommand Design Patternの紹介も交えていきたいと思います。

image.png

1.1 初期設定

NodeJSを使っている方なら誰でも知っていることですが、新しいプロジェクトを始めるには以下のコマンドを端末で実行します。

npm init -y

最後に、"type": "module"という行を package.json ファイルに追加します。それにより、ファイルは次のようになります:

{
  "name": "s3local",
  "version": "1.0.0",
  "description": "",
  "main": "app.js",
  "type": "module",
  "scripts": {
    "dev": "node app.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

image.png

2. 我々のS3Localライブラリ

2.1 概要

S3Localライブラリは主に2つのファイルで構成されています: s3_local.jsapp.js です。

2.2 s3_local.js ファイル

まずは s3_local.js ファイルについて詳しく見ていきましょう。

ここは、S3Localとそれに関連するバケット操作のコマンドを定義した場所です。ここでCommand Design Patternを使って、明確な構造を作り、将来的に容易に拡張できるようにしました。

import fs from "fs";
import path from "path";

// コマンドクラス
class Command {
  constructor(bucket) {
    this.bucket = bucket;
  }

  // 実行メソッド(子クラスでオーバーライドする必要があります)
  async execute() {
    throw new Error("子クラスでexecuteメソッドをオーバーライドする必要があります");
  }
}

// オブジェクト取得コマンドクラス
class GetObjectCommand extends Command {
  async execute(s3Local) {
    return await s3Local.get(this.bucket);
  }
}

// オブジェクト保存コマンドクラス
class PutObjectCommand extends Command {
  async execute(s3Local, file) {
    return await s3Local.put(this.bucket, file);
  }
}

// オブジェクト削除コマンドクラス
class DeleteObjectCommand extends Command {
  async execute(s3Local) {
    return await s3Local.delete(this.bucket);
  }
}

// S3ローカルクラス
class S3Local {
  constructor(config) {
    this.path = config.path;
    this.buckets = [];
  }

  // バケットのパスを取得
  getBucketPath(bucket) {
    return (bucket.Bucket || "") + (bucket.Key || "");
  }

  // オブジェクトの取得
  async get(bucket) {
    const filePath = path.join(this.path, this.getBucketPath(bucket));
    return await fs.promises.readFile(filePath, "utf-8");
  }

  // オブジェクトの保存
  async put(bucket, file) {
    const filePath = path.join(this.path, this.getBucketPath(bucket));
    await fs.promises.writeFile(filePath, file);
    this.buckets.push(bucket);
  }

  // オブジェクトの削除
  async delete(bucket) {
    const filePath = path.join(this.path, this.getBucketPath(bucket));
    await fs.promises.unlink(filePath);
    this.buckets = this.buckets.filter((b) => this.getBucketPath(b) !== this.getBucketPath(bucket));
  }

  // コマンドの実行
  async send(command, file) {
    return await command.execute(this, file);
  }
}

export { GetObjectCommand, PutObjectCommand, DeleteObjectCommand, S3Local };

次に、ライブラリの各コンポーネントについて詳しく説明していきます。

2.2.1 Commandクラス

Commandクラスは親クラスで、execute()メソッドを持っています。このメソッドは、各子クラスがオーバーライドして異なる操作を行うためのものです。

2.2.2 GetObjectCommand、PutObjectCommand、DeleteObjectCommandクラス

これらは子クラスで、それぞれが execute() メソッドをオーバーライドしてバケットからデータを取得(GetObjectCommand)、バケットにデータを設定(PutObjectCommand)、バケットを削除(DeleteObjectCommand)する操作を行います。

2.2.3 S3Localクラス

S3Localクラスは、私が書いたライブラリの中心部です。基本的なメソッド、get(), put(), delete() を含みます。さらに、上記で定義したコマンドを実行する send() メソッドも追加しました。

image.png

3. S3Localライブラリの使用

S3Localについて簡単に紹介しましたが、app.js ファイルを通じてどのように使用するか見てみましょう。

import { DeleteObjectCommand, GetObjectCommand, PutObjectCommand, S3Local } from "./s3_local.js";

// S3Localのインスタンスを作成します
const config = {
  path: ".", // データの保存先ディレクトリのパス
};
const s3Local = new S3Local(config);

// 新しいバケットを作成します
const bucket = {
  Key: "私のファイル.txt",
};

// コマンドを実行します
(async () => {
  try {
    // バケットにデータを書き込みます
    const fileContent = "テストファイルの内容です。";
    await s3Local.send(new PutObjectCommand(bucket), fileContent);

    // バケットからデータを読み込みます
    const result = await s3Local.send(new GetObjectCommand(bucket));
    console.log("データの書き込みと読み込みに成功しました >> ファイルの内容は:", result);

    // バケットを削除します
    await s3Local.send(new DeleteObjectCommand(bucket));

    // バケットが正常に削除されたかどうかを確認します
    try {
      await s3Local.send(new GetObjectCommand(bucket));
    } catch (error) {
      console.log("バケットの削除に成功しました。");
    }

    // バックアップデータを書き込みます
    await s3Local.send(new PutObjectCommand({...bucket, Key: 'バックアップファイル.txt'}), fileContent + '.... バックアップ');
  } catch (error) {
    console.error("エラーが発生しました:", error);
  }
})();

3.1 S3Localの初期化

まず、データを保存するディレクトリへのパスを指定した設定を持つS3Localのインスタンスを作成します。

3.2 コマンドの実行

S3Localができたら、それを使ってデータ操作を行います。send() メソッドを通じて、PutObjectCommand, GetObjectCommand, DeleteObjectCommand といったさまざまなコマンドを送り、バケットにデータを設定したり、取得したり、削除したりします。

image.png

4. まとめ

これがS3Localについてのすべてです。S3Localのようなライブラリを自分で作ることで、AWS S3の動作やCommand Design Patternのライブラリ設計への応用について深く理解できたことを願っています。この記事ではまだ伝えきれていないことがたくさんあると思います。実際のaws s3ライブラリーを語るまでもなく、現在のプロジェクトに上記のライブラリーを適用する際には、プロジェクトのロジックに合わせて大

幅に改変する必要があります。しかし、そのアイデアはそのままです。みなさんの成功を祈っています。

Mình hy vọng bạn thích bài viết này và học thêm được điều gì đó mới.

Donate mình một ly cafe hoặc 1 cây bút bi để mình có thêm động lực cho ra nhiều bài viết hay và chất lượng hơn trong tương lai nhé. À mà nếu bạn có bất kỳ câu hỏi nào thì đừng ngại comment hoặc liên hệ mình qua: Zalo - 0374226770 hoặc Facebook. Mình xin cảm ơn.

Momo: NGUYỄN ANH TUẤN - 0374226770

TPBank: NGUYỄN ANH TUẤN - 0374226770 (hoặc 01681423001)

image.png


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí